How the Code Actually Picks the Stock
A line-by-line tour of the exact functions in engine.ts that decide which stock gets a buy signal on any given day — and which gets ignored.
How the Code Actually Picks the Stock
Every time a backtest runs, a loop visits every symbol in the asset universe on every trading day. The pick — buy, sell, or do nothing — comes from one function: getSignal(). Here is exactly how it works, line by line, in engine.ts.
Step 1 — resolveStrategyType: Turning a Name into a Type
Before any signal can fire, the engine must know which strategy it is running. It does this by matching keywords in the strategy name string.
// engine.ts — lines 64–75
function resolveStrategyType(nameOrId: string): StrategyType {
const s = nameOrId.toLowerCase();
if (s.includes('iron') && s.includes('condor')) return 'iron-condor';
if (s.includes('cash') && (s.includes('put') || s.includes('secured'))) return 'cash-secured-put';
if (s.includes('bear') && (s.includes('put') || s.includes('spread'))) return 'bear-put-spread';
if (s.includes('bull') && (s.includes('call') || s.includes('spread'))) return 'bull-call-spread';
if (s.includes('sma') || s.includes('crossover') || s.includes('moving'))return 'sma-crossover';
if (s.includes('rsi') || s.includes('reversion') || s.includes('mean')) return 'rsi-mean-reversion';
return 'buy-and-hold'; // ← default if nothing matches
}Why Keyword Matching?
Strategies are stored in the database by name, not by a strict enum. Keyword matching lets users name their strategies anything ("My RSI v2", "Mean Reversion Clone") and still have the engine detect the correct logic. The strategy_id field in the config is the fallback when the name is ambiguous.
Step 2 — The closes Array: What the Strategy Actually Sees
Before getSignal is called, the engine builds a running history of closing prices for each symbol. This is the only price data the strategy gets — no opens, no highs, no lows.
// engine.ts — lines 785–796 (inside the main day loop)
const closeHistory = new Map<string, number[]>();
for (const date of tradingDays) {
const bars = pricesByDate.get(date)!;
// Append today's close to each symbol's rolling history
for (const sym of symbols) {
const bar = bars.get(sym);
if (!bar) continue; // ← symbol had no data this day: skip it
if (!closeHistory.has(sym)) closeHistory.set(sym, []);
closeHistory.get(sym)!.push(bar.close); // ← THIS is the array getSignal receives
}
// ... signal evaluation follows immediately below
}By the time getSignal runs on day 50, closes contains 50 values — one per trading day since the backtest started. On day 1, it contains only 1 value. This is why strategies have a warm-up guard: if closes.length < period + 1, return "hold" — not enough history yet to compute the indicator.
Step 3 — getSignal: The Exact Line That Says "Buy This Stock"
This is the single function that makes the pick. It runs once per symbol per day. The return value is either "buy", "sell", or "hold".
// engine.ts — lines 77–116
function getSignal(
type: StrategyType,
closes: number[], // ← full price history for this symbol up to today
hasPosition: boolean, // ← is the engine already holding this stock?
params: Record<string, number>,
): Signal {
switch (type) {
// ── RSI Mean Reversion ────────────────────────────────────────
case 'rsi-mean-reversion': {
const period = params.rsi_period ?? 14;
const oversold = params.oversold ?? 30;
const overbought = params.overbought ?? 70;
if (closes.length < period + 1) return 'hold'; // ← warm-up guard
const r = rsi(closes, period); // ← compute RSI on the full history
if (!hasPosition && r < oversold) return 'buy'; // ← PICK: buy this stock
if ( hasPosition && r > overbought) return 'sell'; // ← EXIT: sell this stock
return 'hold';
}
// ── SMA Crossover ─────────────────────────────────────────────
case 'sma-crossover': {
const fast = params.fast_period ?? 10;
const slow = params.slow_period ?? 30;
if (closes.length < slow + 1) return 'hold';
const fastNow = sma(closes, fast);
const slowNow = sma(closes, slow);
const fastPrev = sma(closes.slice(0, -1), fast); // ← yesterday's fast SMA
const slowPrev = sma(closes.slice(0, -1), slow); // ← yesterday's slow SMA
// Cross detected: yesterday fast ≤ slow, today fast > slow → bullish crossover
if (!hasPosition && fastPrev <= slowPrev && fastNow > slowNow) return 'buy';
if ( hasPosition && fastPrev >= slowPrev && fastNow < slowNow) return 'sell';
return 'hold';
}
}
}The Pick Happens in One Line
For RSI: line "if (!hasPosition && r < oversold) return 'buy'" — that single line is the entire stock selection decision. Everything before it (EMA math, RSI formula, closes array) exists solely to produce the number r so this comparison can happen.
Step 4 — The Symbol Loop: Every Stock Evaluated Every Day
// engine.ts — lines 799–822
for (const sym of symbols) { // ← iterates every ticker in asset_universe
const bar = bars.get(sym);
if (!bar) continue; // ← no price data for this symbol today: skip
const closes = closeHistory.get(sym) ?? [];
const hasPos = positions.has(sym);
// ← THIS is the stock selection call
const signal = getSignal(strategyType, closes, hasPos, params);
if (signal === 'buy' && !hasPos && cash >= bar.close) {
// Stock selected — move to execution (see next lesson)
...
} else if (signal === 'sell' && hasPos) {
// Exit signal — also handled in execution (see next lesson)
...
}
}Notice the loop evaluates every symbol independently. SPY can be in a buy signal while AAPL is in hold and TSLA is in sell — all on the same day. The strategy does not compare stocks against each other; it evaluates each one against its own indicator threshold.
The hasPosition Guard
The condition "!hasPosition" inside getSignal (and again in the outer if) means the engine will never double-buy the same stock. Once a position exists, buy signals are silently ignored until a sell exits it. This is why the RSI strategy can only hold one position per symbol at a time.
- resolveStrategyType maps any strategy name string to one of five known types
- getSignal is the single function that decides buy / sell / hold for every symbol every day
- The closes array is the entire price history fed into the indicator — its length is what triggers "not enough data yet"
- Each symbol in the asset universe is evaluated independently on every trading day