This document describes the client implementation classes that form the core business logic layer of backtest-kit. Client implementations are concrete classes that execute trading operations: signal generation, market data fetching, risk validation, position sizing, partial profit tracking, and strategy optimization.
For schema definitions that configure these clients, see Component Schemas. For the service layer that routes operations to client instances, see Service Layer. For the signal lifecycle state machine, see Signal Lifecycle.
Client implementations sit between the connection service layer and the trading domain logic. They are instantiated by connection services and cached using memoization patterns to ensure singleton behavior per trading context.
ClientStrategy implements the IStrategy interface and is the most complex client class. It manages the complete signal lifecycle from generation through validation, monitoring, and closure.
| Responsibility | Methods | Description |
|---|---|---|
| Signal Generation | GET_SIGNAL_FN |
Calls user's getSignal() with throttling and timeout |
| Validation | VALIDATE_SIGNAL_FN |
Multi-layer validation: types, prices, logic, distance, time |
| State Machine | tick(), backtest() |
Manages transitions between idle → scheduled → pending → active → closed |
| Persistence | setPendingSignal(), setScheduledSignal() |
Atomic writes for crash recovery |
| Lifecycle Callbacks | onOpen, onActive, onClose, onSchedule, onCancel |
User event notifications |
ClientStrategy performs extensive validation using VALIDATE_SIGNAL_FN before allowing signal creation. This function prevents logically impossible or financially dangerous signals.
| Validation Category | Checks Performed | Example Rejection |
|---|---|---|
| Required Fields | id, symbol, position, _isScheduled must exist |
id === undefined |
| NaN/Infinity Protection | All prices and currentPrice must be finite numbers |
priceOpen === NaN |
| Positive Prices | priceOpen, priceTakeProfit, priceStopLoss > 0 |
priceStopLoss === 0 |
| Long Position Logic | priceTakeProfit > priceOpen > priceStopLoss |
TP=40000, Open=42000 |
| Short Position Logic | priceStopLoss > priceOpen > priceTakeProfit |
SL=40000, Open=42000 |
| Instant Closure Protection | Current price not already beyond TP/SL | Long with currentPrice >= TP |
| Minimum TP Distance | CC_MIN_TAKEPROFIT_DISTANCE_PERCENT enforced |
TP only 0.05% from open |
| Minimum SL Distance | CC_MIN_STOPLOSS_DISTANCE_PERCENT enforced |
SL only 0.01% from open |
| Maximum SL Distance | CC_MAX_STOPLOSS_DISTANCE_PERCENT enforced |
SL 50% away from open |
| Time Validation | minuteEstimatedTime must be positive integer |
minuteEstimatedTime === 0 |
| Maximum Lifetime | CC_MAX_SIGNAL_LIFETIME_MINUTES enforced |
minuteEstimatedTime > 10080 (7 days) |
Scheduled signals (limit orders) require special handling for activation and cancellation. The logic checks both price conditions and stop loss violations.
| Method | Signature | Purpose |
|---|---|---|
tick() |
(symbol, strategyName) => Promise<IStrategyTickResult> |
Single execution cycle for live trading |
backtest() |
(symbol, strategyName, candles) => Promise<IStrategyBacktestResult> |
Fast-forward through historical candles |
waitForInit() |
() => Promise<void> |
Load persisted state from disk (live mode only) |
getPendingSignal() |
(symbol, strategyName) => Promise<ISignalRow | null> |
Retrieve active pending signal |
stop() |
(symbol, strategyName, backtest) => Promise<void> |
Prevent new signal generation |
setPendingSignal() |
(signal) => Promise<void> |
Persist pending signal atomically |
setScheduledSignal() |
(signal) => Promise<void> |
Persist scheduled signal atomically |
ClientExchange implements the IExchange interface and provides market data with temporal isolation guarantees.
| Responsibility | Methods | Description |
|---|---|---|
| Candle Fetching | getCandles() |
Retrieves historical OHLCV data with temporal bounds |
| VWAP Calculation | getAveragePrice() |
Volume-weighted average of last N candles |
| Price Formatting | formatPrice(), formatQuantity() |
Display formatting for UI |
| Temporal Isolation | Context awareness | Prevents look-ahead bias via ExecutionContextService |
The system uses Volume Weighted Average Price (VWAP) instead of simple close prices for realistic execution simulation. VWAP is calculated from the last CC_AVG_PRICE_CANDLES_COUNT (default: 5) one-minute candles.
**Formula (from src/client/ClientStrategy.ts:478-489):
typicalPrice = (high + low + close) / 3
sumPriceVolume = Σ(typicalPrice × volume)
totalVolume = Σ(volume)
VWAP = sumPriceVolume / totalVolume
If totalVolume === 0, fallback to simple average: Σ(close) / candles.length
ClientExchange.getCandles() uses ExecutionContextService to enforce temporal bounds. In backtest mode, it never returns data from the future, preventing look-ahead bias.
ClientRisk implements the IRisk interface and enforces portfolio-level constraints through validation chains.
| Responsibility | Methods | Description |
|---|---|---|
| Signal Validation | checkSignal() |
Runs validation chain against pending signal |
| Position Tracking | addSignal(), removeSignal() |
Tracks active positions per strategy |
| Persistence | Via PersistRiskAdapter |
Atomic state saves for crash recovery |
| Validation Callbacks | onAllowed, onRejected |
User notifications for validation results |
ClientRisk maintains _activePositionsMap to track concurrent positions per strategy. This enables maxConcurrentPositions enforcement and portfolio-level risk management.
Data Structure:
_activePositionsMap = Map<symbol, Set<strategyIdentifier>>
// Example: Map { "BTCUSDT" => Set { "strategy1:riskA", "strategy2:riskA" } }
Persistence: State saved to ./dump/data/risk/{riskName}.json via PersistRiskAdapter after every addSignal() or removeSignal() call.
ClientSizing implements the ISizing interface and calculates position sizes based on account balance and risk parameters.
| Method | Formula | Use Case |
|---|---|---|
| Fixed Percentage | position = balance × fixedPercent / 100 |
Conservative, predictable sizing |
| Kelly Criterion | f* = (p × b - q) / bwhere p=winRate, b=avgWin/avgLoss |
Optimal growth, requires historical data |
| ATR-Based | position = (balance × riskPercent) / (ATR × atrMultiplier) |
Volatility-adjusted sizing |
All methods respect constraints from ISizingSchema:
minPositionSize: Minimum position valuemaxPositionSize: Maximum position valuemaxPositionPercent: Maximum % of account balanceClientPartial implements the IPartial interface and tracks profit/loss milestones for active signals.
| Event Type | Levels | Trigger |
|---|---|---|
| Partial Profit | TP_LEVEL1, TP_LEVEL2, TP_LEVEL3 |
33%, 66%, 100% toward TP |
| Partial Loss | SL_LEVEL1, SL_LEVEL2 |
50%, 100% toward SL |
State Persistence: ./dump/data/partial/{strategy}/{symbol}.json stores already-emitted milestones to prevent duplicate events after crash recovery.
ClientFrame implements the IFrame interface and generates timeframes for backtesting.
Example:
startDate: 2025-01-01 00:00:00endDate: 2025-01-02 00:00:00interval: 1hClientOptimizer implements the IOptimizer interface and generates executable strategy code using LLMs.
IOptimizerSource can be a function or an object:
Function-based:
{
fetch: (params) => Promise<any[]>
}
Object-based:
{
fetch: (params) => Promise<any[]>,
user: (doc) => string, // Format as user message
assistant: (doc) => string // Format as assistant message
}
Pagination: Uses iterateDocuments() for paginated fetching and distinctDocuments() for deduplication by ID.
All connection services use memoize() from functools-kit to cache client instances:
private getStrategy = memoize(
([symbol, strategyName, backtest]) => `${symbol}:${strategyName}:${backtest}`,
(symbol, strategyName, backtest) => new ClientStrategy({...})
)
Cache keys include:
symbol: Trading pairstrategyName or component namebacktest: Boolean flag (separate instances for backtest vs live)Benefits:
Client implementations that require crash recovery use persistence adapters:
| Adapter | File Path | Purpose |
|---|---|---|
PersistSignalAdapter |
./dump/data/signal/{strategy}/{symbol}.json |
Active pending signals |
PersistRiskAdapter |
./dump/data/risk/{riskName}.json |
Portfolio position counts |
PersistScheduleAdapter |
./dump/data/schedule/{strategy}/{symbol}.json |
Scheduled signals (limit orders) |
PersistPartialAdapter |
./dump/data/partial/{strategy}/{symbol}.json |
Emitted milestone levels |
Atomic Write Pattern:
data.json.tmpfsync() to ensure disk writedata.json.tmp → data.json (atomic operation)All clients receive ExecutionContextService and MethodContextService in their constructor parameters:
interface IStrategyParams {
execution: TExecutionContextService; // Runtime context (symbol, when, backtest)
method: TMethodContextService; // Schema context (strategyName, exchangeName)
// ... other dependencies
}
ExecutionContextService provides:
context.symbol: Current trading paircontext.when: Current timestamp (Date object)context.backtest: Boolean flag for modeMethodContextService provides:
context.strategyName: Active strategy identifiercontext.exchangeName: Active exchange identifiercontext.frameName: Active frame identifier (backtest only)These contexts use AsyncLocalStorage for implicit propagation without manual parameter threading.
Client implementations emit events through RxJS Subjects after state changes:
// ClientStrategy emits signals
await signalEmitter.next(tickResult);
await signalBacktestEmitter.next(tickResult);
await signalLiveEmitter.next(tickResult);
Emitters used by clients:
signalEmitter: All signals (backtest + live)signalBacktestEmitter: Backtest-only signalssignalLiveEmitter: Live-only signalserrorEmitter: Exception propagationpartialProfitSubject: TP milestone eventspartialLossSubject: SL milestone events