This guide explains how QuantFolio works under the hood. It covers the architecture, dependencies, data flow, and key algorithms. If you want to understand the codebase, extend it, or debug issues, start here.
QuantFolio uses a layered architecture that separates concerns. At the bottom, you have data persistence. Above that, business logic and algorithms. At the top, presentation layers (GUI and CLI). This separation makes the code testable, maintainable, and easier to understand.
The solution contains four main projects. QuantFolio.Core holds the business logic, models, and strategy implementations. QuantFolio.Data handles database operations and external API calls. QuantFolio.GUI provides the WPF desktop interface. QuantFolio.CLI offers a command-line alternative. Tests live in QuantFolio.Tests covering the entire stack.
Each layer talks to the layer below through interfaces. This means you can swap implementations without breaking things. For example, the optimizer doesn't know whether data comes from SQLite, PostgreSQL, or a mock—it just calls IMarketDataRepository methods.
The project runs on .NET 8.0, Microsoft's latest cross-platform runtime. This gives us performance improvements, nullable reference types for safer code, and modern C# 12 features. The framework handles dependency injection, configuration, and async operations out of the box.
For data persistence, we use Entity Framework Core 8.0 with SQLite. EF Core is an ORM that maps C# objects to database tables. You define models like Portfolio or Deal, and EF generates the SQL. SQLite keeps things simple—no server required, just a file in outputs/database/. This works great for single-user desktop apps and makes deployment trivial.
The GUI uses Windows Presentation Foundation (WPF) with the MVVM pattern. WPF is Microsoft's mature desktop framework with powerful data binding and styling. MVVM separates views (XAML) from logic (ViewModels), making the UI testable. We use CommunityToolkit.Mvvm for observable properties and commands, which cuts boilerplate significantly. Charts come from LiveChartsCore, a modern charting library that handles time series and asset allocation visualizations smoothly.
The CLI leverages Spectre.Console, which provides rich terminal UI components like tables, progress bars, and colored output. This makes command-line usage actually pleasant instead of staring at plain text dumps.
For external data, we call three APIs: Alpha Vantage (primary), Yahoo Finance (secondary), and Financial Modeling Prep (backup). The system automatically falls back if one fails. API calls use the standard HttpClient with proper async/await patterns to avoid blocking the UI thread.
The src/ directory contains the core business logic. Inside, you'll find several subdirectories organized by responsibility.
src/core/ holds the domain models, interfaces, and services. Models like Portfolio, Deal, Position, and MarketData represent the business entities. These are POCOs (Plain Old C# Objects) with properties and navigation links. Interfaces define contracts—IPortfolioOptimizer, IDataCollector, IBacktester—that implementations must follow. Services like StrategyFactory and RiskMetrics provide shared functionality used across strategies.
src/strategy/ implements the three quantitative strategies. Each strategy (LowRiskStrategy, LowTurnoverStrategy, HighYieldStrategy) implements IPortfolioOptimizer. They take historical market data, train on it to learn patterns, then generate optimal asset weights. The PortfolioOptimizer class orchestrates this process—it fetches data, calls the strategy, simulates trades, and calculates performance metrics.
src/data/ manages persistence and external data. PortfolioDbContext is the EF Core context defining the database schema. DataCollector fetches market data from APIs. DatabaseManager provides high-level operations like seeding initial data. Repositories (PortfolioRepository, DealRepository, etc.) wrap database queries in a clean interface.
src/backtesting/ contains backtesting logic. Backtester runs strategies on historical data to see how they would have performed. WalkForwardAnalysis implements walk-forward testing—train on period A, test on period B, then roll forward. MetricsCalculator computes performance metrics like Sharpe ratio, maximum drawdown, and volatility.
src/utils/ has utilities like configuration management. ConfigManager loads settings from appsettings.json, handling database paths, API keys, and strategy parameters.
src/visualization/ handles chart generation and report exports. This includes equity curve plots, allocation pie charts, and PDF report generation.
The gui/ directory contains the WPF application. Views are XAML files defining the UI layout. ViewModels contain the presentation logic and expose data to views through bindings. Converters transform data for display (like converting booleans to visibility or numbers to formatted strings).
The cli/ directory houses the command-line interface. Commands are organized by function—PortfolioCommand, OptimizationCommand, DataCommand. Each command class handles argument parsing, calls core services, and displays results using Spectre.Console.
The database schema centers around portfolios. A Portfolio belongs to a Client (the investor) and is managed by a Manager (the advisor). Each portfolio has a strategy ("Low Risk", "High Yield Equity") and an asset universe ("US Equities", "Global Bonds"). These choices determine which securities the optimizer considers.
Portfolios have three main collections. Deals track all trades—every buy and sell order with date, symbol, quantity, price, and total amount. This forms the complete trading history. Positions represent current holdings—what the portfolio owns right now. Each position has a product (security), shares held, average cost basis, and current market value. PerformanceMetrics store daily snapshots of portfolio value, return, volatility, and other metrics over time.
Products represent tradable securities. Each product has a symbol (like "AAPL"), name, type (Stock, ETF, Bond), region (US, Europe, Asia), and sector (Technology, Healthcare). When you run an optimization, the strategy filters products based on the portfolio's asset universe. For example, "US Equities" only considers products where type is Stock or ETF and region is United States.
MarketData stores historical prices. For each product and date, we store open, high, low, close prices and volume (OHLCV). This is what strategies train on to learn patterns. The data comes from external APIs, but once fetched, it's cached locally to avoid redundant calls.
Entity Framework manages relationships. Client → Portfolios is one-to-many (one client can have multiple portfolios). Portfolio → Deals is one-to-many with cascade delete (deleting a portfolio removes all its trades). Portfolio → Product through Positions is many-to-many (portfolios can own multiple products, products can be in multiple portfolios).
All three strategies implement the IPortfolioOptimizer interface, which defines two key methods: TrainAsync and OptimizeAsync.
Training happens first. The optimizer receives historical market data for all products in the asset universe. It calculates daily returns, volatility, correlations, and expected returns. This statistical analysis captures how assets behaved in the past and how they moved together. Different strategies focus on different metrics—Low Risk emphasizes correlations and volatility, High Yield focuses on dividend yields and price stability.
After training, optimization runs. The strategy generates target weights for each asset. Weights are decimals summing to 1.0, representing portfolio allocation (0.15 = 15% of capital). The algorithm tries different weight combinations to maximize the objective function while respecting constraints.
Low Risk Strategy minimizes portfolio variance. It uses the covariance matrix—a mathematical representation of how assets move together. The strategy assigns higher weights to low-volatility assets and diversifies across uncorrelated securities. It targets 10% annualized volatility with strict limits: no position exceeds 20%, minimum position is 2%. The optimizer runs 150 iterations of gradient descent to find weights that minimize variance while staying near the target volatility.
Low Turnover Strategy minimizes rebalancing frequency to reduce transaction costs. It starts with market-cap weighting, then slowly adjusts based on momentum signals. The key constraint is turnover—it won't make changes unless expected improvement exceeds transaction costs. Position limits are 5-30%, encouraging concentration in high-conviction picks. The strategy updates every 30 days unless market conditions drastically change.
High Yield Equity Strategy maximizes dividend income while maintaining reasonable volatility. It ranks stocks by dividend yield, filters out volatility outliers (>30% volatility), and weights positions by yield. Rebalancing happens quarterly to capture new dividend announcements. Position limits are 3-25%, balancing diversification with yield focus.
Each strategy includes filters to avoid bad data. If a security has less than 60 days of history, it's excluded. If prices show suspicious patterns (all zeros, extreme spikes), that data is cleaned or rejected. This prevents garbage-in-garbage-out scenarios where one bad data point ruins the whole optimization.
When you click "Optimize Portfolio" in the GUI, here's what happens step by step.
First, the system fetches the portfolio configuration from the database. This tells us the strategy type, asset universe, client, manager, and initial capital. Then it determines which products to consider by filtering the Products table based on asset universe. For "US Equities", only US stocks and ETFs make the cut.
Next, market data gets loaded. The system queries the MarketData table for all relevant products within the training period. If data is missing, it fetches from external APIs (Alpha Vantage, Yahoo Finance, or FMP). This can take seconds to minutes depending on how much is cached. Progress updates display in the UI so you know it's working.
With data ready, the appropriate strategy is instantiated using StrategyFactory. The factory pattern makes it easy to add new strategies—just register them in the factory, and they become available throughout the app. The selected strategy receives the market data and calls TrainAsync. Training calculates all statistics needed for optimization—returns, volatilities, correlations, yields, momentum factors.
After training, OptimizeAsync runs. The strategy generates target weights and returns them. The PortfolioOptimizer then converts weights into actual trades. If the portfolio currently holds nothing, it generates buy orders for each position based on initial capital. If the portfolio has existing positions, it calculates the difference between current and target allocation, creating buy or sell orders to rebalance.
Trades get simulated chronologically. For each trade, the system updates cash balance and positions. Buy orders reduce cash by (price × shares). Sell orders increase cash by (price × shares). Position quantities adjust accordingly. This simulation runs through the entire testing period, applying trades and tracking portfolio value daily.
Finally, performance metrics are calculated. MetricsCalculator computes total return, Sharpe ratio, maximum drawdown, volatility, and other statistics. It also generates the equity curve—a time series of portfolio value. All results package into an OptimizationResult object that gets displayed in the GUI or saved to the database.
Entity Framework Core handles all database operations. The PortfolioDbContext defines the schema using the Code-First approach. You define C# classes, and EF generates the database tables to match.
When you call _context.Portfolios.Include(p => p.Client).ToListAsync(), EF translates this into SQL:
SELECT * FROM Portfolios
JOIN Clients ON Portfolios.ClientId = Clients.IdThe Include ensures related entities load eagerly instead of lazy-loading later (which can cause N+1 query problems).
Repositories wrap the DbContext in a cleaner interface. Instead of controllers directly accessing _context.Portfolios, they call _portfolioRepository.GetByIdAsync(id). This abstraction makes testing easier—you can mock the repository without setting up a real database. It also centralizes query logic. If every place that needs portfolios writes its own query, inconsistencies creep in. With a repository, there's one canonical implementation.
Migrations manage schema changes. When you modify a model (add a property, change a relationship), you generate a migration using dotnet ef migrations add. This creates a file with Up and Down methods describing how to evolve the schema. Running dotnet ef database update applies pending migrations. This keeps database schema in sync with code across development, testing, and production.
For performance, indexes are critical. The schema includes indexes on frequently-queried columns like Symbol, Date, and foreign keys like PortfolioId. Composite indexes optimize queries that filter by multiple columns, such as "all market data for product X between dates Y and Z". Without indexes, such queries scan the entire table, which gets slow once you have thousands of records.
Transaction handling ensures data consistency. When creating a portfolio with initial trades, everything succeeds or nothing does. If trade insertion fails, the portfolio creation rolls back. EF Core supports this through _context.Database.BeginTransactionAsync(). The repository pattern makes it easy to wrap multiple operations in a transaction.
The DataCollector fetches market data from external APIs. It implements a three-tier fallback strategy for reliability. Alpha Vantage is tried first because it provides consistent daily data with a simple API. If that fails (rate limit, downtime, invalid symbol), the collector falls back to Yahoo Finance. Yahoo's CSV format requires more parsing but is very reliable. If both fail, Financial Modeling Prep serves as the final backup.
Caching happens at two levels. The database is the primary cache. Before making an API call, DataCollector checks if the requested data already exists in the MarketData table. If yes, it returns that immediately. This avoids redundant API calls and speeds up subsequent optimizations dramatically. For a portfolio optimized yesterday, re-running it today hits the cache for 99% of data.
The second cache is in-memory with a 15-minute expiry. This handles repeated requests within a short window. If you fetch data for AAPL at 10:00, then fetch again at 10:05, the in-memory cache returns it instantly without even hitting the database. This is useful during interactive exploration where you might tweak parameters and re-run optimizations multiple times.
Rate limiting prevents API bans. Free tier API keys typically allow 5-25 requests per minute. The collector introduces a 500ms delay between requests in batch operations. For fetching 20 symbols, that's 10 seconds total—annoying but acceptable, and it keeps you within limits. Premium API keys can reduce or eliminate this delay.
Error handling is robust. If an API returns invalid JSON, the parser catches it and tries the next provider. If a symbol doesn't exist, the collector logs it and continues with other symbols rather than aborting the entire batch. Network timeouts use exponential backoff—first retry after 1 second, then 2 seconds, then 4 seconds. After three failures, it gives up on that source and tries the next.
Data validation happens post-fetch. OHLCV data must satisfy basic sanity checks: close price within high-low range, volume non-negative, no null values. If validation fails, that data point is discarded. This prevents corrupted data from polluting optimizations.
Backtesting simulates how a strategy would have performed historically. It's critical for evaluating whether a strategy actually works or just looks good on paper.
The Backtester class orchestrates this. You provide a strategy, a set of products, a date range, and initial capital. The backtester divides time into training and testing periods. The strategy trains on the training period (learning patterns), then gets evaluated on the testing period (seeing if patterns hold).
Walk-forward analysis is more sophisticated. Instead of one training period and one testing period, it uses a rolling window. Train on year 1, test on year 2. Then train on year 2, test on year 3. And so on. This simulates real-world deployment where you continuously retrain models on fresh data. It also prevents overfitting—if a strategy only works on one specific historical period, walk-forward analysis reveals that.
During backtesting, trades execute at historical prices. If the strategy decides to buy AAPL on 2020-05-15, the backtest uses the actual close price from that date. This is realistic because in practice, you'd place market orders at end-of-day. Slippage and transaction costs can be modeled too, though the current implementation assumes zero transaction costs (something to improve for production use).
Performance metrics come from MetricsCalculator. It takes the equity curve (portfolio value over time) and computes statistics. Total return is simple: (final_value - initial_value) / initial_value. Sharpe ratio measures risk-adjusted returns: (average_return - risk_free_rate) / volatility. Higher is better. Values above 1.0 are good, above 2.0 are excellent. Maximum drawdown finds the worst peak-to-trough decline. If your portfolio hit $120k then dropped to $90k, that's a 25% drawdown. This metric matters psychologically—investors panic during drawdowns and sell at the bottom.
Volatility is the annualized standard deviation of returns. Lower volatility means smoother rides. Win rate measures the percentage of days with positive returns. Average trade shows the mean profit/loss per trade. These metrics combine to give a comprehensive view of strategy performance.
The WPF GUI uses the Model-View-ViewModel pattern. This separates concerns clearly: Models hold data, Views display data, ViewModels bridge the two with logic.
Models are the domain objects from QuantFolio.Core. Things like Portfolio, Deal, Client. These are the "business" objects that exist independently of the UI.
Views are XAML files defining layout and styling. They have no logic beyond what WPF requires for bindings. A view might declare a DataGrid, ListBox, or Chart, then bind its properties to ViewModel properties. For example, ItemsSource="{Binding Portfolios}" tells the DataGrid to display whatever collection the ViewModel exposes as Portfolios.
ViewModels contain presentation logic. They expose observable properties that views bind to. When a property changes, it raises PropertyChanged events, and the UI auto-updates. Commands handle user interactions—clicking a button invokes a command on the ViewModel, which then calls core services. ViewModels also manage navigation between views.
CommunityToolkit.Mvvm simplifies this significantly. Instead of writing boilerplate INotifyPropertyChanged implementations, you use attributes like [ObservableProperty]. The toolkit generates the necessary code at compile time. Similarly, [RelayCommand] generates command implementations. This keeps ViewModels clean and focused on logic rather than plumbing.
Dependency injection wires everything together. MainWindow receives a ViewModel via constructor injection. The ViewModel receives services (repositories, optimizers) via its constructor. This makes the whole app testable—you can inject mocks for any dependency.
Data binding is two-way. When you type in a TextBox bound to PortfolioName, the ViewModel's property updates automatically. When the ViewModel's property changes programmatically, the TextBox reflects the new value. This keeps UI and data in sync without manual updates.
The CLI uses a different pattern. Commands are implemented as classes inheriting from a base command class. Each command defines arguments, options, and execution logic.
Spectre.Console provides rich terminal output. Instead of Console.WriteLine, you use AnsiConsole.MarkupLine("[green]Success![/green]") for colored text. Tables format data nicely:
var table = new Table();
table.AddColumn("Portfolio");
table.AddColumn("Return");
foreach (var p in portfolios)
table.AddRow(p.Name, $"{p.Return:P2}");
AnsiConsole.Write(table);This renders a formatted table in the terminal, handling column width and alignment automatically.
The CLI reuses all core services. It doesn't reimplement optimization or data collection. It just calls PortfolioOptimizer, gets results, and formats them for terminal display. This ensures consistency—the same algorithms power both UI and CLI.
Commands are registered in Program.cs using a command-line parser. When you run quantfolio optimize --portfolio 5, the parser routes to OptimizationCommand, passing portfolio ID 5 as an argument. The command retrieves the portfolio, runs optimization, and displays results.
Error handling in CLI focuses on user-friendly messages. If optimization fails, instead of dumping a stack trace, it shows "Error: Insufficient market data for symbol AAPL. Fetch data first with 'quantfolio data fetch --symbol AAPL'."
Configuration comes from appsettings.json at the project root. This file contains database paths, API keys, default strategy parameters, and feature flags. The ConfigManager class loads this using Microsoft.Extensions.Configuration.
Environment-specific overrides are supported. You can have appsettings.Development.json for local dev and appsettings.Production.json for release. The configuration system merges them, with more specific settings overriding defaults.
User secrets handle sensitive data like API keys. Instead of committing keys to version control, you store them separately using dotnet user-secrets set AlphaVantage:ApiKey "yourkey". In code, accessing _configuration["AlphaVantage:ApiKey"] retrieves it securely.
The configuration is strongly-typed. Instead of sprinkling _configuration["Database:Path"] throughout the code, you bind configuration sections to classes:
public class DatabaseConfig
{
public string Path { get; set; }
public bool EnableLogging { get; set; }
}
var dbConfig = new DatabaseConfig();
_configuration.GetSection("Database").Bind(dbConfig);Now you access dbConfig.Path with IntelliSense and compile-time checking.
The test suite has tests covering models, repositories, strategies, backtesting, and services. Tests use xUnit as the framework and Moq for mocking dependencies.
Unit tests isolate single classes. For example, LowRiskStrategyTests tests the strategy in isolation. It creates mock market data, calls TrainAsync and OptimizeAsync, then asserts the output matches expectations. Mocks prevent tests from hitting real databases or APIs, making them fast and deterministic.
Integration tests verify multiple components work together. DatabaseIntegrationTests creates an in-memory SQLite database, seeds test data, runs queries through repositories, and verifies results. These catch issues like incorrect SQL generation or missing foreign keys.
Backtesting tests use known historical scenarios. For instance, "Given market data from 2020 crash, Low Risk strategy should limit drawdown to under 20%". These tests encode business requirements and catch regressions.
Test organization mirrors production code. Tests for PortfolioOptimizer live in tests/strategies/PortfolioOptimizerTests.cs. This makes it easy to find relevant tests when modifying code.
Optimization algorithms can be compute-intensive. Calculating covariance matrices, running gradient descent, and simulating trades across thousands of days adds up. Async/await keeps the UI responsive by running heavy computations on background threads.
Database queries use eager loading (Include) to avoid N+1 problems. Instead of loading a portfolio then making 10 separate queries for its deals, one query fetches everything. Indexes on foreign keys and date columns speed up common queries.
Caching dramatically improves performance. The first optimization run might take 2 minutes fetching data. Subsequent runs with cached data finish in under 10 seconds. The trade-off is stale data—if market conditions changed drastically today, yesterday's cached data won't reflect that. For most backtesting use cases, this is fine since you're analyzing historical periods.
Memory usage stays reasonable. Market data is loaded per-optimization, not all at once globally. After optimization completes, garbage collection reclaims memory. For very large universes (500+ symbols, 10+ years of data), you might hit memory limits. The solution is to batch process or increase RAM.
The biggest bottleneck is external API calls. Alpha Vantage free tier allows 5 requests/minute, so fetching data for 50 symbols takes 10 minutes. Upgrading to a paid API key removes this limit. Alternatively, pre-fetch all data once during setup, then work entirely from cache.
Want to add a new strategy? Implement IPortfolioOptimizer. The interface requires TrainAsync (analyze historical data) and OptimizeAsync (generate weights). Register your strategy in StrategyFactory, and it becomes available everywhere—GUI dropdowns, CLI commands, backtests.
Need a different database? Swap the DbContextOptionsBuilder configuration from SQLite to PostgreSQL or SQL Server. Since all data access goes through repositories and EF Core, no other code changes are needed.
Want to add a new data provider? Create a class implementing the fetch logic, then add it to the DataCollector fallback chain. The existing caching and error handling work automatically.
Custom metrics? Add a method to MetricsCalculator. It receives the equity curve and returns a double. Then expose it in OptimizationResult and display it in the GUI.
The architecture makes extensions straightforward by following SOLID principles. Dependencies point inward (UI depends on Core, not vice versa). Interfaces define contracts. Dependency injection wires everything together. This means you can add features without touching existing code—just implement the interface and register it.
Repository Pattern abstracts data access. Controllers and services depend on IPortfolioRepository, not PortfolioDbContext directly. This decouples business logic from database details.
Factory Pattern creates strategies. StrategyFactory.Create("Low Risk") returns the appropriate instance. Adding new strategies doesn't require modifying call sites—just register in the factory.
Strategy Pattern (the design pattern, not the investment strategy) appears in optimization. Different algorithms (LowRiskStrategy, HighYieldStrategy) implement the same interface. Code that runs optimization doesn't care which strategy it is—it just calls the interface methods.
Async/Await prevents UI blocking. Long-running operations like data fetching and optimization run asynchronously, returning control to the UI thread. Progress callbacks update the UI during execution.
Observer Pattern (via INotifyPropertyChanged) keeps UI and data in sync. ViewModels raise events when properties change, and WPF data binding automatically updates the view.
The app compiles to a single executable with dependencies packaged. For WPF, you run dotnet publish -c Release -r win-x64 --self-contained to create a standalone Windows application. Users don't need .NET installed—everything bundles together.
The database file starts empty. On first run, EF Core creates outputs/database/quantfolio.db automatically. Migrations apply to create tables. Seeding scripts can pre-populate Products or sample Clients/Managers.
Configuration comes from appsettings.json in the executable directory. Users can edit it to change database paths or API keys without recompiling.
For enterprise deployment, you'd replace SQLite with a server database (PostgreSQL, SQL Server), add authentication, and deploy as a web app instead of desktop. The core logic stays the same—only the data and presentation layers change.
Check the database first. If an optimization produces weird results, inspect the MarketData table. Missing dates, zero prices, or incorrect symbols cause problems. Use DB Browser for SQLite to explore the database visually.
Enable logging. EF Core can log SQL queries. Set DbContextOptionsBuilder.LogTo(Console.WriteLine) to see every query executed. This helps diagnose slow queries or unexpected behavior.
Isolate the strategy. If optimization fails, test the strategy alone with mock data. Create a small dataset (5 symbols, 30 days), train, optimize, and inspect weights. This narrows down whether the issue is data, strategy logic, or infrastructure.
Check for nulls. Even with nullable reference types enabled, runtime nulls can sneak in from database queries or API responses. Defensive checks (if (data == null) return;) prevent NullReferenceExceptions.
Review test failures. When tests fail after changes, the failure message often pinpoints exactly what broke. Don't ignore failing tests—fix them or update expectations if behavior intentionally changed.
The architecture supports several enhancements. Real-time data streaming could replace batch fetching. Machine learning models could augment or replace current heuristics—LSTM networks for price prediction, clustering for asset grouping. Multi-objective optimization could balance return, risk, and turnover simultaneously using Pareto fronts.
Transaction costs aren't modeled. Adding fees per trade and slippage would make backtests more realistic. Tax optimization could minimize capital gains. Risk constraints like sector limits or ESG filters would expand strategy options.
Web deployment would enable multi-user access. Replacing WPF with Blazor or React gives browser access. Adding authentication and authorization supports multiple organizations.
Performance improvements include parallel data fetching, vectorized calculations for returns and volatilities, and precomputed correlation matrices. These would speed up optimization, especially for large universes.
The codebase is ready for these extensions. The layered architecture and interfaces mean you can add features incrementally without major refactoring.
QuantFolio demonstrates modern C# development practices: clean architecture, dependency injection, async programming, Entity Framework, WPF, and comprehensive testing. The separation between data, business logic, and presentation makes the codebase maintainable and extensible.
Understanding the architecture helps you navigate the code. Models define data structures. Interfaces define contracts. Services implement business logic. Strategies contain algorithms. Repositories handle persistence. ViewModels bridge UI and logic. Everything connects through dependency injection.
Whether you're fixing a bug, adding a feature, or just exploring, this guide should give you the mental model needed to work effectively with the codebase. The code itself is well-commented, so dive in, experiment, and extend it to meet your needs.