Purpose: This document describes the partial profit/loss tracking system that monitors trading signals and emits events when they reach percentage-based profit or loss milestones (10%, 20%, 30%, etc.). This enables tracking partial take-profit and stop-loss behavior, generating detailed reports, and monitoring strategy performance at granular levels.
For information about overall reporting infrastructure, see Reporting and Analytics. For risk management and portfolio constraints, see Risk Management. For signal lifecycle states, see Signal Lifecycle.
The partial tracking system monitors active trading signals and emits events when they reach predefined profit or loss levels. Each level (10%, 20%, 30%...100%) is emitted exactly once per signal using Set-based deduplication. The system persists state to disk for crash recovery in live trading mode.
| Component | File | Purpose |
|---|---|---|
ClientPartial |
src/client/ClientPartial.ts:1-478 | Core implementation tracking profit/loss levels per signal |
PartialConnectionService |
src/lib/services/connection/PartialConnectionService.ts:1-267 | Factory creating memoized ClientPartial instances |
PartialGlobalService |
src/lib/services/global/PartialGlobalService.ts:1-205 | Global service delegating to connection layer |
PartialMarkdownService |
src/lib/services/markdown/PartialMarkdownService.ts:1-478 | Accumulates events and generates markdown reports |
PersistPartialAdapter |
src/classes/Persist.ts:1-600 | Atomic persistence for partial state |
The system tracks profit and loss milestones at 10-percentage-point intervals from 10% to 100%. Each level is emitted exactly once per signal to prevent duplicate notifications.
Profit Levels (positive price movement):
const PROFIT_LEVELS: PartialLevel[] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
Loss Levels (negative price movement):
const LOSS_LEVELS: PartialLevel[] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
Note: Loss levels are stored as absolute values (10, 20, 30) but represent negative percentages (-10%, -20%, -30%).
The IPartialState interface uses Sets for O(1) deduplication and lookup:
interface IPartialState {
profitLevels: Set<PartialLevel>; // Reached profit levels
lossLevels: Set<PartialLevel>; // Reached loss levels
}
State is stored in a Map<signalId, IPartialState> within each ClientPartial instance. For persistence, Sets are converted to arrays in IPartialData format.
ClientPartial is the core implementation responsible for tracking profit/loss levels for a single signal. Instances are created per signal ID and memoized by PartialConnectionService.
The initialization uses a sentinel value NEED_FETCH to ensure waitForInit() is called before operations. The singleshot pattern from functools-kit guarantees initialization happens exactly once per symbol-strategy combination.
The HANDLE_PROFIT_FN and HANDLE_LOSS_FN functions iterate through level arrays and check for newly reached milestones:
Profit Detection src/client/ClientPartial.ts:44-105:
PROFIT_LEVELS arrayrevenuePercent >= level AND level not in state.profitLevels Setparams.onProfit(), mark for persistence_persistState() if any changes occurredLoss Detection src/client/ClientPartial.ts:122-184:
lossPercent to absolute value: absLoss = Math.abs(lossPercent)LOSS_LEVELS arrayabsLoss >= level AND level not in state.lossLevels Setparams.onLoss(), mark for persistence_persistState() if any changes occurredEmitted when a signal reaches a new profit level milestone:
| Field | Type | Description |
|---|---|---|
symbol |
string |
Trading pair (e.g., "BTCUSDT") |
strategyName |
string |
Strategy that generated the signal |
exchangeName |
string |
Exchange where signal is executing |
data |
ISignalRow |
Complete signal data (id, position, prices) |
currentPrice |
number |
Market price when level reached |
level |
PartialLevel |
Profit level reached (10, 20...100) |
backtest |
boolean |
True if backtest mode, false if live |
timestamp |
number |
Event time (ms since Unix epoch) |
Emitted when a signal reaches a new loss level milestone:
| Field | Type | Description |
|---|---|---|
symbol |
string |
Trading pair (e.g., "BTCUSDT") |
strategyName |
string |
Strategy that generated the signal |
exchangeName |
string |
Exchange where signal is executing |
data |
ISignalRow |
Complete signal data (id, position, prices) |
currentPrice |
number |
Market price when level reached |
level |
PartialLevel |
Loss level reached (10, 20...100 as absolute) |
backtest |
boolean |
True if backtest mode, false if live |
timestamp |
number |
Event time (ms since Unix epoch) |
Note: level is stored as a positive number but represents negative loss. level=20 means -20% loss.
Partial state is persisted to disk for crash recovery in live trading mode. Backtest mode skips persistence for performance.
Write Path src/client/ClientPartial.ts:349-367:
Map<signalId, IPartialState> to Record<signalId, IPartialData>Array.from(state.profitLevels), Array.from(state.lossLevels)PersistPartialAdapter.writePartialData(partialData, symbol, strategyName)Read Path src/client/ClientPartial.ts:199-235:
PersistPartialAdapter.readPartialData(symbol, strategyName)Record<signalId, IPartialData>new Set(data.profitLevels), new Set(data.lossLevels)_states Map with restored statePartial state files are stored at:
./dump/data/partial/{strategy}/{symbol}.json
Example: Strategy "my-strategy" tracking BTCUSDT would create:
./dump/data/partial/my-strategy/BTCUSDT.json
The markdown service subscribes to partial profit/loss events and generates reports showing milestone progression.
The ReportStorage class src/lib/services/markdown/PartialMarkdownService.ts:60-236 maintains a bounded queue of partial events:
Methods:
addProfitEvent(data, currentPrice, level, backtest, timestamp): Add profit event to queueaddLossEvent(data, currentPrice, level, backtest, timestamp): Add loss event to queuegetData(): Calculate statistics (totalProfit, totalLoss counts)getReport(symbol, strategyName, columns): Generate markdown tabledump(symbol, strategyName, path, columns): Save report to diskQueue Management: Uses unshift() to add events at beginning, pop() to remove oldest when exceeding MAX_EVENTS=250.
Generated markdown reports include:
# Partial Profit/Loss Report: {symbol}:{strategyName}ColumnModel<PartialEvent> columnsDefault Columns src/config/columns.ts:
ClientStrategy calls partial tracking during signal monitoring src/client/ClientStrategy.ts:
Profit Tracking:
if (revenuePercent > 0) {
await this.params.partial.profit(
symbol,
signal,
currentPrice,
revenuePercent,
backtest,
when
);
}
Loss Tracking:
if (revenuePercent < 0) {
await this.params.partial.loss(
symbol,
signal,
currentPrice,
revenuePercent, // Negative value
backtest,
when
);
}
Cleanup on Close:
await this.params.partial.clear(symbol, signal, closePrice, backtest);
The hierarchy follows the standard service layer pattern:
Partial reports use configurable ColumnModel<PartialEvent> interfaces. The default configuration is defined in COLUMN_CONFIG.partial_columns src/config/columns.ts.
Custom columns can be provided to override default formatting:
const customColumns: ColumnModel<PartialEvent>[] = [
{
key: "timestamp",
label: "Time",
format: (event) => new Date(event.timestamp).toISOString(),
isVisible: () => true
},
{
key: "level",
label: "Level",
format: (event) => `${event.level}%`,
isVisible: () => true
}
];
await partialMarkdownService.dump(
"BTCUSDT",
"my-strategy",
"./reports",
customColumns
);
Default persistence path: ./dump/data/partial/{strategy}/{symbol}.json
The base directory can be customized when creating PersistPartialAdapter instances, though this is typically managed internally by the framework.
Listen to all profit events:
import { listenPartialProfit } from "backtest-kit";
listenPartialProfit((event) => {
console.log(`Signal ${event.data.id} reached ${event.level}% profit`);
console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
if (event.level >= 50 && !event.backtest) {
console.log("MAJOR PROFIT: Consider partial exit");
}
});
Wait for specific loss level:
import { listenPartialLossOnce } from "backtest-kit";
listenPartialLossOnce(
(event) => event.level === 30,
(event) => {
console.warn(`30% loss reached on ${event.symbol}`);
// Trigger alerts or intervention logic
}
);
Programmatic report access:
import { PartialMarkdownService } from "backtest-kit";
const service = new PartialMarkdownService();
// Get statistics
const stats = await service.getData("BTCUSDT", "my-strategy");
console.log(`Total events: ${stats.totalEvents}`);
console.log(`Profit events: ${stats.totalProfit}`);
console.log(`Loss events: ${stats.totalLoss}`);
// Generate markdown
const markdown = await service.getReport("BTCUSDT", "my-strategy");
console.log(markdown);
// Save to disk
await service.dump("BTCUSDT", "my-strategy", "./reports");
ClientPartial instances are memoized per signal ID and backtest mode src/lib/services/connection/PartialConnectionService.ts:132-143ReportStorage limits events to MAX_EVENTS=250 per symbol-strategy pair src/lib/services/markdown/PartialMarkdownService.ts:54clear() removes signal state and memoized instances when signals close src/lib/services/connection/PartialConnectionService.ts:246-263Backtest mode skips all disk I/O operations:
waitForInit() src/client/ClientPartial.ts:214-218_persistState() src/client/ClientPartial.ts:350-352This prevents unnecessary filesystem operations during historical simulation where crash recovery is not needed.
Using Set<PartialLevel> for state provides:
The system enforces several validation rules:
Pre-initialization check src/client/ClientPartial.ts:53-56:
if (self._states === NEED_FETCH) {
throw new Error(
"ClientPartial not initialized. Call waitForInit() before using."
);
}
Signal ID mismatch src/client/ClientPartial.ts:59-62:
if (data.id !== self.params.signalId) {
throw new Error(
`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`
);
}
Strategy validation: PartialGlobalService validates strategy and risk names before delegation src/lib/services/global/PartialGlobalService.ts:83-95
PersistBase implements retry logic and auto-cleanup: