Base class for custom action handlers.

Provides default implementations for all IPublicAction methods that log events. Extend this class to implement custom action handlers for:

  • State management (Redux, Zustand, MobX)
  • Real-time notifications (Telegram, Discord, Email)
  • Event logging and monitoring
  • Analytics and metrics collection
  • Custom business logic triggers

Key features:

  • All methods have default implementations (no need to implement unused methods)
  • Automatic logging of all events via backtest.loggerService
  • Access to strategy context (strategyName, frameName, actionName)
  • Implements full IPublicAction interface

Lifecycle:

  1. Constructor called with (strategyName, frameName, actionName)
  2. init() called once for async initialization
  3. Event methods called as strategy executes (signal, breakeven, partialProfit, etc.)
  4. dispose() called once for cleanup

Event flow:

  • signal() - Called on every tick/candle (all modes)
  • signalLive() - Called only in live mode
  • signalBacktest() - Called only in backtest mode
  • breakevenAvailable() - Called when SL moved to entry
  • partialProfitAvailable() - Called on profit milestones (10%, 20%, etc.)
  • partialLossAvailable() - Called on loss milestones (-10%, -20%, etc.)
  • pingScheduled() - Called every minute during scheduled signal monitoring
  • pingActive() - Called every minute during active pending signal monitoring
  • riskRejection() - Called when signal rejected by risk management
import { ActionBase } from "backtest-kit";

// Extend ActionBase and override only needed methods
class TelegramNotifier extends ActionBase {
private bot: TelegramBot | null = null;

async init() {
super.init(); // Call parent for logging
this.bot = new TelegramBot(process.env.TELEGRAM_TOKEN);
await this.bot.connect();
}

async signal(event: IStrategyTickResult) {
super.signal(event); // Call parent for logging
if (event.action === 'opened') {
await this.bot.send(
`[${this.strategyName}/${this.frameName}] Signal opened: ${event.signal.side}`
);
}
}

async breakeven(event: BreakevenContract) {
super.breakeven(event); // Call parent for logging
await this.bot.send(
`[${this.strategyName}] Breakeven reached at ${event.currentPrice}`
);
}

async dispose() {
super.dispose(); // Call parent for logging
await this.bot?.disconnect();
this.bot = null;
}
}

// Register the action
addActionSchema({
actionName: "telegram-notifier",
handler: TelegramNotifier
});
// Redux state management example
class ReduxAction extends ActionBase {
constructor(
strategyName: StrategyName,
frameName: FrameName,
actionName: ActionName,
private store: Store
) {
super(strategyName, frameName, actionName);
}

signal(event: IStrategyTickResult) {
this.store.dispatch({
type: 'STRATEGY_SIGNAL',
payload: { event, strategyName: this.strategyName, frameName: this.frameName }
});
}

partialProfit(event: PartialProfitContract) {
this.store.dispatch({
type: 'PARTIAL_PROFIT',
payload: { event, strategyName: this.strategyName }
});
}
}

Implements

Constructors

  • Creates a new ActionBase instance.

    Parameters

    • strategyName: string

      Strategy identifier this action is attached to

    • frameName: string

      Timeframe identifier this action is attached to

    • actionName: string

      Action identifier

    • backtest: boolean

      If running in backtest

    Returns ActionBase

Properties

actionName: string
backtest: boolean
frameName: string
strategyName: string

Methods

  • Handles breakeven events when stop-loss is moved to entry price.

    Called once per signal when price moves far enough to cover fees and slippage. Breakeven threshold: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2 + CC_BREAKEVEN_THRESHOLD

    Triggered by: ActionCoreService.breakevenAvailable() via BreakevenConnectionService Source: breakevenSubject.next() in CREATE_COMMIT_BREAKEVEN_FN callback Frequency: Once per signal when threshold reached

    Default implementation: Logs breakeven event.

    Parameters

    • event: BreakevenContract

      Breakeven milestone data with signal info, current price, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    async breakevenAvailable(event: BreakevenContract) {
    await this.telegram.send(
    `[${event.strategyName}] Breakeven reached! ` +
    `Signal: ${event.data.side} @ ${event.currentPrice}`
    );
    }
  • Cleans up resources and subscriptions when action handler is disposed.

    Called once when strategy execution ends. Guaranteed to run exactly once via singleshot pattern.

    Override to:

    • Close database connections
    • Disconnect from external services
    • Flush buffers
    • Save state to disk
    • Unsubscribe from observables

    Default implementation: Logs dispose event.

    Parameters

    • Optionalsource: string

    Returns void | Promise<void>

    async dispose() {
    super.dispose(); // Keep parent logging
    await this.db?.disconnect();
    await this.telegram?.close();
    await this.cache?.quit();
    console.log('Action disposed successfully');
    }
  • Initializes the action handler.

    Called once after construction. Override to perform async initialization:

    • Establish database connections
    • Initialize API clients
    • Load configuration files
    • Open file handles or network sockets

    Default implementation: Logs initialization event.

    Parameters

    • Optionalsource: string

    Returns void | Promise<void>

    async init() {
    super.init(); // Keep parent logging
    this.db = await connectToDatabase();
    this.telegram = new TelegramBot(process.env.TOKEN);
    }
  • Handles partial loss level events (-10%, -20%, -30%, etc).

    Called once per loss level per signal (deduplicated). Use to track loss milestones and implement risk management actions.

    Triggered by: ActionCoreService.partialLossAvailable() via PartialConnectionService Source: partialLossSubject.next() in CREATE_COMMIT_LOSS_FN callback Frequency: Once per loss level per signal

    Default implementation: Logs partial loss event.

    Parameters

    • event: PartialLossContract

      Loss milestone data with signal info, level (-10, -20, -30...), price, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    async partialLossAvailable(event: PartialLossContract) {
    await this.telegram.send(
    `[${event.strategyName}] Loss ${event.level}% reached! ` +
    `Current price: ${event.currentPrice}`
    );
    // Optionally adjust risk management
    }
  • Handles partial profit level events (10%, 20%, 30%, etc).

    Called once per profit level per signal (deduplicated). Use to track profit milestones and adjust position management.

    Triggered by: ActionCoreService.partialProfitAvailable() via PartialConnectionService Source: partialProfitSubject.next() in CREATE_COMMIT_PROFIT_FN callback Frequency: Once per profit level per signal

    Default implementation: Logs partial profit event.

    Parameters

    • event: PartialProfitContract

      Profit milestone data with signal info, level (10, 20, 30...), price, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    async partialProfitAvailable(event: PartialProfitContract) {
    await this.telegram.send(
    `[${event.strategyName}] Profit ${event.level}% reached! ` +
    `Current price: ${event.currentPrice}`
    );
    // Optionally tighten stop-loss or take partial profit
    }
  • Handles active ping events during active pending signal monitoring.

    Called every minute while a pending signal is active (position open). Use to monitor active positions and track lifecycle.

    Triggered by: ActionCoreService.pingActive() via StrategyConnectionService Source: activePingSubject.next() in CREATE_COMMIT_ACTIVE_PING_FN callback Frequency: Every minute while pending signal is active

    Default implementation: Logs active ping event.

    Parameters

    • event: ActivePingContract

      Active pending signal monitoring data with symbol, strategy info, signal data, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    pingActive(event: ActivePingContract) {
    const holdTime = getTimestamp() - event.data.pendingAt;
    const holdMinutes = Math.floor(holdTime / 60000);
    console.log(`Active signal holding ${holdMinutes} minutes`);
    }
  • Handles scheduled ping events during scheduled signal monitoring.

    Called every minute while a scheduled signal is waiting for activation. Use to monitor pending signals and track wait time.

    Triggered by: ActionCoreService.pingScheduled() via StrategyConnectionService Source: schedulePingSubject.next() in CREATE_COMMIT_SCHEDULE_PING_FN callback Frequency: Every minute while scheduled signal is waiting

    Default implementation: Logs scheduled ping event.

    Parameters

    • event: SchedulePingContract

      Scheduled signal monitoring data with symbol, strategy info, signal data, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    pingScheduled(event: SchedulePingContract) {
    const waitTime = getTimestamp() - event.data.timestampScheduled;
    const waitMinutes = Math.floor(waitTime / 60000);
    console.log(`Scheduled signal waiting ${waitMinutes} minutes`);
    }
  • Handles risk rejection events when signals fail risk validation.

    Called only when signal is rejected (not emitted for allowed signals). Use to track rejected signals and analyze risk management effectiveness.

    Triggered by: ActionCoreService.riskRejection() via RiskConnectionService Source: riskSubject.next() in CREATE_COMMIT_REJECTION_FN callback Frequency: Only when signal fails risk validation

    Default implementation: Logs risk rejection event.

    Parameters

    • event: RiskContract

      Risk rejection data with symbol, pending signal, rejection reason, timestamp

    • Optionalsource: string

    Returns void | Promise<void>

    async riskRejection(event: RiskContract) {
    await this.telegram.send(
    `[${event.strategyName}] Signal rejected!\n` +
    `Reason: ${event.rejectionNote}\n` +
    `Active positions: ${event.activePositionCount}`
    );
    this.metrics.recordRejection(event.rejectionId);
    }
  • Handles signal events from all modes (live + backtest).

    Called every tick/candle when strategy is evaluated. Receives all signal states: idle, scheduled, opened, active, closed, cancelled.

    Triggered by: ActionCoreService.signal() via StrategyConnectionService Source: signalEmitter.next() in tick() and backtest() methods Frequency: Every tick/candle

    Default implementation: Logs signal event.

    Parameters

    • event: IStrategyTickResult

      Signal state result with action, state, signal data, and context

    • Optionalsource: string

    Returns void | Promise<void>

    signal(event: IStrategyTickResult) {
    if (event.action === 'opened') {
    console.log(`Signal opened: ${event.signal.side} at ${event.signal.priceOpen}`);
    }
    if (event.action === 'closed') {
    console.log(`Signal closed: PNL ${event.signal.revenue}%`);
    }
    }
  • Handles signal events from backtest only.

    Called every candle in backtest mode. Use for actions specific to backtesting (e.g., collecting test metrics).

    Triggered by: ActionCoreService.signalBacktest() via StrategyConnectionService Source: signalBacktestEmitter.next() in tick() and backtest() methods when backtest=true Frequency: Every candle in backtest mode

    Default implementation: Logs backtest signal event.

    Parameters

    Returns void | Promise<void>

    signalBacktest(event: IStrategyTickResult) {
    if (event.action === 'closed') {
    this.backtestMetrics.recordTrade(event.signal);
    }
    }
  • Handles signal events from live trading only.

    Called every tick in live mode. Use for actions that should only run in production (e.g., sending real notifications).

    Triggered by: ActionCoreService.signalLive() via StrategyConnectionService Source: signalLiveEmitter.next() in tick() and backtest() methods when backtest=false Frequency: Every tick in live mode

    Default implementation: Logs live signal event.

    Parameters

    Returns void | Promise<void>

    async signalLive(event: IStrategyTickResult) {
    if (event.action === 'opened') {
    await this.telegram.send('Real trade opened!');
    await this.placeRealOrder(event.signal);
    }
    }