This page documents the heatmap analytics system, which aggregates trading performance across multiple symbols for portfolio-wide analysis. The system tracks per-symbol and portfolio-level metrics for a single strategy executing across multiple trading pairs.
For individual symbol-strategy pair statistics in backtest mode, see Backtest Reports. For live trading reports, see Live Trading Reports. For multi-strategy comparison, see Walker Mode Reports.
The heatmap analytics system provides portfolio-level performance visualization by aggregating closed signal data across all symbols that a single strategy trades. It calculates per-symbol statistics (PNL, Sharpe ratio, max drawdown, win rate) and portfolio-wide aggregated metrics. The system generates markdown reports with tabular breakdowns showing which symbols perform best and which are underperforming.
Key characteristics:
signalEmitter to track closed signals as they occurService Integration: HeatMarkdownService is instantiated via the dependency injection system and subscribes to signalEmitter on first use. Unlike BacktestMarkdownService and LiveMarkdownService which use symbol:strategyName as storage keys, HeatMarkdownService uses only strategyName as the key because it aggregates across all symbols.
Storage Strategy: Each strategy gets exactly one HeatmapStorage instance via memoization src/lib/services/markdown/HeatMarkdownService.ts:442-445. Within each storage, a Map<string, IStrategyTickResultClosed[]> maintains per-symbol closed signal arrays src/lib/services/markdown/HeatMarkdownService.ts:84.
Each symbol tracked by a strategy has statistics calculated into an IHeatmapRow structure:
| Field | Type | Description |
|---|---|---|
symbol |
string |
Trading pair identifier |
totalPnl |
number | null |
Sum of all PNL percentages |
sharpeRatio |
number | null |
Risk-adjusted return metric |
maxDrawdown |
number | null |
Maximum cumulative loss from peak |
totalTrades |
number |
Count of closed signals |
winCount |
number |
Count of profitable trades |
lossCount |
number |
Count of losing trades |
winRate |
number | null |
Percentage of winning trades |
avgPnl |
number | null |
Mean PNL per trade |
stdDev |
number | null |
Standard deviation of returns |
profitFactor |
number | null |
Total wins / total losses ratio |
avgWin |
number | null |
Average winning trade PNL |
avgLoss |
number | null |
Average losing trade PNL |
maxWinStreak |
number |
Longest consecutive wins |
maxLossStreak |
number |
Longest consecutive losses |
expectancy |
number | null |
Expected PNL per trade |
The HeatmapStatisticsModel contains aggregated portfolio-wide data:
{
symbols: IHeatmapRow[], // Sorted by Sharpe ratio desc
totalSymbols: number, // Count of tracked symbols
portfolioTotalPnl: number | null, // Sum across all symbols
portfolioSharpeRatio: number | null, // Trade-weighted average
portfolioTotalTrades: number // Total across all symbols
}
Sharpe Ratio Weighting: The portfolio Sharpe ratio is calculated as a trade-weighted average rather than a simple mean src/lib/services/markdown/HeatMarkdownService.ts:309-317:
portfolioSharpeRatio = Σ(sharpeRatio_i × totalTrades_i) / portfolioTotalTrades
Event Filtering: The tick method only processes signals where action === "closed" src/lib/services/markdown/HeatMarkdownService.ts:460-462. Idle, opened, active, and scheduled signals are ignored.
Deque Behavior: New signals are added to the front with unshift, and old signals are removed from the back with pop when exceeding MAX_EVENTS=250 src/lib/services/markdown/HeatMarkdownService.ts:99-104. This maintains a rolling window of the most recent 250 closed signals per symbol.
The calculateSymbolStats method src/lib/services/markdown/HeatMarkdownService.ts:115-271 computes all metrics for a single symbol. Key calculations:
totalTrades = signals.length
winCount = signals where pnl > 0
lossCount = signals where pnl < 0
winRate = (winCount / totalTrades) × 100
totalPnl = Σ(signal.pnl.pnlPercentage)
avgPnl = totalPnl / totalTrades
variance = Σ((pnl_i - avgPnl)²) / totalTrades
stdDev = √variance
sharpeRatio = avgPnl / stdDev (if stdDev > 0)
Calculated iteratively by tracking cumulative PNL and measuring distance from peak src/lib/services/markdown/HeatMarkdownService.ts:159-178:
for each signal:
peak += signal.pnl
if peak > 0:
currentDrawdown = 0
else:
currentDrawdown = |peak|
maxDrawdown = max(maxDrawdown, currentDrawdown)
sumWins = Σ(pnl where pnl > 0)
sumLosses = |Σ(pnl where pnl < 0)|
profitFactor = sumWins / sumLosses (if sumLosses > 0)
expectancy = (winRate/100) × avgWin + ((100-winRate)/100) × avgLoss
Safe Math: All calculated values are checked with isUnsafe() before returning. If a value is NaN, Infinity, or not a number, it's set to null src/lib/services/markdown/HeatMarkdownService.ts:242-251.
The getData method src/lib/services/markdown/HeatMarkdownService.ts:278-330 performs three operations:
calculateSymbolStats for each symbol in symbolData MapReturns HeatmapStatisticsModel with all computed statistics.
public getData = async (
strategyName: StrategyName
): Promise<HeatmapStatisticsModel>
Usage:
const service = new HeatMarkdownService();
const stats = await service.getData("my-strategy");
console.log(`Total symbols: ${stats.totalSymbols}`);
console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
stats.symbols.forEach(row => {
console.log(`${row.symbol}: ${row.totalPnl}% (${row.totalTrades} trades)`);
});
Generates markdown-formatted report with portfolio metrics table.
public getReport = async (
strategyName: StrategyName,
columns: Columns[] = COLUMN_CONFIG.heat_columns
): Promise<string>
Report Structure:
# Portfolio Heatmap: {strategyName}Example output src/lib/services/markdown/HeatMarkdownService.ts:370-376:
# Portfolio Heatmap: my-strategy
**Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
| Symbol | Total PNL | Sharpe | Max DD | Trades | Win Rate | ... |
|--------|-----------|--------|--------|--------|----------|-----|
| BTCUSDT | +15.5% | 2.10 | -2.5% | 45 | 68.9% | ... |
| ETHUSDT | +12.3% | 1.85 | -3.1% | 38 | 63.2% | ... |
Writes markdown report to disk.
public dump = async (
strategyName: StrategyName,
path = "./dump/heatmap",
columns: Columns[] = COLUMN_CONFIG.heat_columns
): Promise<void>
File naming: {strategyName}.md in the specified directory.
Example:
// Saves to ./dump/heatmap/my-strategy.md
await service.dump("my-strategy");
// Custom path: ./reports/my-strategy.md
await service.dump("my-strategy", "./reports");
Clears accumulated data from storage.
public clear = async (strategyName?: StrategyName): Promise<void>
Display columns are defined via ColumnModel<IHeatmapRow> interface src/model/Column.model.ts:26-38. Default columns are provided in COLUMN_CONFIG.heat_columns.
Column Interface:
interface ColumnModel<T> {
key: string; // Unique identifier
label: string; // Header display text
format: (data: T, index: number) => string | Promise<string>;
isVisible: () => boolean | Promise<boolean>;
}
Example column definitions:
const symbolColumn: Columns = {
key: "symbol",
label: "Symbol",
format: (row) => row.symbol,
isVisible: () => true
};
const pnlColumn: Columns = {
key: "totalPnl",
label: "Total PNL %",
format: (row) => row.totalPnl !== null
? row.totalPnl.toFixed(2) + '%'
: 'N/A',
isVisible: () => true
};
Custom columns: Users can pass custom Columns[] arrays to getReport and dump methods to override default display configuration.
Key Differences:
| Service | Storage Key | Scope | Event Source |
|---|---|---|---|
BacktestMarkdownService |
symbol:strategyName |
Per symbol-strategy pair | signalBacktestEmitter |
LiveMarkdownService |
symbol:strategyName |
Per symbol-strategy pair | signalLiveEmitter |
HeatMarkdownService |
strategyName |
All symbols for strategy | signalEmitter (both) |
Use Cases:
The service uses lazy initialization with singleshot pattern src/lib/services/markdown/HeatMarkdownService.ts:602-605:
protected init = singleshot(async () => {
this.loggerService.log("heatMarkdownService init");
signalEmitter.subscribe(this.tick);
});
Lifecycle:
init()init() subscribes to signalEmitter exactly oncetick() method processes all subsequent closed signalsclear() can reset storage without unsubscribingAutomatic initialization: Users do not need to manually call init(). The service begins accumulating data automatically when first accessed.