Logic Services

Logic Services implement the core execution orchestration layer for the three operational modes: Backtest, Live, and Walker. These services manage the temporal progression, signal lifecycle coordination, and result streaming for each execution mode.

This page documents the Logic Service architecture, the Private/Public service separation pattern, and the AsyncGenerator streaming model. For information about Strategy execution logic, see ClientStrategy. For command validation and delegation, see Service Architecture Overview.

Logic Services are organized into three execution mode families, each with Private and Public service tiers:

Execution Mode Private Service Public Service Purpose
Backtest BacktestLogicPrivateService BacktestLogicPublicService Historical simulation with timeframe iteration
Live LiveLogicPrivateService LiveLogicPublicService Real-time trading with crash recovery
Walker WalkerLogicPrivateService WalkerLogicPublicService Strategy comparison with metric optimization

Private Services implement the core execution algorithms using AsyncGenerator streaming. They have no context management and require explicit parameters.

Public Services wrap Private Services with MethodContextService to provide implicit context propagation, allowing downstream functions to access strategyName, exchangeName, and frameName without explicit parameters.

Mermaid Diagram

The Logic Service architecture implements a two-tier separation pattern where Private Services contain pure execution algorithms and Public Services add context management.

Private Services implement the core execution logic with these properties:

  • No Context Dependencies: All parameters are explicitly passed to methods
  • AsyncGenerator Streaming: Results are yielded incrementally for memory efficiency
  • Dependency Injection: Services are injected via inject<T>(TYPES.*) pattern
  • Event Emission: Progress, performance, and error events are emitted directly
  • No Context Wrapping: Calls to Global Services pass context explicitly

src/lib/services/logic/private/BacktestLogicPrivateService.ts:33-46 demonstrates the Private Service dependency pattern:

export class BacktestLogicPrivateService {
private readonly loggerService = inject<LoggerService>(TYPES.loggerService);
private readonly strategyGlobalService = inject<StrategyGlobalService>(
TYPES.strategyGlobalService
);
private readonly exchangeGlobalService = inject<ExchangeGlobalService>(
TYPES.exchangeGlobalService
);
private readonly frameGlobalService = inject<FrameGlobalService>(
TYPES.frameGlobalService
);
private readonly methodContextService = inject<TMethodContextService>(
TYPES.methodContextService
);

Public Services wrap Private Services with context injection:

  • Context Propagation: Wraps Private Service calls with MethodContextService.bind()
  • Simplified API: Accepts context object instead of explicit service references
  • Consistent Interface: All Public Services expose run(symbol, context) signature
  • Transparent Delegation: Context is injected, then Private Service is called

The Public Service pattern is implemented in src/lib/services/logic/public/BacktestLogicPublicService.ts using function composition:

public readonly run = (
symbol: string,
context: {
strategyName: string;
exchangeName: string;
frameName: string;
}
): AsyncGenerator<IStrategyBacktestResult> =>
this.methodContextService.bind(
context,
() => this.backtestLogicPrivateService.run(symbol)
);

This pattern allows downstream functions like getCandles() and getSignal() to access context implicitly through MethodContextService.

BacktestLogicPrivateService implements historical simulation through sequential timeframe iteration with skip-ahead optimization.

Mermaid Diagram

Timeframe Iteration: src/lib/services/logic/private/BacktestLogicPrivateService.ts:69-74 loads the complete date range from FrameGlobalService and iterates sequentially:

const timeframes = await this.frameGlobalService.getTimeframe(
symbol,
this.methodContextService.context.frameName
);

Signal Detection: src/lib/services/logic/private/BacktestLogicPrivateService.ts:96 calls tick() with backtest=true to check for signal generation at each timeframe:

result = await this.strategyGlobalService.tick(symbol, when, true);

Scheduled Signal Handling: src/lib/services/logic/private/BacktestLogicPrivateService.ts:113-155 fetches extended candle range for scheduled signals to monitor activation/cancellation plus full signal duration:

const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
candles = await this.exchangeGlobalService.getNextCandles(
symbol,
"1m",
candlesNeeded,
when,
true
);

Skip-Ahead Optimization: src/lib/services/logic/private/BacktestLogicPrivateService.ts:227-232 advances the timeframe iterator to the signal close time, avoiding redundant tick() calls:

while (
i < timeframes.length &&
timeframes[i].getTime() < backtestResult.closeTimestamp
) {
i++;
}

Performance Tracking: src/lib/services/logic/private/BacktestLogicPrivateService.ts:214-224 emits granular timing metrics for signal processing duration:

await performanceEmitter.next({
timestamp: currentTimestamp,
previousTimestamp: previousEventTimestamp,
metricType: "backtest_signal",
duration: signalEndTime - signalStartTime,
strategyName: this.methodContextService.context.strategyName,
exchangeName: this.methodContextService.context.exchangeName,
symbol,
backtest: true,
});

All operations are wrapped in try-catch blocks that emit errors via errorEmitter and continue iteration:

LiveLogicPrivateService implements real-time trading through an infinite polling loop with crash recovery support.

Mermaid Diagram

Infinite Loop: src/lib/services/logic/private/LiveLogicPrivateService.ts:68 implements continuous monitoring that never completes:

while (true) {
const when = new Date();
// ... tick logic
await sleep(TICK_TTL);
}

Real-Time Timestamps: src/lib/services/logic/private/LiveLogicPrivateService.ts:70 creates fresh Date objects for each iteration, ensuring time progresses naturally:

const when = new Date();

Tick Interval: src/lib/services/logic/private/LiveLogicPrivateService.ts:12 defines a 61-second polling interval (slightly over 1 minute to avoid clock skew issues):

const TICK_TTL = 1 * 60 * 1_000 + 1;

Selective Yielding: src/lib/services/logic/private/LiveLogicPrivateService.ts:110-126 filters tick results, only yielding opened and closed actions while sleeping for idle, active, and scheduled:

if (result.action === "active") {
await sleep(TICK_TTL);
continue;
}

if (result.action === "idle") {
await sleep(TICK_TTL);
continue;
}

if (result.action === "scheduled") {
await sleep(TICK_TTL);
continue;
}

// Yield opened, closed results
yield result as IStrategyTickResultClosed | IStrategyTickResultOpened;

Crash Recovery: State recovery happens transparently in ClientStrategy.waitForInit() which is called by StrategyGlobalService before the first tick(). The Live Logic Service has no explicit recovery logic because persistence is handled by ClientStrategy.

Error Resilience: src/lib/services/logic/private/LiveLogicPrivateService.ts:75-88 catches all tick errors, emits them, sleeps, and continues the loop:

try {
result = await this.strategyGlobalService.tick(symbol, when, false);
} catch (error) {
this.loggerService.warn(
"liveLogicPrivateService tick failed, retrying after sleep",
{ symbol, when: when.toISOString(), error: errorData(error) }
);
await errorEmitter.next(error);
await sleep(TICK_TTL);
continue;
}

WalkerLogicPrivateService orchestrates strategy comparison by running multiple backtest executions sequentially and tracking the best-performing strategy by a configurable metric.

Mermaid Diagram

Sequential Execution: src/lib/services/logic/private/WalkerLogicPrivateService.ts:107-228 runs one backtest at a time to avoid resource contention:

for (const strategyName of strategies) {
const iterator = this.backtestLogicPublicService.run(symbol, {
strategyName,
exchangeName: context.exchangeName,
frameName: context.frameName,
});

result = await resolveDocuments(iterator);
// ... process results
}

Backtest Delegation: src/lib/services/logic/private/WalkerLogicPrivateService.ts:117-121 uses BacktestLogicPublicService to execute each strategy with proper context injection:

const iterator = this.backtestLogicPublicService.run(symbol, {
strategyName,
exchangeName: context.exchangeName,
frameName: context.frameName,
});

Statistics Extraction: src/lib/services/logic/private/WalkerLogicPrivateService.ts:165 retrieves computed metrics from BacktestMarkdownService:

const stats = await this.backtestMarkdownService.getData(symbol, strategyName);

Metric Comparison: src/lib/services/logic/private/WalkerLogicPrivateService.ts:167-186 extracts and validates the configured metric, updating best strategy if superior:

const value = stats[metric];
const metricValue =
value !== null &&
value !== undefined &&
typeof value === "number" &&
!isNaN(value) &&
isFinite(value)
? value
: null;

const isBetter =
bestMetric === null ||
(metricValue !== null && metricValue > bestMetric);

if (isBetter && metricValue !== null) {
bestMetric = metricValue;
bestStrategy = strategyName;
}

Progress Yielding: src/lib/services/logic/private/WalkerLogicPrivateService.ts:190-227 constructs and yields WalkerContract after each strategy completes:

const walkerContract: WalkerContract = {
walkerName: context.walkerName,
exchangeName: context.exchangeName,
frameName: context.frameName,
symbol,
strategyName,
stats,
metricValue,
metric,
bestMetric,
bestStrategy,
strategiesTested,
totalStrategies: strategies.length,
};

await walkerEmitter.next(walkerContract);
yield walkerContract;

Schema Callbacks: src/lib/services/logic/private/WalkerLogicPrivateService.ts:109-111, src/lib/services/logic/private/WalkerLogicPrivateService.ts:217-224, and src/lib/services/logic/private/WalkerLogicPrivateService.ts:246-248 invoke optional lifecycle callbacks:

