Key Features

This document details the production-ready features of the backtest-kit framework that enable reliable algorithmic trading strategy development and deployment. Each feature is explained with its corresponding code implementation and architectural role.

For information about the overall architecture and layer separation, see Architecture. For implementation details of specific components, see Core Business Logic and Service Layer.


The framework supports two distinct execution modes with shared business logic but different orchestration patterns: backtesting for historical analysis and live trading for production deployment.

Backtesting executes strategies against historical data using BacktestLogicPrivateService to orchestrate timeframe iteration:

Mermaid Diagram

Backtest Flow Characteristics:

Feature Implementation
Timeframe Generation ClientFrame.getTimeframe() creates timestamp array with configured interval
Context Injection ExecutionContextService sets when to historical timestamp, backtest=true
Fast-Forward Simulation ClientStrategy.backtest() processes future candles without tick iteration
Memory Efficiency Generator yields only closed signals, no accumulation
Early Termination User can break from async iterator at any time

Live trading runs an infinite loop with 1-minute intervals, monitoring active signals in real-time:

Mermaid Diagram

Live Trading Characteristics:

Feature Implementation
Infinite Generator LiveLogicPrivateService.execute() loops with while (true)
Real-Time Context ExecutionContextService sets when=Date.now(), backtest=false
State Persistence PersistSignalAdapter.writeSignalData() before every state change
Crash Recovery ClientStrategy.waitForInit() loads last known state on restart
Interval Control sleep(60000 + 1ms) ensures 1-minute tick rate
Filtered Output Only opened and closed yielded, active filtered

Live trading uses atomic file writes to persist signal state before every state transition, enabling crash recovery without signal duplication or data loss.

Mermaid Diagram

The PersistSignalAdapter ensures atomicity through temporary file writes:

Step Operation Purpose
1 Write to .tmp file Prevent corruption if crash during write
2 Sync to disk Ensure OS flushes write buffer
3 Rename .tmp to final Atomic filesystem operation (all-or-nothing)
4 Sync directory Ensure directory entry is persisted

Key Code Locations:


Every signal generated by getSignal() is validated before execution to prevent invalid trades. Validation failures throw descriptive errors. The framework provides configurable validation parameters to protect against common trading mistakes.

The VALIDATE_SIGNAL_FN function enforces the following constraints:

Mermaid Diagram

Configurable via setConfig() from src/config/params.ts:1-35:

Parameter Default Purpose
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT 0.1% Ensures TP covers trading fees (prevents micro-profits eaten by costs)
CC_MAX_STOPLOSS_DISTANCE_PERCENT 20% Prevents catastrophic losses from extreme SL values
CC_MAX_SIGNAL_LIFETIME_MINUTES 1440 (1 day) Prevents eternal signals blocking risk limits
CC_SCHEDULE_AWAIT_MINUTES 120 (2 hours) Maximum wait time for scheduled signal activation

Validation Error Examples:

Invalid Signal Error Message
Long with TP below open Long: priceTakeProfit (49000) must be > priceOpen (50000)
Long with SL above open Long: priceStopLoss (51000) must be < priceOpen (50000)
Short with TP above open Short: priceTakeProfit (51000) must be < priceOpen (50000)
Short with SL below open Short: priceStopLoss (49000) must be > priceOpen (50000)
Negative price priceOpen must be positive, got -50000
Zero time minuteEstimatedTime must be positive, got 0
TP too close TakeProfit distance (0.05%) below minimum (0.1%)
SL too far StopLoss distance (25%) exceeds maximum (20%)
Excessive lifetime Signal lifetime (2000min) exceeds maximum (1440min)

Mermaid Diagram


Both backtest and live execution use async generators (AsyncIterableIterator) to stream results without accumulating data in memory, enabling processing of arbitrarily large datasets.

Mermaid Diagram

