Paper Trading Engine Architecture and Simulation Logic in FinceptTerminal
The FinceptTerminal paper trading engine is a self‑contained C++ simulation layer that mimics live broker behavior through atomic SQLite transactions, thread‑safe fill processing, and realistic margin and fee calculations without network dependencies.
The FinceptTerminal project provides a deterministic backtesting environment through its paper trading subsystem, which replicates real market mechanics for testing strategies and UI demos. This article examines the engine’s three‑tier architecture—spanning the public API, core simulation logic, and SQLite persistence—to explain how it handles order validation, position management, and P&L accounting.
Core Architecture: API, Engine, and Persistence Layers
The engine follows a strict separation of concerns implemented across three distinct layers in the FinceptTerminal source tree.
The API layer exposes C++ functions such as pt_create_portfolio, pt_place_order, and pt_fill_order via fincept-qt/src/trading/PaperTrading.h. These functions provide the public interface used by UI components and algorithmic testers.
The Engine layer in fincept-qt/src/trading/PaperTrading.cpp implements validation routines, margin calculations, order matching, and fill processing. This layer contains the simulation logic that determines how orders interact with existing positions and portfolio balances.
The Persistence layer residing in fincept-qt/src/storage/repositories/PaperTradingRepository.cpp manages all SQLite interactions. It stores portfolios, orders, positions, and trades, ensuring that every engine action runs inside a database transaction for atomicity.
Simulation Logic and Order Processing Flow
The simulation follows a deterministic pipeline from portfolio creation through order execution to fill processing.
Portfolio Creation and Validation
The pt_create_portfolio function validates that portfolio names are non‑empty and that initial balance, leverage, and fee rates are positive values. Upon validation, it inserts a new row into the pt_portfolios table via the repository layer.
Order Placement and Margin Checks
When pt_place_order is invoked, the engine first validates side, type, quantity, and price parameters. It then performs margin checks only on the net new exposure—specifically ignoring quantity that would close an existing opposite position. The required margin calculates as:
required = net_new_qty * ref_price / portfolio.leverage
Here, ref_price represents the order price or stop price. Valid orders are inserted into pt_orders with a pending status, awaiting simulation fills.
The Fill Process: Closing, Averaging, and New Positions
The pt_fill_order function represents the heart of the simulation logic. It acquires a global s_fill_mutex to serialize fills per portfolio, preventing race conditions during balance and position updates. The function implements three distinct position handling strategies:
- Closing Logic: When an opposite‑side position exists, the engine calculates realized P&L, updates or removes the existing position, and opens a new opposite‑side position if residual quantity remains after closure.
- Averaging Logic: For same‑side positions, the entry price updates to a weighted average of the existing and newly filled quantities.
- New Position Logic: If no position exists for the symbol and side, the engine inserts a fresh row into
pt_positions.
Fees calculate as qty * fill_price * portfolio.fee_rate but are deducted only on closing fills, matching standard exchange behavior where opening trades incur no costs. The portfolio balance updates reflect balance_change = pnl - fee exclusively during position reduction or reversal.
After committing the SQLite transaction, the engine emits paper_trading.order_filled via the EventBus to notify UI components.
Fee Calculation and P&L Accounting
The simulation uses a realistic fee model defined in the portfolio’s fee_rate. During the fill process, the engine distinguishes between opening and closing volume. Closing fills trigger fee deduction and realized P&L calculation, while opening fills update position size without balance reduction. This distinction ensures that strategy backtests accurately reflect the cost structure of live trading where fees typically apply to liquidity‑taking trades that reduce exposure.
Thread Safety and Transactional Integrity
All engine operations run inside explicit SQLite transactions managed through fincept-qt/src/storage/sqlite/Database.cpp. The pattern uses db.begin_transaction() followed by commit() or rollback(), ensuring that a fill is either completely applied—including balance updates, position changes, and trade records—or not applied at all.
The global QMutex s_fill_mutex provides thread‑safe access to the fill process. By serializing fills per portfolio, the engine prevents concurrent modifications to balances during high‑frequency simulation scenarios or rapid UI interactions.
Persistence Layer and Repository Pattern
The PaperTradingRepository class provides thin wrappers around SQLite statements. It uses row mappers to convert QSqlQuery results into domain structs (PtPortfolio, PtOrder, PtPosition, PtTrade). CRUD helpers such as insert_portfolio, update_balance, and insert_trade return Result<T> types indicating success or specific error strings.
The repository enforces referential integrity through foreign‑key constraints with cascade deletes, while explicit cleanup in delete_portfolio guarantees deterministic removal of all related orders, positions, and trades. Statistics aggregation via get_stats calculates total P&L, win/loss counts, and largest drawdowns through optimized SQL queries.
Integration with the Trading Interface
The UI interacts with the paper trading engine through the UnifiedTrading façade implemented in fincept-qt/src/trading/UnifiedTrading.cpp. This routing layer checks the trading mode and directs requests accordingly:
if (mode == "paper") {
auto paper_order = pt_place_order(...);
pt_fill_order(paper_order.id, fill_price);
}
This abstraction allows identical frontend code to operate against either live brokers or the simulation engine without modification. The EventBus in fincept-qt/src/events/EventBus.h provides the pub/sub mechanism that broadcasts fill events to portfolio views, order books, and statistics panels.
Practical Code Examples
Creating a Paper Portfolio
auto portfolio = pt_create_portfolio(
"My Demo Portfolio", // name
1'000'000.0, // initial balance
"USD", // currency
1.0, // leverage
"cross", // margin mode
0.001, // fee rate (0.1%)
"paper_exchange" // exchange identifier
);
Placing and Filling Orders
// Place a limit order
auto order = pt_place_order(
portfolio.id, // portfolio_id
"AAPL", // symbol
"buy", // side
"limit", // order_type
100, // quantity
150.0, // price
std::nullopt, // stop_price
false // reduce_only
);
// Simulate a partial fill
auto trade1 = pt_fill_order(order.id, 149.5, 60); // 60 shares filled
// Fill remaining quantity
auto trade2 = pt_fill_order(order.id, 149.5, std::nullopt); // fills the rest
Retrieving Statistics
PtStats stats = pt_get_stats(portfolio.id);
qDebug() << "Total P&L:" << stats.total_pnl
<< "Win rate:" << stats.win_rate;
Resetting a Portfolio
pt_reset_portfolio(portfolio.id);
Summary
- The paper trading engine uses a three‑layer architecture separating API declarations in
PaperTrading.h, simulation logic inPaperTrading.cpp, and SQLite persistence inPaperTradingRepository.cpp. - Margin checks validate only net new exposure against available buying power, calculated as
net_new_qty * ref / leverage. - The fill process handles position closing, averaging, and creation atomically inside a global mutex and SQLite transaction to prevent data corruption.
- Fees are charged only on closing fills, calculated as
qty * fill_price * fee_rate, ensuring realistic P&L tracking that matches live exchange behavior. - Thread safety is enforced via
s_fill_mutex, while event broadcasting throughEventBuskeeps UI components synchronized with state changes.
Frequently Asked Questions
How does the paper trading engine handle concurrent order fills?
The engine serializes all fill operations per portfolio using a global QMutex named s_fill_mutex. This prevents race conditions when updating portfolio balances and positions during simultaneous fill requests, ensuring accurate balance accounting even under rapid simulation scenarios.
Are fees charged on both opening and closing trades?
No. According to the simulation logic in PaperTrading.cpp, fees are calculated and deducted only on closing fills. The fee formula is qty * fill_price * portfolio.fee_rate, and the balance update reflects pnl - fee exclusively during position reduction or reversal, matching the behavior of many live exchanges.
What database does the paper trading engine use for persistence?
The engine uses SQLite through the PaperTradingRepository class in fincept-qt/src/storage/repositories/PaperTradingRepository.cpp. All operations run inside explicit transactions (begin_transaction/commit) to guarantee atomicity, ensuring that fills, balance updates, and trade records are persisted together or rolled back on error.
Can the paper trading engine simulate partial order fills?
Yes. The pt_fill_order function accepts an optional fill quantity parameter. If omitted, it fills the remaining order quantity; otherwise, it processes only the specified amount. This allows realistic simulation of partial executions common with limit orders in live markets.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →