This page explains how profit and loss (PnL) are calculated when signals close in the backtest-kit framework. It covers fee application, slippage simulation, price adjustments, and the final percentage calculation returned in closed signal events.
For information about signal lifecycle states and when signals close, see Signal States. For validation rules that prevent unprofitable signals, see Signal Generation and Validation.
PnL calculation occurs when a signal transitions to the closed state (action: "closed"). The framework applies realistic trading costs to both entry and exit prices before computing the profit or loss percentage. This ensures backtests and live trading reflect real-world execution conditions.
Key Features:
IStrategyPnL with adjusted pricesThe framework applies two types of transaction costs to simulate real market conditions:
Configuration: CC_PERCENT_FEE (default: 0.1%)
Fees are charged twice:
priceOpenpriceCloseTotal fee impact: 0.2% (0.1% × 2 transactions)
Configuration: CC_PERCENT_SLIPPAGE (default: 0.1%)
Slippage simulates market impact and order book depth. Applied twice:
Total slippage impact: ~0.2% (0.1% × 2 transactions)
Total cost per round-trip trade: ~0.4% (0.2% fees + 0.2% slippage)
This cost structure explains why CC_MIN_TAKEPROFIT_DISTANCE_PERCENT defaults to 0.5% - positions must move at least 0.5% favorably to cover costs and generate minimum profit.
Price adjustments differ for long and short positions to reflect directional cost impact:
For long positions (buy low, sell high):
// Entry: Buy at higher price (unfavorable)
adjustedPriceOpen = priceOpen × (1 + CC_PERCENT_SLIPPAGE/100 + CC_PERCENT_FEE/100)
// Exit: Sell at lower price (unfavorable)
adjustedPriceClose = priceClose × (1 - CC_PERCENT_SLIPPAGE/100 - CC_PERCENT_FEE/100)
Example (default 0.1% fees, 0.1% slippage):
For short positions (sell high, buy low):
// Entry: Sell at lower price (unfavorable)
adjustedPriceOpen = priceOpen × (1 - CC_PERCENT_SLIPPAGE/100 - CC_PERCENT_FEE/100)
// Exit: Buy at higher price (unfavorable)
adjustedPriceClose = priceClose × (1 + CC_PERCENT_SLIPPAGE/100 + CC_PERCENT_FEE/100)
Example (default 0.1% fees, 0.1% slippage):
pnlPercentage = ((adjustedPriceClose - adjustedPriceOpen) / adjustedPriceOpen) × 100
Example (continuing from earlier):
Raw profit before fees/slippage: ((51,000 - 50,000) / 50,000) × 100 = 2.0%
Net profit after fees/slippage: 1.59%
Cost impact: 0.41% (roughly 0.4% as expected)
pnlPercentage = ((adjustedPriceOpen - adjustedPriceClose) / adjustedPriceOpen) × 100
Note the reversed order: short positions profit when price falls (priceOpen > priceClose).
Example (continuing from earlier):
Raw profit before fees/slippage: ((50,000 - 49,000) / 50,000) × 100 = 2.0%
Net profit after fees/slippage: 1.61%
Cost impact: 0.39% (roughly 0.4% as expected)
The calculated PnL is returned as part of IStrategyTickResultClosed in the pnl field:
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;
}
interface IStrategyTickResultClosed {
action: "closed";
signal: ISignalRow;
currentPrice: number;
closeReason: "time_expired" | "take_profit" | "stop_loss";
closeTimestamp: number;
pnl: IStrategyPnL; // ← PnL calculation result
strategyName: StrategyName;
exchangeName: ExchangeName;
symbol: string;
}
Access Pattern:
listenSignal((result) => {
if (result.action === "closed") {
console.log("PnL:", result.pnl.pnlPercentage.toFixed(2) + "%");
console.log("Adjusted entry:", result.pnl.priceOpen);
console.log("Adjusted exit:", result.pnl.priceClose);
}
});
Default: 0.1 (0.1%)
Fee percentage charged per transaction. Applied twice (entry and exit) for total 0.2% fee cost.
Configuration:
import { setConfig } from "backtest-kit";
setConfig({
CC_PERCENT_FEE: 0.075 // 0.075% per transaction (0.15% total)
});
Default: 0.1 (0.1%)
Slippage percentage simulating market impact and order book depth. Applied twice (entry and exit) for total ~0.2% slippage cost.
Configuration:
import { setConfig } from "backtest-kit";
setConfig({
CC_PERCENT_SLIPPAGE: 0.05 // 0.05% per transaction (0.1% total)
});
setConfig({
CC_PERCENT_FEE: 0.05, // 0.05% per transaction
CC_PERCENT_SLIPPAGE: 0.05, // 0.05% per transaction
// Total cost: ~0.2% (0.1% fees + 0.1% slippage)
});
Impact on minimum profit requirements: When reducing fees/slippage, you can also reduce CC_MIN_TAKEPROFIT_DISTANCE_PERCENT proportionally.
Default: 0.5 (0.5%)
Minimum distance between priceTakeProfit and priceOpen to ensure profitable trades after fees and slippage.
Rationale (from configuration comments):
Calculation:
- Slippage effect: ~0.2% (0.1% × 2 transactions)
- Fees: 0.2% (0.1% × 2 transactions)
- Minimum profit buffer: 0.1%
- Total: 0.5%
The framework validates take profit distance during signal creation:
Long Position:
const tpDistancePercent = ((priceTakeProfit - priceOpen) / priceOpen) × 100;
if (tpDistancePercent < CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
throw new Error("TakeProfit too close to priceOpen");
}
Short Position:
const tpDistancePercent = ((priceOpen - priceTakeProfit) / priceOpen) × 100;
if (tpDistancePercent < CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
throw new Error("TakeProfit too close to priceOpen");
}
This validation prevents strategies from generating signals that would be unprofitable even if take profit is hit.
PnL calculation occurs at the final stage of the signal lifecycle:
Close Reasons and PnL:
| Close Reason | priceClose Value |
PnL Sign (typical) |
|---|---|---|
take_profit |
signal.priceTakeProfit |
Positive |
stop_loss |
signal.priceStopLoss |
Negative |
time_expired |
Current VWAP price | Variable |
Note: Even take profit hits can result in negative PnL if the distance to TP was too small to cover fees/slippage. This is why CC_MIN_TAKEPROFIT_DISTANCE_PERCENT validation exists.
PnL data is aggregated in backtest markdown reports:
| Signal ID | Position | PnL % | Entry Price | Exit Price | Close Reason |
|-----------|----------|-------|-------------|------------|--------------|
| abc123 | long | +1.59 | $50,100 | $50,898 | take_profit |
| def456 | short | -2.15 | $49,900 | $51,000 | stop_loss |
Statistics calculated:
Live reports include both closed and active signals:
| Signal ID | State | PnL % | Entry Price | Current Price |
|-----------|--------|---------|-------------|---------------|
| ghi789 | active | +0.85 | $50,100 | $50,525 |
| jkl012 | closed | +1.59 | $50,100 | $50,898 |
Real-time monitoring: Active signals show unrealized PnL based on current price and estimated costs.
Closed signals with PnL data are emitted through multiple event channels:
import { listenSignal, listenSignalBacktest, listenSignalLive } from "backtest-kit";
// Listen to all closed signals (backtest + live)
listenSignal((result) => {
if (result.action === "closed") {
const pnl = result.pnl;
console.log(`PnL: ${pnl.pnlPercentage.toFixed(2)}%`);
console.log(`Close reason: ${result.closeReason}`);
console.log(`Adjusted entry: $${pnl.priceOpen.toFixed(2)}`);
console.log(`Adjusted exit: $${pnl.priceClose.toFixed(2)}`);
}
});
// Listen only to backtest closed signals
listenSignalBacktest((result) => {
if (result.action === "closed") {
// Process backtest PnL data
}
});
// Listen only to live trading closed signals
listenSignalLive((result) => {
if (result.action === "closed") {
// Process live trading PnL data
}
});
Event Channels:
signalEmitter: All signals (backtest + live)signalBacktestEmitter: Backtest-only signalssignalLiveEmitter: Live-only signals| Price Point | Original | Fee Impact | Slippage Impact | Adjusted | Net Change |
|---|---|---|---|---|---|
| Entry (Open) | $50,000 | +$50 (0.1%) | +$50 (0.1%) | $50,100 | +$100 (+0.2%) |
| Exit (Close) | $51,000 | -$51 (0.1%) | -$51 (0.1%) | $50,898 | -$102 (-0.2%) |
Gross profit: $1,000 (2.0%)
Transaction costs: ~$202 (0.4%)
Net profit: $798 (1.59%)
| Price Point | Original | Fee Impact | Slippage Impact | Adjusted | Net Change |
|---|---|---|---|---|---|
| Entry (Open) | $50,000 | -$50 (0.1%) | -$50 (0.1%) | $49,900 | -$100 (-0.2%) |
| Exit (Close) | $49,000 | +$49 (0.1%) | +$49 (0.1%) | $49,098 | +$98 (+0.2%) |
Gross profit: $1,000 (2.0%)
Transaction costs: ~$198 (0.4%)
Net profit: $802 (1.61%)
The PnL calculation system in backtest-kit provides realistic profit/loss simulation by:
IStrategyPnL interfaceKey formulas:
((adjustedClose - adjustedOpen) / adjustedOpen) × 100((adjustedOpen - adjustedClose) / adjustedOpen) × 100This realistic cost modeling ensures backtest results closely match live trading performance, accounting for exchange fees, market impact, and order execution slippage.