How the Code Executes the Trade
Once a buy signal fires, exactly which lines open the position, deduct cash, calculate shares, and eventually close the trade — traced through simulateStrategy and closeTrade.
How the Code Executes the Trade
The previous lesson showed which line fires the buy signal. This lesson shows what happens next — how cash is allocated, how many shares are bought, how the position is tracked, and how the exit is calculated. All of this lives in simulateStrategy() and closeTrade() in engine.ts.
Step 1 — Capital Allocation: capPerSymbol
// engine.ts — lines 778–784
const symbols = config.asset_universe; // e.g. ['SPY','AAPL','TSLA']
const capPerSymbol = config.initial_capital / symbols.length;
// capPerSymbol = $100,000 / 3 = $33,333 per symbol
let cash = config.initial_capital; // starts at full bankroll
const positions = new Map<string, Position>(); // symbol → open position
const trades = []; // closed trade logcapPerSymbol is calculated once before the loop starts. It is the maximum the engine will ever invest in a single stock. This is the position-sizing rule — equal-weight across the universe. If SPY is already owned, its $33k slot is locked; cash falls; the other two symbols can still take their slots.
Why Equal Weight?
Equal-weight is the simplest position sizing approach and hard to beat in practice for diversified portfolios. More sophisticated systems use volatility-adjusted sizing (e.g. Kelly Criterion or ATR-based) but require more code and more parameters to tune. The engine uses equal-weight to keep the backtest clean and reproducible.
Step 2 — Opening the Position: The Buy Block
// engine.ts — lines 807–816
if (signal === 'buy' && !hasPos && cash >= bar.close) {
// How much money to deploy into this stock
const allocate = Math.min(capPerSymbol, cash);
// allocate = min($33,333, remaining cash)
// The min() prevents deploying more than cash available
// Fees reduce the invested amount — NOT added on top
const feeRate = config.commission + config.slippage;
// feeRate = 0.001 + 0.001 = 0.002 (0.2% total round-trip cost)
const investAmount = allocate / (1 + feeRate);
// investAmount = $33,333 / 1.002 = $33,266 (fees come off the top)
// ← THIS LINE calculates how many shares are bought
const shares = investAmount / bar.close;
// shares = $33,266 / $450 (AAPL price) = 73.92 shares
if (shares > 0) {
// Record the open position in memory
positions.set(sym, {
symbol: sym,
shares, // ← fractional shares allowed in backtest
entry_price: bar.close,
entry_date: date,
});
cash -= allocate; // ← deduct full allocated amount from cash
}
}| Variable | Example Value | What It Represents |
|---|---|---|
| allocate | $33,333 | Capital slot for this symbol |
| feeRate | 0.002 (0.2%) | Commission + slippage combined |
| investAmount | $33,266 | Amount reaching the market after fees |
| shares | 73.92 | Number of shares purchased |
| cash after | $66,667 | Remaining cash for other symbols |
Why Divide by (1 + feeRate)?
If you invested $33,333 and then subtracted fees separately, you would sometimes over-spend — ending up with negative cash when all symbols trigger at once. Dividing by (1 + feeRate) first means fees are already baked in, so cash never goes negative. The math: investAmount × (1 + feeRate) = allocate exactly.
Step 3 — Closing the Position: The Sell Block
// engine.ts — lines 817–821
} else if (signal === 'sell' && hasPos) {
const pos = positions.get(sym)!; // retrieve the open position record
// Compute P&L and log the closed trade
trades.push(closeTrade(pos, bar.close, date, config));
// Return cash: shares × current price, minus exit fees
cash += pos.shares * bar.close * (1 - config.commission - config.slippage);
// = 73.92 shares × $490 × (1 - 0.002) = $36,145 back into cash
positions.delete(sym); // ← position is gone; symbol can be re-entered later
}Step 4 — closeTrade: Computing Realized P&L
// engine.ts — lines 850–878
function closeTrade(pos, exitPrice, exitDate, config): BacktestTrade {
const value = pos.shares * exitPrice;
// value = 73.92 × $490 = $36,221 (gross exit proceeds)
const fees = value * (config.commission + config.slippage);
// fees = $36,221 × 0.002 = $72.44
const netValue = value - fees;
// netValue = $36,221 - $72 = $36,149 (what you actually receive)
const costBasis = pos.shares * pos.entry_price;
// costBasis = 73.92 × $450 = $33,266 (what you paid, already net of entry fees)
const pnl = netValue - costBasis;
// pnl = $36,149 - $33,266 = $2,883 profit
const durationDays = Math.round(
(new Date(exitDate) - new Date(pos.entry_date)) / 86_400_000
);
return {
symbol: pos.symbol,
direction: 'long',
entry_date: pos.entry_date,
entry_price: pos.entry_price,
exit_date: exitDate,
exit_price: exitPrice,
quantity: pos.shares,
pnl, // ← realized profit/loss in dollars
pnl_percent: (pnl / costBasis) * 100,
duration_days: durationDays,
};
}The Full Lifecycle in Four Variables
allocate → what capital enters. investAmount → what actually buys shares (after entry fee). pos.shares × exitPrice → gross exit value. pnl = netValue − costBasis → the final realized profit or loss. Every number shown in the backtest trade log flows from these four calculations.
Step 5 — Options Execution: How It Differs
For options strategies (Bull Call Spread, Bear Put Spread, Iron Condor, Cash-Secured Put), execution is handled by separate simulator functions — simulateBullCallSpread, simulateIronCondor, etc. The key differences from equity execution are:
- Capital deployed = net debit (for spreads) or collateral (for CSP), not share price × quantity
- Positions are held for a fixed duration (roll at 21 DTE) rather than until a signal fires
- P&L is marked-to-market daily using the binomial tree pricer — not just entry vs. exit price
- Profit targets and stop losses are checked every day against the current spread value
- At expiry, the CRR tree returns a terminal value of max(S−K, 0) for calls and max(K−S, 0) for puts
// engine.ts — options execution excerpt (simulateBullCallSpread)
// On the first trading day of each month, open a new spread:
const { spreadValue: entryValue } = priceBullSpread(spot, longStrike, shortStrike, dte);
const allocate = capital * positionSize; // e.g. 10% of capital
const contracts = Math.floor(allocate / (entryValue * 100));
capital -= contracts * entryValue * 100; // debit paid
// Every day after entry, mark-to-market:
const { spreadValue: currentValue } = priceBullSpread(spot, longStrike, shortStrike, dte);
const unrealizedPnl = (currentValue - entryValue) * contracts * 100;
// Close early if profit target or stop loss hit:
if (currentValue >= entryValue * (1 + profitTarget)) closeSpread('profit_target');
if (currentValue <= entryValue * (1 - stopLossPct)) closeSpread('stop_loss');
if (dte <= closeDte) closeSpread('time_exit');Why Options Use a Separate Simulator
Equity simulateStrategy() uses a simple close-price comparison. Options require daily Black-Scholes / CRR pricing, DTE tracking, strike selection, and contract sizing — too much logic to fold into the generic getSignal switch. Each options strategy gets its own function so the math stays clean and each strategy's edge is accurately modelled.
- capPerSymbol splits capital equally so no single stock can use the whole bankroll
- Fees are deducted from the allocated amount before shares are calculated — cash never overflows
- shares = investAmount / bar.close is the one line that determines position size
- closeTrade computes realized P&L as netValue − costBasis