Aspect Backtest Generator Live Generator
Termination Finite (timeframe exhausted) Infinite (while (true))
Yield Condition Only closed results opened and closed (filters active)
Context Setting Historical timestamp from timeframe Date.now() on each iteration
Sleep Interval None (fast iteration) 60000ms + 1ms between ticks
Fast-Forward Yes (backtest() method) No (real-time only)
Cancellation break from iterator cancel() function returned by background()
  1. Prototype Methods: All client classes use prototype functions instead of arrow functions

    • Location: src/client/ClientStrategy.ts (all methods use public methodName = async () => {} pattern)
    • Benefit: Single function instance shared across all instances
  2. Memoization: Connection services cache client instances

  3. Streaming Accumulation: Markdown services accumulate passively

  4. Lazy Initialization: Services created only when needed

    • Pattern: DI container resolves dependencies on first access

Signals follow a deterministic state machine with discriminated union types for type-safe handling. The framework supports both market orders (immediate execution) and limit orders (scheduled execution).

The discriminated union IStrategyTickResult enables type narrowing:

// Example usage (not actual code, just illustration)
const result = await strategy.tick();

if (result.action === "idle") {
// TypeScript knows: result.signal === null
console.log(result.currentPrice);
}

if (result.action === "scheduled") {
// TypeScript knows: result.signal is ISignalRow with scheduledAt
console.log(result.signal.priceOpen);
console.log(result.signal.scheduledAt);
}

if (result.action === "cancelled") {
// TypeScript knows: result has closeReason, closeTimestamp
console.log(result.closeReason); // "timeout" | "stop_loss"
console.log(result.closeTimestamp);
}

if (result.action === "opened") {
// TypeScript knows: result.signal is ISignalRow with pendingAt
console.log(result.signal.priceOpen);
console.log(result.signal.pendingAt);
}

if (result.action === "active") {
// TypeScript knows: result.signal is ISignalRow
// result.currentPrice is available
}

if (result.action === "closed") {
// TypeScript knows: result has pnl, closeReason, closeTimestamp
console.log(result.pnl.pnlPercentage);
console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
}
State Entry Point Exit Point Notes
idle src/client/ClientStrategy.ts:306-322 getSignal() returns non-null No active signal
scheduled Signal generation with future priceOpen Price activation or timeout Limit order waiting
cancelled Timeout or SL before activation Return to idle Scheduled signal not filled
opened src/client/ClientStrategy.ts:275-291 Next tick iteration Position activated
active src/client/ClientStrategy.ts:447-463 TP/SL/time condition met Position monitoring
closed src/client/ClientStrategy.ts:416-435 setPendingSignal(null) Final state with PNL

Profit and loss calculations include realistic trading costs (fees and slippage) for accurate backtesting:

Cost Type Value Application
Fee 0.1% (0.001) Applied to both entry and exit
Slippage 0.1% (0.001) Simulates market impact
Total Cost 0.2% per side 0.4% round-trip (0.2% entry + 0.2% exit)

Long Position:

priceOpenWithCosts  = priceOpen  × (1 + slippage + fee)
priceCloseWithCosts = priceClose × (1 - slippage - fee)
pnl% = (priceCloseWithCosts - priceOpenWithCosts) / priceOpenWithCosts × 100

Short Position:

priceOpenWithCosts  = priceOpen  × (1 - slippage + fee)
priceCloseWithCosts = priceClose × (1 + slippage + fee)
pnl% = (priceOpenWithCosts - priceCloseWithCosts) / priceOpenWithCosts × 100

Mermaid Diagram

Example Calculation (Long Position):

Parameter Value
Entry Price $50,000
Exit Price $51,000
Adjusted Entry $50,000 × 1.002 = $50,100
Adjusted Exit $51,000 × 0.998 = $50,898
PNL % ($50,898 - $50,100) / $50,100 × 100 = +1.59%

Without costs, this would be +2.0%. The 0.41% difference represents realistic trading costs.


Signal generation is throttled at the strategy level to prevent spam and ensure consistent signal spacing:

Mermaid Diagram

Interval Minutes Use Case
"1m" 1 High-frequency strategies
"3m" 3 Short-term signals
"5m" 5 Medium-frequency trading
"15m" 15 Moderate signals
"30m" 30 Low-frequency strategies
"1h" 60 Hourly signals

The throttling check occurs at src/client/ClientStrategy.ts:94-106:

// Pseudocode representation (not actual code)
const intervalMinutes = INTERVAL_MINUTES[interval]; // e.g., "5m" → 5
const intervalMs = intervalMinutes × 60 × 1000;

if (lastSignalTimestamp !== null &&
currentTime - lastSignalTimestamp < intervalMs) {
return null; // Too soon, throttle
}

lastSignalTimestamp = currentTime; // Update for next check

All price monitoring uses Volume-Weighted Average Price (VWAP) calculated from the last CC_AVG_PRICE_CANDLES_COUNT (default: 5) one-minute candles, providing more accurate price discovery than simple close prices:

Mermaid Diagram

Context Method Purpose
Live Tick ClientStrategy.tick() Check TP/SL against current VWAP
Live Idle ClientStrategy.tick() (no signal) Report current market price
Backtest ClientStrategy.backtest() Check TP/SL on each candle's VWAP
Public API getAveragePrice(symbol) Expose VWAP to user strategies

Mermaid Diagram

Edge Case: If total volume is zero, fallback to simple average of close prices:

// Pseudocode (not actual code)
if (totalVolume === 0) {
return candles.reduce((sum, c) => sum + c.close, 0) / candles.length;
}

The framework generates detailed markdown reports with statistics for backtest, live trading, and scheduled signals:

Mermaid Diagram

Backtest Report Metrics (BacktestMarkdownService):

  • Total closed signals
  • Win rate, average PNL, total PNL
  • Standard deviation, Sharpe ratio, annualized Sharpe ratio
  • Certainty ratio (avgWin / |avgLoss|)
  • Expected yearly returns
  • Signal-by-signal table with prices, PNL, close reason, duration, timestamps

Live Report Metrics (LiveMarkdownService):

  • Total events (idle, opened, active, closed)
  • Closed signals count, win count, loss count
  • Win rate (percentage and W/L ratio)
  • Average PNL, total PNL
  • Standard deviation, Sharpe ratio, annualized Sharpe ratio
  • Certainty ratio, expected yearly returns
  • Event table with all state transitions

Schedule Report Metrics (ScheduleMarkdownService):

  • Total scheduled signals
  • Total cancelled signals
  • Cancellation rate (cancelled / scheduled × 100)
  • Average wait time for cancelled signals
  • Event table with scheduled and cancelled events
Method Backtest Live Schedule Purpose
getData(strategy) Get statistics object
getReport(strategy) Generate markdown string
dump(strategy, path?) Save to disk (default: ./logs/{mode}/{strategy}.md)
clear(strategy?) Clear accumulated data

Mermaid Diagram


The framework uses a registry pattern with dependency injection to support custom implementations of exchanges, strategies, and timeframes:

Mermaid Diagram

Schema Required Methods Purpose
IStrategySchema getSignal(symbol) Define signal generation logic
IExchangeSchema getCandles(symbol, interval, since, limit) Provide market data
IFrameSchema startDate, endDate, interval Define backtest period

Connection services use memoization to ensure single instance per schema name:

Mermaid Diagram

Users can implement custom exchanges by providing a schema:

// Example pattern (not actual code)
addExchange({
exchangeName: "custom-db",
getCandles: async (symbol, interval, since, limit) => {
// Fetch from PostgreSQL, MongoDB, etc.
const rows = await db.query(`SELECT * FROM candles WHERE ...`);
return rows.map(row => ({
timestamp: row.time,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume
}));
},
formatPrice: async (symbol, price) => price.toFixed(8),
formatQuantity: async (symbol, qty) => qty.toFixed(8)
});

Feature Key Components Primary Benefit
Multi-Mode Execution BacktestLogicPrivateService, LiveLogicPrivateService Single codebase for research and production
Crash-Safe Persistence PersistSignalAdapter, FilePersist Zero data loss in production crashes
Signal Validation VALIDATE_SIGNAL_FN, GET_SIGNAL_FN Prevents invalid trades at source
Async Generators execute() generator methods Constant memory usage, early termination
Signal Lifecycle IStrategyTickResult discriminated union Type-safe state handling
Accurate PNL toProfitLossDto() Realistic performance metrics
Interval Throttling _lastSignalTimestamp check Controlled signal frequency
VWAP Pricing GET_AVG_PRICE_FN, getAveragePrice() Better price discovery
Markdown Reports BacktestMarkdownService, LiveMarkdownService Performance analysis and auditing
Plugin Architecture Schema services, connection services Easy integration with custom data sources