  • onStrategyStart(strategyName, symbol): Before backtest begins
  • onStrategyComplete(strategyName, symbol, stats, metricValue): After backtest finishes
  • onStrategyError(strategyName, symbol, error): If backtest fails
  • onComplete(finalResults): After all strategies complete

Cancellation Support: src/lib/services/logic/private/WalkerLogicPrivateService.ts:96-104 listens for walkerStopSubject events to allow external cancellation:

const listenStop = walkerStopSubject
.filter((data) => {
let isOk = true;
isOk = isOk && data.symbol === symbol;
isOk = isOk && data.strategyName === pendingStrategy;
return isOk;
})
.map(() => CANCEL_SYMBOL)
.toPromise();

All Logic Services use AsyncGenerator functions (async *) for memory-efficient result streaming with support for early termination.

Mermaid Diagram

Aspect Traditional Array Return AsyncGenerator Streaming
Memory Usage Accumulates all results in memory Processes one result at a time
Time to First Result Must complete entire execution Yields results incrementally
Early Termination Not supported Consumer can break to cancel
Progress Monitoring No visibility until complete Real-time progress via yielded results
Error Recovery Single point of failure Can catch errors per iteration

Backtest Mode - Finite Generator:

  • Yields IStrategyBacktestResult for each closed signal
  • Completes when all timeframes are processed
  • Consumer receives final result when generator exhausts

Live Mode - Infinite Generator:

  • Yields IStrategyTickResultOpened and IStrategyTickResultClosed
  • Never completes (infinite while(true) loop)
  • Consumer must handle continuous stream

Walker Mode - Progress Generator:

  • Yields WalkerContract after each strategy completes
  • Shows incremental progress with strategiesTested counter
  • Completes when all strategies are compared

Consumers iterate Logic Services using for await...of:

// Backtest: Collect all results
const results = [];
for await (const result of backtestLogic.run("BTCUSDT")) {
results.push(result);
console.log("Closed:", result.closeReason, result.pnl.pnlPercentage);
}

// Live: Infinite monitoring
for await (const result of liveLogic.run("BTCUSDT")) {
if (result.action === "opened") {
console.log("New signal:", result.signal.id);
}
if (result.action === "closed") {
console.log("PNL:", result.pnl.pnlPercentage);
}
}

// Walker: Progress tracking
for await (const progress of walkerLogic.run("BTCUSDT", strategies, metric, ctx)) {
console.log(`Progress: ${progress.strategiesTested}/${progress.totalStrategies}`);
console.log(`Best: ${progress.bestStrategy} = ${progress.bestMetric}`);
}

Early termination example from src/lib/services/logic/private/BacktestLogicPrivateService.ts:58-59:

for await (const result of backtestLogic.run("BTCUSDT")) {
if (result.pnl.pnlPercentage < -10) break; // Cancel remaining execution
}

Logic Services emit events at key execution points for observability and progress tracking.

Service Events Emitted Purpose
BacktestLogicPrivateService progressBacktestEmitter
performanceEmitter
errorEmitter
Frame progress
Timing metrics
Error logging
LiveLogicPrivateService performanceEmitter
errorEmitter
Tick timing
Error logging
WalkerLogicPrivateService progressWalkerEmitter
walkerEmitter
walkerCompleteSubject
errorEmitter
Strategy progress
Current best
Final results
Error logging

Backtest Progress: src/lib/services/logic/private/BacktestLogicPrivateService.ts:84-92 emits frame-level progress with completion percentage:

await progressBacktestEmitter.next({
exchangeName: this.methodContextService.context.exchangeName,
strategyName: this.methodContextService.context.strategyName,
symbol,
totalFrames,
processedFrames: i,
progress: totalFrames > 0 ? i / totalFrames : 0,
});

Walker Progress: src/lib/services/logic/private/WalkerLogicPrivateService.ts:206-214 emits strategy-level progress:

await progressWalkerEmitter.next({
walkerName: context.walkerName,
exchangeName: context.exchangeName,
frameName: context.frameName,
symbol,
totalStrategies: strategies.length,
processedStrategies: strategiesTested,
progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
});

All Logic Services emit granular timing metrics via performanceEmitter:

