backtest-kit
Version:
A TypeScript library for trading system backtest
1,385 lines (1,371 loc) • 312 kB
TypeScript
import * as di_scoped from 'di-scoped';
import * as functools_kit from 'functools-kit';
import { Subject } from 'functools-kit';
declare const GLOBAL_CONFIG: {
/**
* Time to wait for scheduled signal to activate (in minutes)
* If signal does not activate within this time, it will be cancelled.
*/
CC_SCHEDULE_AWAIT_MINUTES: number;
/**
* Number of candles to use for average price calculation (VWAP)
* Default: 5 candles (last 5 minutes when using 1m interval)
*/
CC_AVG_PRICE_CANDLES_COUNT: number;
/**
* Minimum TakeProfit distance from priceOpen (percentage)
* Must be greater than trading fees to ensure profitable trades
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
*/
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: number;
/**
* Maximum StopLoss distance from priceOpen (percentage)
* Prevents catastrophic losses from extreme StopLoss values
* Default: 20% (one signal cannot lose more than 20% of position)
*/
CC_MAX_STOPLOSS_DISTANCE_PERCENT: number;
/**
* Maximum signal lifetime in minutes
* Prevents eternal signals that block risk limits for weeks/months
* Default: 1440 minutes (1 day)
*/
CC_MAX_SIGNAL_LIFETIME_MINUTES: number;
/**
* Number of retries for getCandles function
* Default: 3 retries
*/
CC_GET_CANDLES_RETRY_COUNT: number;
/**
* Delay between retries for getCandles function (in milliseconds)
* Default: 5000 ms (5 seconds)
*/
CC_GET_CANDLES_RETRY_DELAY_MS: number;
/**
* Maximum allowed deviation factor for price anomaly detection.
* Price should not be more than this factor lower than reference price.
*
* Reasoning:
* - Incomplete candles from Binance API typically have prices near 0 (e.g., $0.01-1)
* - Normal BTC price ranges: $20,000-100,000
* - Factor 1000 catches prices below $20-100 when median is $20,000-100,000
* - Factor 100 would be too permissive (allows $200 when median is $20,000)
* - Factor 10000 might be too strict for low-cap altcoins
*
* Example: BTC at $50,000 median → threshold $50 (catches $0.01-1 anomalies)
*/
CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR: number;
/**
* Minimum number of candles required for reliable median calculation.
* Below this threshold, use simple average instead of median.
*
* Reasoning:
* - Each candle provides 4 price points (OHLC)
* - 5 candles = 20 price points, sufficient for robust median calculation
* - Below 5 candles, single anomaly can heavily skew median
* - Statistical rule of thumb: minimum 7-10 data points for median stability
* - Average is more stable than median for small datasets (n < 20)
*
* Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
*/
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: number;
};
/**
* Type for global configuration object.
*/
type GlobalConfig = typeof GLOBAL_CONFIG;
/**
* Interface representing a logging mechanism for the swarm system.
* Provides methods to record messages at different severity levels, used across components like agents, sessions, states, storage, swarms, history, embeddings, completions, and policies.
* Logs are utilized to track lifecycle events (e.g., initialization, disposal), operational details (e.g., tool calls, message emissions), validation outcomes (e.g., policy checks), and errors (e.g., persistence failures), aiding in debugging, monitoring, and auditing.
*/
interface ILogger {
/**
* Logs a general-purpose message.
* Used throughout the swarm system to record significant events or state changes, such as agent execution, session connections, or storage updates.
*/
log(topic: string, ...args: any[]): void;
/**
* Logs a debug-level message.
* Employed for detailed diagnostic information, such as intermediate states during agent tool calls, swarm navigation changes, or embedding creation processes, typically enabled in development or troubleshooting scenarios.
*/
debug(topic: string, ...args: any[]): void;
/**
* Logs an info-level message.
* Used to record informational updates, such as successful completions, policy validations, or history commits, providing a high-level overview of system activity without excessive detail.
*/
info(topic: string, ...args: any[]): void;
/**
* Logs a warning-level message.
* Used to record potentially problematic situations that don't prevent execution but may require attention, such as missing data, unexpected conditions, or deprecated usage.
*/
warn(topic: string, ...args: any[]): void;
}
/**
* Sets custom logger implementation for the framework.
*
* All log messages from internal services will be forwarded to the provided logger
* with automatic context injection (strategyName, exchangeName, symbol, etc.).
*
* @param logger - Custom logger implementing ILogger interface
*
* @example
* ```typescript
* setLogger({
* log: (topic, ...args) => console.log(topic, args),
* debug: (topic, ...args) => console.debug(topic, args),
* info: (topic, ...args) => console.info(topic, args),
* });
* ```
*/
declare function setLogger(logger: ILogger): Promise<void>;
/**
* Sets global configuration parameters for the framework.
* @param config - Partial configuration object to override default settings
*
* @example
* ```typescript
* setConfig({
* CC_SCHEDULE_AWAIT_MINUTES: 90,
* });
* ```
*/
declare function setConfig(config: Partial<GlobalConfig>): Promise<void>;
/**
* Execution context containing runtime parameters for strategy/exchange operations.
*
* Propagated through ExecutionContextService to provide implicit context
* for getCandles(), tick(), backtest() and other operations.
*/
interface IExecutionContext {
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Current timestamp for operation */
when: Date;
/** Whether running in backtest mode (true) or live mode (false) */
backtest: boolean;
}
/**
* Scoped service for execution context propagation.
*
* Uses di-scoped for implicit context passing without explicit parameters.
* Context includes symbol, when (timestamp), and backtest flag.
*
* Used by GlobalServices to inject context into operations.
*
* @example
* ```typescript
* ExecutionContextService.runInContext(
* async () => {
* // Inside this callback, context is automatically available
* return await someOperation();
* },
* { symbol: "BTCUSDT", when: new Date(), backtest: true }
* );
* ```
*/
declare const ExecutionContextService: (new () => {
readonly context: IExecutionContext;
}) & Omit<{
new (context: IExecutionContext): {
readonly context: IExecutionContext;
};
}, "prototype"> & di_scoped.IScopedClassRun<[context: IExecutionContext]>;
/**
* Type helper for ExecutionContextService instance.
* Used for dependency injection type annotations.
*/
type TExecutionContextService = InstanceType<typeof ExecutionContextService>;
/**
* Candle time interval for fetching historical data.
*/
type CandleInterval = "1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h";
/**
* Single OHLCV candle data point.
* Used for VWAP calculation and backtesting.
*/
interface ICandleData {
/** Unix timestamp in milliseconds when candle opened */
timestamp: number;
/** Opening price at candle start */
open: number;
/** Highest price during candle period */
high: number;
/** Lowest price during candle period */
low: number;
/** Closing price at candle end */
close: number;
/** Trading volume during candle period */
volume: number;
}
/**
* Exchange parameters passed to ClientExchange constructor.
* Combines schema with runtime dependencies.
*/
interface IExchangeParams extends IExchangeSchema {
/** Logger service for debug output */
logger: ILogger;
/** Execution context service (symbol, when, backtest flag) */
execution: TExecutionContextService;
}
/**
* Optional callbacks for exchange data events.
*/
interface IExchangeCallbacks {
/** Called when candle data is fetched */
onCandleData: (symbol: string, interval: CandleInterval, since: Date, limit: number, data: ICandleData[]) => void;
}
/**
* Exchange schema registered via addExchange().
* Defines candle data source and formatting logic.
*/
interface IExchangeSchema {
/** Unique exchange identifier for registration */
exchangeName: ExchangeName;
/** Optional developer note for documentation */
note?: string;
/**
* Fetch candles from data source (API or database).
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param interval - Candle time interval (e.g., "1m", "1h")
* @param since - Start date for candle fetching
* @param limit - Maximum number of candles to fetch
* @returns Promise resolving to array of OHLCV candle data
*/
getCandles: (symbol: string, interval: CandleInterval, since: Date, limit: number) => Promise<ICandleData[]>;
/**
* Format quantity according to exchange precision rules.
*
* @param symbol - Trading pair symbol
* @param quantity - Raw quantity value
* @returns Promise resolving to formatted quantity string
*/
formatQuantity: (symbol: string, quantity: number) => Promise<string>;
/**
* Format price according to exchange precision rules.
*
* @param symbol - Trading pair symbol
* @param price - Raw price value
* @returns Promise resolving to formatted price string
*/
formatPrice: (symbol: string, price: number) => Promise<string>;
/** Optional lifecycle event callbacks (onCandleData) */
callbacks?: Partial<IExchangeCallbacks>;
}
/**
* Exchange interface implemented by ClientExchange.
* Provides candle data access and VWAP calculation.
*/
interface IExchange {
/**
* Fetch historical candles backwards from execution context time.
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param interval - Candle time interval (e.g., "1m", "1h")
* @param limit - Maximum number of candles to fetch
* @returns Promise resolving to array of candle data
*/
getCandles: (symbol: string, interval: CandleInterval, limit: number) => Promise<ICandleData[]>;
/**
* Fetch future candles forward from execution context time (for backtest).
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param interval - Candle time interval (e.g., "1m", "1h")
* @param limit - Maximum number of candles to fetch
* @returns Promise resolving to array of candle data
*/
getNextCandles: (symbol: string, interval: CandleInterval, limit: number) => Promise<ICandleData[]>;
/**
* Format quantity for exchange precision.
*
* @param symbol - Trading pair symbol
* @param quantity - Raw quantity value
* @returns Promise resolving to formatted quantity string
*/
formatQuantity: (symbol: string, quantity: number) => Promise<string>;
/**
* Format price for exchange precision.
*
* @param symbol - Trading pair symbol
* @param price - Raw price value
* @returns Promise resolving to formatted price string
*/
formatPrice: (symbol: string, price: number) => Promise<string>;
/**
* Calculate VWAP from last 5 1-minute candles.
*
* Formula: VWAP = Σ(Typical Price × Volume) / Σ(Volume)
* where Typical Price = (High + Low + Close) / 3
*
* @param symbol - Trading pair symbol
* @returns Promise resolving to volume-weighted average price
*/
getAveragePrice: (symbol: string) => Promise<number>;
}
/**
* Unique exchange identifier.
*/
type ExchangeName = string;
/**
* Timeframe interval for backtest period generation.
* Determines the granularity of timestamps in the generated timeframe array.
*
* Minutes: 1m, 3m, 5m, 15m, 30m
* Hours: 1h, 2h, 4h, 6h, 8h, 12h
* Days: 1d, 3d
*/
type FrameInterval = "1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d" | "3d";
/**
* Frame parameters passed to ClientFrame constructor.
* Extends IFrameSchema with logger instance for internal logging.
*/
interface IFrameParams extends IFrameSchema {
/** Logger service for debug output */
logger: ILogger;
}
/**
* Callbacks for frame lifecycle events.
*/
interface IFrameCallbacks {
/**
* Called after timeframe array generation.
* Useful for logging or validating the generated timeframes.
*
* @param timeframe - Array of Date objects representing tick timestamps
* @param startDate - Start of the backtest period
* @param endDate - End of the backtest period
* @param interval - Interval used for generation
*/
onTimeframe: (timeframe: Date[], startDate: Date, endDate: Date, interval: FrameInterval) => void;
}
/**
* Frame schema registered via addFrame().
* Defines backtest period and interval for timestamp generation.
*
* @example
* ```typescript
* addFrame({
* frameName: "1d-backtest",
* interval: "1m",
* startDate: new Date("2024-01-01T00:00:00Z"),
* endDate: new Date("2024-01-02T00:00:00Z"),
* callbacks: {
* onTimeframe: (timeframe, startDate, endDate, interval) => {
* console.log(`Generated ${timeframe.length} timestamps`);
* },
* },
* });
* ```
*/
interface IFrameSchema {
/** Unique identifier for this frame */
frameName: FrameName;
/** Optional developer note for documentation */
note?: string;
/** Interval for timestamp generation */
interval: FrameInterval;
/** Start of backtest period (inclusive) */
startDate: Date;
/** End of backtest period (inclusive) */
endDate: Date;
/** Optional lifecycle callbacks */
callbacks?: Partial<IFrameCallbacks>;
}
/**
* Frame interface for timeframe generation.
* Used internally by backtest orchestration.
*/
interface IFrame {
/**
* Generates array of timestamps for backtest iteration.
* Timestamps are spaced according to the configured interval.
*
* @param symbol - Trading pair symbol (unused, for API consistency)
* @returns Promise resolving to array of Date objects
*/
getTimeframe: (symbol: string, frameName: FrameName) => Promise<Date[]>;
}
/**
* Unique identifier for a frame schema.
* Used to retrieve frame instances via dependency injection.
*/
type FrameName = string;
/**
* Method context containing schema names for operation routing.
*
* Propagated through MethodContextService to provide implicit context
* for retrieving correct strategy/exchange/frame instances.
*/
interface IMethodContext {
/** Name of exchange schema to use */
exchangeName: ExchangeName;
/** Name of strategy schema to use */
strategyName: StrategyName;
/** Name of frame schema to use (empty string for live mode) */
frameName: FrameName;
}
/**
* Scoped service for method context propagation.
*
* Uses di-scoped for implicit context passing without explicit parameters.
* Context includes strategyName, exchangeName, and frameName.
*
* Used by PublicServices to inject schema names into ConnectionServices.
*
* @example
* ```typescript
* MethodContextService.runAsyncIterator(
* backtestGenerator,
* {
* strategyName: "my-strategy",
* exchangeName: "my-exchange",
* frameName: "1d-backtest"
* }
* );
* ```
*/
declare const MethodContextService: (new () => {
readonly context: IMethodContext;
}) & Omit<{
new (context: IMethodContext): {
readonly context: IMethodContext;
};
}, "prototype"> & di_scoped.IScopedClassRun<[context: IMethodContext]>;
/**
* Risk check arguments for evaluating whether to allow opening a new position.
* Called BEFORE signal creation to validate if conditions allow new signals.
* Contains only passthrough arguments from ClientStrategy context.
*/
interface IRiskCheckArgs {
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Strategy name requesting to open a position */
strategyName: StrategyName;
/** Exchange name */
exchangeName: ExchangeName;
/** Current VWAP price */
currentPrice: number;
/** Current timestamp */
timestamp: number;
}
/**
* Active position tracked by ClientRisk for cross-strategy analysis.
*/
interface IRiskActivePosition {
/** Signal details for the active position */
signal: ISignalRow;
/** Strategy name owning the position */
strategyName: string;
/** Exchange name */
exchangeName: string;
/** Timestamp when the position was opened */
openTimestamp: number;
}
/**
* Optional callbacks for risk events.
*/
interface IRiskCallbacks {
/** Called when a signal is rejected due to risk limits */
onRejected: (symbol: string, params: IRiskCheckArgs) => void;
/** Called when a signal passes risk checks */
onAllowed: (symbol: string, params: IRiskCheckArgs) => void;
}
/**
* Payload passed to risk validation functions.
* Extends IRiskCheckArgs with portfolio state data.
*/
interface IRiskValidationPayload extends IRiskCheckArgs {
/** Number of currently active positions across all strategies */
activePositionCount: number;
/** List of currently active positions across all strategies */
activePositions: IRiskActivePosition[];
}
/**
* Risk validation function type.
* Validates risk parameters and throws error if validation fails.
*/
interface IRiskValidationFn {
(payload: IRiskValidationPayload): void | Promise<void>;
}
/**
* Risk validation configuration.
* Defines validation logic with optional documentation.
*/
interface IRiskValidation {
/**
* The validation function to apply to the risk check parameters.
*/
validate: IRiskValidationFn;
/**
* Optional description for documentation purposes.
* Aids in understanding the purpose or behavior of the validation.
*/
note?: string;
}
/**
* Risk schema registered via addRisk().
* Defines portfolio-level risk controls via custom validations.
*/
interface IRiskSchema {
/** Unique risk profile identifier */
riskName: RiskName;
/** Optional developer note for documentation */
note?: string;
/** Optional lifecycle event callbacks (onRejected, onAllowed) */
callbacks?: Partial<IRiskCallbacks>;
/** Custom validations array for risk logic */
validations: (IRiskValidation | IRiskValidationFn)[];
}
/**
* Risk parameters passed to ClientRisk constructor.
* Combines schema with runtime dependencies.
*/
interface IRiskParams extends IRiskSchema {
/** Logger service for debug output */
logger: ILogger;
}
/**
* Risk interface implemented by ClientRisk.
* Provides risk checking for signals and position tracking.
*/
interface IRisk {
/**
* Check if a signal should be allowed based on risk limits.
*
* @param params - Risk check arguments (position size, portfolio state, etc.)
* @returns Promise resolving to risk check result
*/
checkSignal: (params: IRiskCheckArgs) => Promise<boolean>;
/**
* Register a new opened signal/position.
*
* @param symbol - Trading pair symbol
* @param context - Context information (strategyName, riskName)
*/
addSignal: (symbol: string, context: {
strategyName: string;
riskName: string;
}) => Promise<void>;
/**
* Remove a closed signal/position.
*
* @param symbol - Trading pair symbol
* @param context - Context information (strategyName, riskName)
*/
removeSignal: (symbol: string, context: {
strategyName: string;
riskName: string;
}) => Promise<void>;
}
/**
* Unique risk profile identifier.
*/
type RiskName = string;
/**
* Profit or loss level milestone in percentage points.
* Represents 10%, 20%, 30%, ..., 100% profit or loss thresholds.
*
* Used to track when a signal reaches specific profit/loss milestones.
* Each level is emitted only once per signal (deduplication via Set).
*
* @example
* ```typescript
* const level: PartialLevel = 50; // 50% profit or loss milestone
* ```
*/
type PartialLevel = 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
/**
* Serializable partial data for persistence layer.
* Converts Sets to arrays for JSON serialization.
*
* Stored in PersistPartialAdapter as Record<signalId, IPartialData>.
* Loaded on initialization and converted back to IPartialState.
*/
interface IPartialData {
/**
* Array of profit levels that have been reached for this signal.
* Serialized form of IPartialState.profitLevels Set.
*/
profitLevels: PartialLevel[];
/**
* Array of loss levels that have been reached for this signal.
* Serialized form of IPartialState.lossLevels Set.
*/
lossLevels: PartialLevel[];
}
/**
* Partial profit/loss tracking interface.
* Implemented by ClientPartial and PartialConnectionService.
*
* Tracks profit/loss level milestones for active trading signals.
* Emits events when signals reach 10%, 20%, 30%, etc profit or loss.
*
* @example
* ```typescript
* import { ClientPartial } from "./client/ClientPartial";
*
* const partial = new ClientPartial({
* logger: loggerService,
* onProfit: (symbol, data, price, level, backtest, timestamp) => {
* console.log(`Signal ${data.id} reached ${level}% profit`);
* },
* onLoss: (symbol, data, price, level, backtest, timestamp) => {
* console.log(`Signal ${data.id} reached ${level}% loss`);
* }
* });
*
* await partial.waitForInit("BTCUSDT");
*
* // During signal monitoring
* await partial.profit("BTCUSDT", signal, 51000, 15.5, false, new Date());
* // Emits event when reaching 10% profit milestone
*
* // When signal closes
* await partial.clear("BTCUSDT", signal, 52000);
* ```
*/
interface IPartial {
/**
* Processes profit state and emits events for new profit levels reached.
*
* Called by ClientStrategy during signal monitoring when revenuePercent > 0.
* Checks which profit levels (10%, 20%, 30%, etc) have been reached
* and emits events for new levels only (Set-based deduplication).
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param data - Signal row data
* @param currentPrice - Current market price
* @param revenuePercent - Current profit percentage (positive value)
* @param backtest - True if backtest mode, false if live mode
* @param when - Event timestamp (current time for live, candle time for backtest)
* @returns Promise that resolves when profit processing is complete
*
* @example
* ```typescript
* // Signal opened at $50000, current price $51500
* // Revenue: 3% profit
* await partial.profit("BTCUSDT", signal, 51500, 3.0, false, new Date());
* // No events emitted (below 10% threshold)
*
* // Price rises to $55000
* // Revenue: 10% profit
* await partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
* // Emits partialProfitSubject event for 10% level
*
* // Price rises to $61000
* // Revenue: 22% profit
* await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
* // Emits events for 20% level only (10% already emitted)
* ```
*/
profit(symbol: string, data: ISignalRow, currentPrice: number, revenuePercent: number, backtest: boolean, when: Date): Promise<void>;
/**
* Processes loss state and emits events for new loss levels reached.
*
* Called by ClientStrategy during signal monitoring when revenuePercent < 0.
* Checks which loss levels (10%, 20%, 30%, etc) have been reached
* and emits events for new levels only (Set-based deduplication).
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param data - Signal row data
* @param currentPrice - Current market price
* @param lossPercent - Current loss percentage (negative value)
* @param backtest - True if backtest mode, false if live mode
* @param when - Event timestamp (current time for live, candle time for backtest)
* @returns Promise that resolves when loss processing is complete
*
* @example
* ```typescript
* // Signal opened at $50000, current price $48000
* // Loss: -4% loss
* await partial.loss("BTCUSDT", signal, 48000, -4.0, false, new Date());
* // No events emitted (below -10% threshold)
*
* // Price drops to $45000
* // Loss: -10% loss
* await partial.loss("BTCUSDT", signal, 45000, -10.0, false, new Date());
* // Emits partialLossSubject event for 10% level
*
* // Price drops to $39000
* // Loss: -22% loss
* await partial.loss("BTCUSDT", signal, 39000, -22.0, false, new Date());
* // Emits events for 20% level only (10% already emitted)
* ```
*/
loss(symbol: string, data: ISignalRow, currentPrice: number, lossPercent: number, backtest: boolean, when: Date): Promise<void>;
/**
* Clears partial profit/loss state when signal closes.
*
* Called by ClientStrategy when signal completes (TP/SL/time_expired).
* Removes signal state from memory and persists changes to disk.
* Cleans up memoized ClientPartial instance in PartialConnectionService.
*
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
* @param data - Signal row data
* @param priceClose - Final closing price
* @returns Promise that resolves when clear is complete
*
* @example
* ```typescript
* // Signal closes at take profit
* await partial.clear("BTCUSDT", signal, 52000);
* // State removed from _states Map
* // Persisted to disk without this signal's data
* // Memoized instance cleared from getPartial cache
* ```
*/
clear(symbol: string, data: ISignalRow, priceClose: number): Promise<void>;
}
/**
* Signal generation interval for throttling.
* Enforces minimum time between getSignal calls.
*/
type SignalInterval = "1m" | "3m" | "5m" | "15m" | "30m" | "1h";
/**
* Signal data transfer object returned by getSignal.
* Will be validated and augmented with auto-generated id.
*/
interface ISignalDto {
/** Optional signal ID (auto-generated if not provided) */
id?: string;
/** Trade direction: "long" (buy) or "short" (sell) */
position: "long" | "short";
/** Human-readable description of signal reason */
note?: string;
/** Entry price for the position */
priceOpen?: number;
/** Take profit target price (must be > priceOpen for long, < priceOpen for short) */
priceTakeProfit: number;
/** Stop loss exit price (must be < priceOpen for long, > priceOpen for short) */
priceStopLoss: number;
/** Expected duration in minutes before time_expired */
minuteEstimatedTime: number;
}
/**
* Complete signal with auto-generated id.
* Used throughout the system after validation.
*/
interface ISignalRow extends ISignalDto {
/** Unique signal identifier (UUID v4 auto-generated) */
id: string;
/** Entry price for the position */
priceOpen: number;
/** Unique exchange identifier for execution */
exchangeName: ExchangeName;
/** Unique strategy identifier for execution */
strategyName: StrategyName;
/** Signal creation timestamp in milliseconds (when signal was first created/scheduled) */
scheduledAt: number;
/** Pending timestamp in milliseconds (when position became pending/active at priceOpen) */
pendingAt: number;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Internal runtime marker for scheduled signals */
_isScheduled: boolean;
}
/**
* Scheduled signal row for delayed entry at specific price.
* Inherits from ISignalRow - represents a signal waiting for price to reach priceOpen.
* Once price reaches priceOpen, will be converted to regular _pendingSignal.
* Note: pendingAt will be set to scheduledAt until activation, then updated to actual pending time.
*/
interface IScheduledSignalRow extends ISignalRow {
/** Entry price for the position */
priceOpen: number;
}
/**
* Optional lifecycle callbacks for signal events.
* Called when signals are opened, active, idle, closed, scheduled, or cancelled.
*/
interface IStrategyCallbacks {
/** Called on every tick with the result */
onTick: (symbol: string, result: IStrategyTickResult, backtest: boolean) => void;
/** Called when new signal is opened (after validation) */
onOpen: (symbol: string, data: ISignalRow, currentPrice: number, backtest: boolean) => void;
/** Called when signal is being monitored (active state) */
onActive: (symbol: string, data: ISignalRow, currentPrice: number, backtest: boolean) => void;
/** Called when no active signal exists (idle state) */
onIdle: (symbol: string, currentPrice: number, backtest: boolean) => void;
/** Called when signal is closed with final price */
onClose: (symbol: string, data: ISignalRow, priceClose: number, backtest: boolean) => void;
/** Called when scheduled signal is created (delayed entry) */
onSchedule: (symbol: string, data: IScheduledSignalRow, currentPrice: number, backtest: boolean) => void;
/** Called when scheduled signal is cancelled without opening position */
onCancel: (symbol: string, data: IScheduledSignalRow, currentPrice: number, backtest: boolean) => void;
/** Called when signal is written to persist storage (for testing) */
onWrite: (symbol: string, data: ISignalRow | null, backtest: boolean) => void;
/** Called when signal is in partial profit state (price moved favorably but not reached TP yet) */
onPartialProfit: (symbol: string, data: ISignalRow, currentPrice: number, revenuePercent: number, backtest: boolean) => void;
/** Called when signal is in partial loss state (price moved against position but not hit SL yet) */
onPartialLoss: (symbol: string, data: ISignalRow, currentPrice: number, lossPercent: number, backtest: boolean) => void;
}
/**
* Strategy schema registered via addStrategy().
* Defines signal generation logic and configuration.
*/
interface IStrategySchema {
/** Unique strategy identifier for registration */
strategyName: StrategyName;
/** Optional developer note for documentation */
note?: string;
/** Minimum interval between getSignal calls (throttling) */
interval: SignalInterval;
/**
* Signal generation function (returns null if no signal, validated DTO if signal).
* If priceOpen is provided - becomes scheduled signal waiting for price to reach entry point.
* If priceOpen is omitted - opens immediately at current price.
*/
getSignal: (symbol: string, when: Date) => Promise<ISignalDto | null>;
/** Optional lifecycle event callbacks (onOpen, onClose) */
callbacks?: Partial<IStrategyCallbacks>;
/** Optional risk profile identifier for risk management */
riskName?: RiskName;
}
/**
* Reason why signal was closed.
* Used in discriminated union for type-safe handling.
*/
type StrategyCloseReason = "time_expired" | "take_profit" | "stop_loss";
/**
* Profit and loss calculation result.
* Includes adjusted prices with fees (0.1%) and slippage (0.1%).
*/
interface IStrategyPnL {
/** Profit/loss as percentage (e.g., 1.5 for +1.5%, -2.3 for -2.3%) */
pnlPercentage: number;
/** Entry price adjusted with slippage and fees */
priceOpen: number;
/** Exit price adjusted with slippage and fees */
priceClose: number;
}
/**
* Tick result: no active signal, idle state.
*/
interface IStrategyTickResultIdle {
/** Discriminator for type-safe union */
action: "idle";
/** No signal in idle state */
signal: null;
/** Strategy name for tracking idle events */
strategyName: StrategyName;
/** Exchange name for tracking idle events */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Current VWAP price during idle state */
currentPrice: number;
}
/**
* Tick result: scheduled signal created, waiting for price to reach entry point.
* Triggered when getSignal returns signal with priceOpen specified.
*/
interface IStrategyTickResultScheduled {
/** Discriminator for type-safe union */
action: "scheduled";
/** Scheduled signal waiting for activation */
signal: IScheduledSignalRow;
/** Strategy name for tracking */
strategyName: StrategyName;
/** Exchange name for tracking */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Current VWAP price when scheduled signal created */
currentPrice: number;
}
/**
* Tick result: new signal just created.
* Triggered after getSignal validation and persistence.
*/
interface IStrategyTickResultOpened {
/** Discriminator for type-safe union */
action: "opened";
/** Newly created and validated signal with generated ID */
signal: ISignalRow;
/** Strategy name for tracking */
strategyName: StrategyName;
/** Exchange name for tracking */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Current VWAP price at signal open */
currentPrice: number;
}
/**
* Tick result: signal is being monitored.
* Waiting for TP/SL or time expiration.
*/
interface IStrategyTickResultActive {
/** Discriminator for type-safe union */
action: "active";
/** Currently monitored signal */
signal: ISignalRow;
/** Current VWAP price for monitoring */
currentPrice: number;
/** Strategy name for tracking */
strategyName: StrategyName;
/** Exchange name for tracking */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
}
/**
* Tick result: signal closed with PNL.
* Final state with close reason and profit/loss calculation.
*/
interface IStrategyTickResultClosed {
/** Discriminator for type-safe union */
action: "closed";
/** Completed signal with original parameters */
signal: ISignalRow;
/** Final VWAP price at close */
currentPrice: number;
/** Why signal closed (time_expired | take_profit | stop_loss) */
closeReason: StrategyCloseReason;
/** Unix timestamp in milliseconds when signal closed */
closeTimestamp: number;
/** Profit/loss calculation with fees and slippage */
pnl: IStrategyPnL;
/** Strategy name for tracking */
strategyName: StrategyName;
/** Exchange name for tracking */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
}
/**
* Tick result: scheduled signal cancelled without opening position.
* Occurs when scheduled signal doesn't activate or hits stop loss before entry.
*/
interface IStrategyTickResultCancelled {
/** Discriminator for type-safe union */
action: "cancelled";
/** Cancelled scheduled signal */
signal: IScheduledSignalRow;
/** Final VWAP price at cancellation */
currentPrice: number;
/** Unix timestamp in milliseconds when signal cancelled */
closeTimestamp: number;
/** Strategy name for tracking */
strategyName: StrategyName;
/** Exchange name for tracking */
exchangeName: ExchangeName;
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
}
/**
* Discriminated union of all tick results.
* Use type guards: `result.action === "closed"` for type safety.
*/
type IStrategyTickResult = IStrategyTickResultIdle | IStrategyTickResultScheduled | IStrategyTickResultOpened | IStrategyTickResultActive | IStrategyTickResultClosed | IStrategyTickResultCancelled;
/**
* Backtest returns closed result (TP/SL or time_expired) or cancelled result (scheduled signal never activated).
*/
type IStrategyBacktestResult = IStrategyTickResultClosed | IStrategyTickResultCancelled;
/**
* Unique strategy identifier.
*/
type StrategyName = string;
/**
* Statistical data calculated from backtest results.
*
* All numeric values are null if calculation is unsafe (NaN, Infinity, etc).
* Provides comprehensive metrics for strategy performance analysis.
*
* @example
* ```typescript
* const stats = await Backtest.getData("my-strategy");
*
* console.log(`Total signals: ${stats.totalSignals}`);
* console.log(`Win rate: ${stats.winRate}%`);
* console.log(`Sharpe Ratio: ${stats.sharpeRatio}`);
*
* // Access raw signal data
* stats.signalList.forEach(signal => {
* console.log(`Signal ${signal.signal.id}: ${signal.pnl.pnlPercentage}%`);
* });
* ```
*/
interface BacktestStatistics {
/** Array of all closed signals with full details (price, PNL, timestamps, etc.) */
signalList: IStrategyTickResultClosed[];
/** Total number of closed signals */
totalSignals: number;
/** Number of winning signals (PNL > 0) */
winCount: number;
/** Number of losing signals (PNL < 0) */
lossCount: number;
/** Win rate as percentage (0-100), null if unsafe. Higher is better. */
winRate: number | null;
/** Average PNL per signal as percentage, null if unsafe. Higher is better. */
avgPnl: number | null;
/** Cumulative PNL across all signals as percentage, null if unsafe. Higher is better. */
totalPnl: number | null;
/** Standard deviation of returns (volatility metric), null if unsafe. Lower is better. */
stdDev: number | null;
/** Sharpe Ratio (risk-adjusted return = avgPnl / stdDev), null if unsafe. Higher is better. */
sharpeRatio: number | null;
/** Annualized Sharpe Ratio (sharpeRatio × √365), null if unsafe. Higher is better. */
annualizedSharpeRatio: number | null;
/** Certainty Ratio (avgWin / |avgLoss|), null if unsafe. Higher is better. */
certaintyRatio: number | null;
/** Expected yearly returns based on average trade duration and PNL, null if unsafe. Higher is better. */
expectedYearlyReturns: number | null;
}
/**
* Service for generating and saving backtest markdown reports.
*
* Features:
* - Listens to signal events via onTick callback
* - Accumulates closed signals per strategy using memoized storage
* - Generates markdown tables with detailed signal information
* - Saves reports to disk in logs/backtest/{strategyName}.md
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
*
* // Add to strategy callbacks
* addStrategy({
* strategyName: "my-strategy",
* callbacks: {
* onTick: (symbol, result, backtest) => {
* service.tick(result);
* }
* }
* });
*
* // After backtest, generate and save report
* await service.saveReport("my-strategy");
* ```
*/
declare class BacktestMarkdownService {
/** Logger service for debug output */
private readonly loggerService;
/**
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
* Each symbol-strategy combination gets its own isolated storage instance.
*/
private getStorage;
/**
* Processes tick events and accumulates closed signals.
* Should be called from IStrategyCallbacks.onTick.
*
* Only processes closed signals - opened signals are ignored.
*
* @param data - Tick result from strategy execution (opened or closed)
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
*
* callbacks: {
* onTick: (symbol, result, backtest) => {
* service.tick(result);
* }
* }
* ```
*/
private tick;
/**
* Gets statistical data from all closed signals for a symbol-strategy pair.
* Delegates to ReportStorage.getData().
*
* @param symbol - Trading pair symbol
* @param strategyName - Strategy name to get data for
* @returns Statistical data object with all metrics
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
* const stats = await service.getData("BTCUSDT", "my-strategy");
* console.log(stats.sharpeRatio, stats.winRate);
* ```
*/
getData: (symbol: string, strategyName: StrategyName) => Promise<BacktestStatistics>;
/**
* Generates markdown report with all closed signals for a symbol-strategy pair.
* Delegates to ReportStorage.generateReport().
*
* @param symbol - Trading pair symbol
* @param strategyName - Strategy name to generate report for
* @returns Markdown formatted report string with table of all closed signals
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
* const markdown = await service.getReport("BTCUSDT", "my-strategy");
* console.log(markdown);
* ```
*/
getReport: (symbol: string, strategyName: StrategyName) => Promise<string>;
/**
* Saves symbol-strategy report to disk.
* Creates directory if it doesn't exist.
* Delegates to ReportStorage.dump().
*
* @param symbol - Trading pair symbol
* @param strategyName - Strategy name to save report for
* @param path - Directory path to save report (default: "./dump/backtest")
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
*
* // Save to default path: ./dump/backtest/my-strategy.md
* await service.dump("BTCUSDT", "my-strategy");
*
* // Save to custom path: ./custom/path/my-strategy.md
* await service.dump("BTCUSDT", "my-strategy", "./custom/path");
* ```
*/
dump: (symbol: string, strategyName: StrategyName, path?: string) => Promise<void>;
/**
* Clears accumulated signal data from storage.
* If ctx is provided, clears only that specific symbol-strategy pair's data.
* If nothing is provided, clears all data.
*
* @param ctx - Optional context with symbol and strategyName
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
*
* // Clear specific symbol-strategy pair
* await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
*
* // Clear all data
* await service.clear();
* ```
*/
clear: (ctx?: {
symbol: string;
strategyName: StrategyName;
}) => Promise<void>;
/**
* Initializes the service by subscribing to backtest signal events.
* Uses singleshot to ensure initialization happens only once.
* Automatically called on first use.
*
* @example
* ```typescript
* const service = new BacktestMarkdownService();
* await service.init(); // Subscribe to backtest events
* ```
*/
protected init: (() => Promise<void>) & functools_kit.ISingleshotClearable;
}
/**
* Optimization metric for comparing strategies.
* Higher values are always better (metric is maximized).
*/
type WalkerMetric = "sharpeRatio" | "annualizedSharpeRatio" | "winRate" | "totalPnl" | "certaintyRatio" | "avgPnl" | "expectedYearlyReturns";
/**
* Walker schema registered via addWalker().
* Defines A/B testing configuration for multiple strategies.
*/
interface IWalkerSchema {
/** Unique walker identifier for registration */
walkerName: WalkerName;
/** Optional developer note for documentation */
note?: string;
/** Exchange to use for backtesting all strategies */
exchangeName: ExchangeName;
/** Timeframe generator to use for backtesting all strategies */
frameName: FrameName;
/** List of strategy names to compare (must be registered via addStrategy) */
strategies: StrategyName[];
/** Metric to optimize (default: "sharpeRatio") */
metric?: WalkerMetric;
/** Optional lifecycle event callbacks */
callbacks?: Partial<IWalkerCallbacks>;
}
/**
* Optional lifecycle callbacks for walker events.
* Called during strategy comparison process.
*/
interface IWalkerCallbacks {
/** Called when starting to test a specific strategy */
onStrategyStart: (strategyName: StrategyName, symbol: string) => void;
/** Called when a strategy backtest completes */
onStrategyComplete: (strategyName: StrategyName, symbol: string, stats: BacktestStatistics, metric: number | null) => void;
/** Called when a strategy backtest fails with an error */
onStrategyError: (strategyName: StrategyName, symbol: string, error: Error | unknown) => void;
/** Called when all strategies have been tested */
onComplete: (results: IWalkerResults) => void;
}
/**
* Result for a single strategy in the comparison.
*/
interface IWalkerStrategyResult {
/** Strategy name */
strategyName: StrategyName;
/** Backtest statistics for this strategy */
stats: BacktestStatistics;
/** Metric value used for comparison (null if invalid) */
metric: number | null;
/** Rank position (1 = best, 2 = second best, etc.) */
rank: number;
}
/**
* Complete walker results after comparing all strategies.
*/
interface IWalkerResults {
/** Walker name */
walkerName: WalkerName;
/** Symbol tested */
symbol: string;
/** Exchange used */
exchangeName: ExchangeName;
/** Frame used */
frameName: FrameName;
/** Metric used for optimization */
metric: WalkerMetric;
/** Total number of strategies tested */
totalStrategies: number;
/** Best performing strategy name */
bestStrategy: StrategyName | null;
/** Best metric value achieved */
bestMetric: number | null;
/** Best strategy statistics */
bestStats: BacktestStatistics | null;
}
/**
* Unique walker identifier.
*/
type WalkerName = string;
/**
* Base parameters common to all sizing calculations.
*/
interface ISizingCalculateParamsBase {
/** Trading pair symbol (e.g., "BTCUSDT") */
symbol: string;
/** Current account balance */
accountBalance: number;
/** Planned entry price */
priceOpen: number;
}
/**
* Public API parameters for fixed percentage sizing (without method field).
*/
interface IPositionSizeFixedPercentageParams extends ISizingCalculateParamsBase {
/** Stop-loss price */
priceStopLoss: number;
}
/**
* Public API parameters for Kelly Criterion sizing (without method field).
*/
interface IPositionSizeKellyParams extends ISizingCalculateParamsBase {
/** Win rate (0-1) */
winRate: number;
/** Average win/loss ratio */
winLossRatio: number;
}
/**
* Public API parameters for ATR-based sizing (without method field).
*/
interface IPositionSizeATRParams extends ISizingCalculateParamsBase {
/** Current ATR value */
atr: number;
}
/**
* Parameters for fixed percentage sizing calculation.
*/
interface ISizingCalculateParamsFixedPercentage extends ISizingCalculateParamsBase {
method: "fixed-percentage";
/** Stop-loss price */
priceStopLoss: number;
}
/**
* Parameters for Kelly Criterion sizing calculation.
*/
interface ISizingCalculateParamsKelly extends ISizingCalculateParamsBase {
method: "kelly-criterion";
/** Win rate (0-1) */
winRate: number;
/** Average win/loss ratio */
winLossRatio: number;
}
/**
* Parameters for ATR-based sizing calculation.
*/
interface ISizingCalculateParamsATR extends ISizingCalculateParamsBase {
method: "atr-based";
/** Current ATR value */
atr: number;
}
/**
* Discriminated union for position size calculation parameters.
* Type-safe parameters based on sizing method.
*/
type ISizingCalculateParams = ISizingCalculateParamsFixedPercentage | ISizingCalculateParamsKelly | ISizingCalculateParamsATR;
/**
* Fixed percentage sizing parameters for ClientSizing constructor.
*/
interface ISizingParamsFixedPercentage extends ISizingSchemaFixedPercentage {
/** Logger service for debug output */
logger: ILogger;
}
/**
* Kelly Criterion sizing parameters for ClientSizing constructor.
*/
interface ISizingParamsKelly extends ISizingSchemaKelly {
/** Logger service for debug output */
logger: ILogger;
}
/**
* ATR-based sizing parameters for ClientSizing constructor.
*/
interface ISizingParamsATR extends ISizingSchemaATR {
/** Logger service for debug output */
logger: ILogger;
}
/**
* Discriminated union for sizing parameters passed to ClientSizing constructor.
* Extends ISizingSchema with logger instance for internal logging.
*/
type ISizingParams = ISizingParamsFixedPercentage | ISizingParamsKelly | ISizingParamsATR;
/**
* Callbacks for sizing lifecycle events.
*/
interface ISizingCallbacks {
/**
* Called after position size calculation.
* Useful for logging or validating the calculated size.
*
* @param quantity - Calculated position size
* @param params - Parameters used for calculation
*/
onCalculate: (quantity: number, params: ISizingCalculateParams) => void;
}
/**
* Base sizing schema with common fields.
*/
interface ISizingSchemaBase {
/** Unique identifier for this sizing configuration */
sizingName: SizingName;
/** Optional developer note for documentation */
note?: string;
/** Maximum position size as % of account (0-100) */
maxPositionPercentage?: number;
/** Minimum position size (absolute value) */
minPositionSize?: number;
/** Maximum position size (absolute value) */
maxPositionSize?: number;
/** Optional lifecycle callbacks */
callbacks?: Partial<ISizingCallbacks>;
}
/**
* Fixed percentage sizing schema.
*
* @example
* ```typescript
* addSizing({
* sizingName: "conservative",
* method: "fixed-percentage",
* riskPercentage: 1,
* });
* ```
*/
interface ISizingSchemaFixedPercentage extends ISizingSchemaBase {
method: "fixed-percentage";