  • metricType: Identifies the operation type
    • "backtest_timeframe": Time per timeframe iteration
    • "backtest_signal": Time to process one signal
    • "backtest_total": Total backtest duration
    • "live_tick": Time per live tick
  • duration: Elapsed time in milliseconds
  • timestamp: Current timestamp
  • previousTimestamp: Previous event timestamp (for interval calculation)
  • strategyName, exchangeName, symbol, backtest: Context metadata

Example from src/lib/services/logic/private/BacktestLogicPrivateService.ts:214-224:

await performanceEmitter.next({
timestamp: currentTimestamp,
previousTimestamp: previousEventTimestamp,
metricType: "backtest_signal",
duration: signalEndTime - signalStartTime,
strategyName: this.methodContextService.context.strategyName,
exchangeName: this.methodContextService.context.exchangeName,
symbol,
backtest: true,
});

Logic Services coordinate execution by orchestrating calls to Global Services, Schema Services, and Markdown Services.

Mermaid Diagram

BacktestLogicPrivateService Dependencies:

  • StrategyGlobalService: Calls tick() and backtest() for signal lifecycle
  • ExchangeGlobalService: Calls getNextCandles() for future data fetching
  • FrameGlobalService: Calls getTimeframe() to load date range
  • MethodContextService: Reads strategyName, exchangeName, frameName from context

LiveLogicPrivateService Dependencies:

  • StrategyGlobalService: Calls tick() for signal status checking
  • MethodContextService: Reads strategyName, exchangeName from context

WalkerLogicPrivateService Dependencies:

  • BacktestLogicPublicService: Delegates to backtest execution
  • WalkerSchemaService: Retrieves walker configuration and callbacks
  • BacktestMarkdownService: Extracts statistics for metric comparison

Backtest Timeframe Iteration: src/lib/services/logic/private/BacktestLogicPrivateService.ts:69-74

FrameGlobalService.getTimeframe()
returns Date[] array
BacktestLogic iterates sequentially
StrategyGlobalService.tick() per timeframe

Live Continuous Polling: src/lib/services/logic/private/LiveLogicPrivateService.ts:68-74

while (true):
new Date() creates current timestamp
StrategyGlobalService.tick(when, false)
sleep(TICK_TTL)

Walker Strategy Comparison: src/lib/services/logic/private/WalkerLogicPrivateService.ts:107-165

for each strategy:
BacktestLogicPublicService.run(symbol, context)
await all results
BacktestMarkdownService.getData(symbol, strategy)
compare metric values
Characteristic Backtest Live Walker
Completion Finite (exhausts timeframes) Infinite (never completes) Finite (exhausts strategies)
Time Progression Historical (from frame array) Real-time (new Date()) Historical (delegates to Backtest)
Result Type IStrategyBacktestResult IStrategyTickResultOpened
IStrategyTickResultClosed
WalkerContract
Primary Loop for over timeframes while(true) infinite for over strategies
Sleep/Delay None (continuous) TICK_TTL (61s) between ticks None (sequential)
Persistence None (stateless) Crash recovery via ClientStrategy None (stateless)
Progress Events Frame count and percentage No progress events Strategy count and percentage
Signal Processing Fast-forward with getNextCandles() Real-time monitoring Delegates to Backtest
Skip Optimization Yes (jump to closeTimestamp) No (continuous monitoring) N/A (runs full backtest)
Error Handling Skip timeframe, continue Sleep and retry Skip strategy, continue
Context Parameters strategyName, exchangeName, frameName strategyName, exchangeName walkerName, exchangeName, frameName
Use Case Historical analysis Production trading Strategy comparison