backtest-kit 1.4.10 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -7,6 +7,7 @@ var fs = require('fs/promises');
7
7
  var path = require('path');
8
8
  var crypto = require('crypto');
9
9
  var os = require('os');
10
+ var fs$1 = require('fs');
10
11
 
11
12
  const GLOBAL_CONFIG = {
12
13
  /**
@@ -162,6 +163,7 @@ const markdownServices$1 = {
162
163
  walkerMarkdownService: Symbol('walkerMarkdownService'),
163
164
  heatMarkdownService: Symbol('heatMarkdownService'),
164
165
  partialMarkdownService: Symbol('partialMarkdownService'),
166
+ outlineMarkdownService: Symbol('outlineMarkdownService'),
165
167
  };
166
168
  const validationServices$1 = {
167
169
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -7262,6 +7264,118 @@ function formatMetric(value) {
7262
7264
  }
7263
7265
  return value.toFixed(2);
7264
7266
  }
7267
+ /**
7268
+ * Creates strategy comparison columns based on metric name.
7269
+ * Dynamically builds column configuration with metric-specific header.
7270
+ *
7271
+ * @param metric - Metric being optimized
7272
+ * @returns Array of column configurations for strategy comparison table
7273
+ */
7274
+ function createStrategyColumns(metric) {
7275
+ return [
7276
+ {
7277
+ key: "rank",
7278
+ label: "Rank",
7279
+ format: (data, index) => `${index + 1}`,
7280
+ },
7281
+ {
7282
+ key: "strategy",
7283
+ label: "Strategy",
7284
+ format: (data) => data.strategyName,
7285
+ },
7286
+ {
7287
+ key: "metric",
7288
+ label: metric,
7289
+ format: (data) => formatMetric(data.metricValue),
7290
+ },
7291
+ {
7292
+ key: "totalSignals",
7293
+ label: "Total Signals",
7294
+ format: (data) => `${data.stats.totalSignals}`,
7295
+ },
7296
+ {
7297
+ key: "winRate",
7298
+ label: "Win Rate",
7299
+ format: (data) => data.stats.winRate !== null
7300
+ ? `${data.stats.winRate.toFixed(2)}%`
7301
+ : "N/A",
7302
+ },
7303
+ {
7304
+ key: "avgPnl",
7305
+ label: "Avg PNL",
7306
+ format: (data) => data.stats.avgPnl !== null
7307
+ ? `${data.stats.avgPnl > 0 ? "+" : ""}${data.stats.avgPnl.toFixed(2)}%`
7308
+ : "N/A",
7309
+ },
7310
+ {
7311
+ key: "totalPnl",
7312
+ label: "Total PNL",
7313
+ format: (data) => data.stats.totalPnl !== null
7314
+ ? `${data.stats.totalPnl > 0 ? "+" : ""}${data.stats.totalPnl.toFixed(2)}%`
7315
+ : "N/A",
7316
+ },
7317
+ {
7318
+ key: "sharpeRatio",
7319
+ label: "Sharpe Ratio",
7320
+ format: (data) => data.stats.sharpeRatio !== null
7321
+ ? `${data.stats.sharpeRatio.toFixed(3)}`
7322
+ : "N/A",
7323
+ },
7324
+ {
7325
+ key: "stdDev",
7326
+ label: "Std Dev",
7327
+ format: (data) => data.stats.stdDev !== null
7328
+ ? `${data.stats.stdDev.toFixed(3)}%`
7329
+ : "N/A",
7330
+ },
7331
+ ];
7332
+ }
7333
+ /**
7334
+ * Column configuration for PNL table.
7335
+ * Defines all columns for displaying closed signals across strategies.
7336
+ */
7337
+ const pnlColumns = [
7338
+ {
7339
+ key: "strategy",
7340
+ label: "Strategy",
7341
+ format: (data) => data.strategyName,
7342
+ },
7343
+ {
7344
+ key: "signalId",
7345
+ label: "Signal ID",
7346
+ format: (data) => data.signalId,
7347
+ },
7348
+ {
7349
+ key: "symbol",
7350
+ label: "Symbol",
7351
+ format: (data) => data.symbol,
7352
+ },
7353
+ {
7354
+ key: "position",
7355
+ label: "Position",
7356
+ format: (data) => data.position.toUpperCase(),
7357
+ },
7358
+ {
7359
+ key: "pnl",
7360
+ label: "PNL (net)",
7361
+ format: (data) => `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`,
7362
+ },
7363
+ {
7364
+ key: "closeReason",
7365
+ label: "Close Reason",
7366
+ format: (data) => data.closeReason,
7367
+ },
7368
+ {
7369
+ key: "openTime",
7370
+ label: "Open Time",
7371
+ format: (data) => new Date(data.openTime).toISOString(),
7372
+ },
7373
+ {
7374
+ key: "closeTime",
7375
+ label: "Close Time",
7376
+ format: (data) => new Date(data.closeTime).toISOString(),
7377
+ },
7378
+ ];
7265
7379
  /**
7266
7380
  * Storage class for accumulating walker results.
7267
7381
  * Maintains a list of all strategy results and provides methods to generate reports.
@@ -7274,9 +7388,12 @@ let ReportStorage$1 = class ReportStorage {
7274
7388
  this._bestStats = null;
7275
7389
  this._bestMetric = null;
7276
7390
  this._bestStrategy = null;
7391
+ /** All strategy results for comparison table */
7392
+ this._strategyResults = [];
7277
7393
  }
7278
7394
  /**
7279
7395
  * Adds a strategy result to the storage.
7396
+ * Updates best strategy tracking and accumulates result for comparison table.
7280
7397
  *
7281
7398
  * @param data - Walker contract with strategy result
7282
7399
  */
@@ -7290,6 +7407,12 @@ let ReportStorage$1 = class ReportStorage {
7290
7407
  if (data.strategyName === data.bestStrategy) {
7291
7408
  this._bestStats = data.stats;
7292
7409
  }
7410
+ // Add strategy result to comparison list
7411
+ this._strategyResults.push({
7412
+ strategyName: data.strategyName,
7413
+ stats: data.stats,
7414
+ metricValue: data.metricValue,
7415
+ });
7293
7416
  }
7294
7417
  /**
7295
7418
  * Calculates walker results from strategy results.
@@ -7314,10 +7437,79 @@ let ReportStorage$1 = class ReportStorage {
7314
7437
  bestStrategy: this._bestStrategy,
7315
7438
  bestMetric: this._bestMetric,
7316
7439
  bestStats: this._bestStats,
7440
+ strategyResults: this._strategyResults,
7317
7441
  };
7318
7442
  }
7443
+ /**
7444
+ * Generates comparison table for top N strategies (View).
7445
+ * Sorts strategies by metric value and formats as markdown table.
7446
+ *
7447
+ * @param metric - Metric being optimized
7448
+ * @param topN - Number of top strategies to include (default: 10)
7449
+ * @returns Markdown formatted comparison table
7450
+ */
7451
+ getComparisonTable(metric, topN = 10) {
7452
+ if (this._strategyResults.length === 0) {
7453
+ return "No strategy results available.";
7454
+ }
7455
+ // Sort strategies by metric value (descending)
7456
+ const sortedResults = [...this._strategyResults].sort((a, b) => {
7457
+ const aValue = a.metricValue ?? -Infinity;
7458
+ const bValue = b.metricValue ?? -Infinity;
7459
+ return bValue - aValue;
7460
+ });
7461
+ // Take top N strategies
7462
+ const topStrategies = sortedResults.slice(0, topN);
7463
+ // Get columns configuration
7464
+ const columns = createStrategyColumns(metric);
7465
+ // Build table header
7466
+ const header = columns.map((col) => col.label);
7467
+ const separator = columns.map(() => "---");
7468
+ // Build table rows
7469
+ const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7470
+ const tableData = [header, separator, ...rows];
7471
+ return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7472
+ }
7473
+ /**
7474
+ * Generates PNL table showing all closed signals across all strategies (View).
7475
+ * Collects all signals from all strategies and formats as markdown table.
7476
+ *
7477
+ * @returns Markdown formatted PNL table
7478
+ */
7479
+ getPnlTable() {
7480
+ if (this._strategyResults.length === 0) {
7481
+ return "No strategy results available.";
7482
+ }
7483
+ // Collect all closed signals from all strategies
7484
+ const allSignals = [];
7485
+ for (const result of this._strategyResults) {
7486
+ for (const signal of result.stats.signalList) {
7487
+ allSignals.push({
7488
+ strategyName: result.strategyName,
7489
+ signalId: signal.signal.id,
7490
+ symbol: signal.signal.symbol,
7491
+ position: signal.signal.position,
7492
+ pnl: signal.pnl.pnlPercentage,
7493
+ closeReason: signal.closeReason,
7494
+ openTime: signal.signal.pendingAt,
7495
+ closeTime: signal.closeTimestamp,
7496
+ });
7497
+ }
7498
+ }
7499
+ if (allSignals.length === 0) {
7500
+ return "No closed signals available.";
7501
+ }
7502
+ // Build table header
7503
+ const header = pnlColumns.map((col) => col.label);
7504
+ const separator = pnlColumns.map(() => "---");
7505
+ // Build table rows
7506
+ const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7507
+ const tableData = [header, separator, ...rows];
7508
+ return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7509
+ }
7319
7510
  /**
7320
7511
  * Generates markdown report with all strategy results (View).
7512
+ * Includes best strategy summary, comparison table, and PNL table.
7321
7513
  *
7322
7514
  * @param symbol - Trading symbol
7323
7515
  * @param metric - Metric being optimized
@@ -7326,7 +7518,9 @@ let ReportStorage$1 = class ReportStorage {
7326
7518
  */
7327
7519
  async getReport(symbol, metric, context) {
7328
7520
  const results = await this.getData(symbol, metric, context);
7329
- return functoolsKit.str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7521
+ // Get total signals for best strategy
7522
+ const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7523
+ return functoolsKit.str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, `**Total Signals:** ${bestStrategySignals}`, "", "## Top Strategies Comparison", "", this.getComparisonTable(metric, 10), "", "## All Signals (PNL Table)", "", this.getPnlTable(), "", "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7330
7524
  }
7331
7525
  /**
7332
7526
  * Saves walker report to disk.
@@ -10714,6 +10908,145 @@ class PartialGlobalService {
10714
10908
  }
10715
10909
  }
10716
10910
 
10911
+ /**
10912
+ * Warning threshold for message size in kilobytes.
10913
+ * Messages exceeding this size trigger console warnings.
10914
+ */
10915
+ const WARN_KB = 100;
10916
+ /**
10917
+ * Internal function for dumping signal data to markdown files.
10918
+ * Creates a directory structure with system prompts, user messages, and LLM output.
10919
+ *
10920
+ * @param signalId - Unique identifier for the result
10921
+ * @param history - Array of message models from LLM conversation
10922
+ * @param signal - Signal DTO with trade parameters
10923
+ * @param outputDir - Output directory path (default: "./dump/strategy")
10924
+ * @returns Promise that resolves when all files are written
10925
+ */
10926
+ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/strategy") => {
10927
+ // Extract system messages and system reminders from existing data
10928
+ const systemMessages = history.filter((m) => m.role === "system");
10929
+ const userMessages = history.filter((m) => m.role === "user");
10930
+ const subfolderPath = path.join(outputDir, String(signalId));
10931
+ try {
10932
+ await fs$1.promises.access(subfolderPath);
10933
+ return;
10934
+ }
10935
+ catch {
10936
+ await fs$1.promises.mkdir(subfolderPath, { recursive: true });
10937
+ }
10938
+ {
10939
+ let summary = "# Outline Result Summary\n";
10940
+ {
10941
+ summary += "\n";
10942
+ summary += `**ResultId**: ${String(signalId)}\n`;
10943
+ summary += "\n";
10944
+ }
10945
+ if (signal) {
10946
+ summary += "## Output Data\n\n";
10947
+ summary += "```json\n";
10948
+ summary += JSON.stringify(signal, null, 2);
10949
+ summary += "\n```\n\n";
10950
+ }
10951
+ // Add system messages to summary
10952
+ if (systemMessages.length > 0) {
10953
+ summary += "## System Messages\n\n";
10954
+ systemMessages.forEach((msg, idx) => {
10955
+ summary += `### System Message ${idx + 1}\n\n`;
10956
+ summary += msg.content;
10957
+ summary += "\n";
10958
+ });
10959
+ }
10960
+ const summaryFile = path.join(subfolderPath, "00_system_prompt.md");
10961
+ await fs$1.promises.writeFile(summaryFile, summary, "utf8");
10962
+ }
10963
+ {
10964
+ await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
10965
+ const messageNum = String(idx + 1).padStart(2, "0");
10966
+ const contentFileName = `${messageNum}_user_message.md`;
10967
+ const contentFilePath = path.join(subfolderPath, contentFileName);
10968
+ {
10969
+ const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
10970
+ const messageSizeKb = Math.floor(messageSizeBytes / 1024);
10971
+ if (messageSizeKb > WARN_KB) {
10972
+ console.warn(`User message ${idx + 1} is ${messageSizeBytes} bytes (${messageSizeKb}kb), which exceeds warning limit`);
10973
+ }
10974
+ }
10975
+ let content = `# User Input ${idx + 1}\n\n`;
10976
+ content += `**ResultId**: ${String(signalId)}\n\n`;
10977
+ content += message.content;
10978
+ content += "\n";
10979
+ await fs$1.promises.writeFile(contentFilePath, content, "utf8");
10980
+ }));
10981
+ }
10982
+ {
10983
+ const messageNum = String(userMessages.length + 1).padStart(2, "0");
10984
+ const contentFileName = `${messageNum}_llm_output.md`;
10985
+ const contentFilePath = path.join(subfolderPath, contentFileName);
10986
+ let content = "# Full Outline Result\n\n";
10987
+ content += `**ResultId**: ${String(signalId)}\n\n`;
10988
+ if (signal) {
10989
+ content += "## Output Data\n\n";
10990
+ content += "```json\n";
10991
+ content += JSON.stringify(signal, null, 2);
10992
+ content += "\n```\n";
10993
+ }
10994
+ await fs$1.promises.writeFile(contentFilePath, content, "utf8");
10995
+ }
10996
+ };
10997
+ /**
10998
+ * Service for generating markdown documentation from LLM outline results.
10999
+ * Used by AI Strategy Optimizer to save debug logs and conversation history.
11000
+ *
11001
+ * Creates directory structure:
11002
+ * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
11003
+ * - ./dump/strategy/{signalId}/01_user_message.md - First user input
11004
+ * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
11005
+ * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
11006
+ */
11007
+ class OutlineMarkdownService {
11008
+ constructor() {
11009
+ /** Logger service injected via DI */
11010
+ this.loggerService = inject(TYPES.loggerService);
11011
+ /**
11012
+ * Dumps signal data and conversation history to markdown files.
11013
+ * Skips if directory already exists to avoid overwriting previous results.
11014
+ *
11015
+ * Generated files:
11016
+ * - 00_system_prompt.md - System messages and output summary
11017
+ * - XX_user_message.md - Each user message in separate file (numbered)
11018
+ * - XX_llm_output.md - Final LLM output with signal data
11019
+ *
11020
+ * @param signalId - Unique identifier for the result (used as directory name)
11021
+ * @param history - Array of message models from LLM conversation
11022
+ * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
11023
+ * @param outputDir - Output directory path (default: "./dump/strategy")
11024
+ * @returns Promise that resolves when all files are written
11025
+ *
11026
+ * @example
11027
+ * ```typescript
11028
+ * await outlineService.dumpSignal(
11029
+ * "strategy-1",
11030
+ * conversationHistory,
11031
+ * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
11032
+ * );
11033
+ * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
11034
+ * // ./dump/strategy/strategy-1/01_user_message.md
11035
+ * // ./dump/strategy/strategy-1/02_llm_output.md
11036
+ * ```
11037
+ */
11038
+ this.dumpSignal = async (signalId, history, signal, outputDir = "./dump/strategy") => {
11039
+ this.loggerService.log("outlineMarkdownService dumpSignal", {
11040
+ signalId,
11041
+ history,
11042
+ signal,
11043
+ outputDir,
11044
+ });
11045
+ return await DUMP_SIGNAL_FN(signalId, history, signal, outputDir);
11046
+ };
11047
+ }
11048
+ }
11049
+
10717
11050
  {
10718
11051
  provide(TYPES.loggerService, () => new LoggerService());
10719
11052
  }
@@ -10771,6 +11104,7 @@ class PartialGlobalService {
10771
11104
  provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
10772
11105
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
10773
11106
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11107
+ provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
10774
11108
  }
10775
11109
  {
10776
11110
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -10842,6 +11176,7 @@ const markdownServices = {
10842
11176
  walkerMarkdownService: inject(TYPES.walkerMarkdownService),
10843
11177
  heatMarkdownService: inject(TYPES.heatMarkdownService),
10844
11178
  partialMarkdownService: inject(TYPES.partialMarkdownService),
11179
+ outlineMarkdownService: inject(TYPES.outlineMarkdownService),
10845
11180
  };
10846
11181
  const validationServices = {
10847
11182
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12508,6 +12843,83 @@ async function getMode() {
12508
12843
  return bt ? "backtest" : "live";
12509
12844
  }
12510
12845
 
12846
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
12847
+ /**
12848
+ * Dumps signal data and LLM conversation history to markdown files.
12849
+ * Used by AI-powered strategies to save debug logs for analysis.
12850
+ *
12851
+ * Creates a directory structure with:
12852
+ * - 00_system_prompt.md - System messages and output summary
12853
+ * - XX_user_message.md - Each user message in separate file (numbered)
12854
+ * - XX_llm_output.md - Final LLM output with signal data
12855
+ *
12856
+ * Skips if directory already exists to avoid overwriting previous results.
12857
+ *
12858
+ * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
12859
+ * @param history - Array of message models from LLM conversation
12860
+ * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
12861
+ * @param outputDir - Output directory path (default: "./dump/strategy")
12862
+ * @returns Promise that resolves when all files are written
12863
+ *
12864
+ * @example
12865
+ * ```typescript
12866
+ * import { dumpSignal, getCandles } from "backtest-kit";
12867
+ * import { v4 as uuid } from "uuid";
12868
+ *
12869
+ * addStrategy({
12870
+ * strategyName: "llm-strategy",
12871
+ * interval: "5m",
12872
+ * getSignal: async (symbol) => {
12873
+ * const messages = [];
12874
+ *
12875
+ * // Build multi-timeframe analysis conversation
12876
+ * const candles1h = await getCandles(symbol, "1h", 24);
12877
+ * messages.push(
12878
+ * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
12879
+ * { role: "assistant", content: "Trend analyzed" }
12880
+ * );
12881
+ *
12882
+ * const candles5m = await getCandles(symbol, "5m", 24);
12883
+ * messages.push(
12884
+ * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
12885
+ * { role: "assistant", content: "Structure analyzed" }
12886
+ * );
12887
+ *
12888
+ * // Request signal
12889
+ * messages.push({
12890
+ * role: "user",
12891
+ * content: "Generate trading signal. Use position: 'wait' if uncertain."
12892
+ * });
12893
+ *
12894
+ * const resultId = uuid();
12895
+ * const signal = await llmRequest(messages);
12896
+ *
12897
+ * // Save conversation and result for debugging
12898
+ * await dumpSignal(resultId, messages, signal);
12899
+ *
12900
+ * return signal;
12901
+ * }
12902
+ * });
12903
+ *
12904
+ * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
12905
+ * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
12906
+ * // ./dump/strategy/{uuid}/02_assistant_message.md
12907
+ * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
12908
+ * // ./dump/strategy/{uuid}/04_assistant_message.md
12909
+ * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
12910
+ * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
12911
+ * ```
12912
+ */
12913
+ async function dumpSignal(signalId, history, signal, outputDir = "./dump/strategy") {
12914
+ backtest$1.loggerService.info(DUMP_SIGNAL_METHOD_NAME, {
12915
+ signalId,
12916
+ history,
12917
+ signal,
12918
+ outputDir,
12919
+ });
12920
+ return await backtest$1.outlineMarkdownService.dumpSignal(signalId, history, signal, outputDir);
12921
+ }
12922
+
12511
12923
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
12512
12924
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
12513
12925
  const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
@@ -13976,6 +14388,7 @@ exports.addRisk = addRisk;
13976
14388
  exports.addSizing = addSizing;
13977
14389
  exports.addStrategy = addStrategy;
13978
14390
  exports.addWalker = addWalker;
14391
+ exports.dumpSignal = dumpSignal;
13979
14392
  exports.emitters = emitters;
13980
14393
  exports.formatPrice = formatPrice;
13981
14394
  exports.formatQuantity = formatQuantity;
package/build/index.mjs CHANGED
@@ -5,6 +5,7 @@ import fs, { mkdir, writeFile } from 'fs/promises';
5
5
  import path, { join } from 'path';
6
6
  import crypto from 'crypto';
7
7
  import os from 'os';
8
+ import { promises } from 'fs';
8
9
 
9
10
  const GLOBAL_CONFIG = {
10
11
  /**
@@ -160,6 +161,7 @@ const markdownServices$1 = {
160
161
  walkerMarkdownService: Symbol('walkerMarkdownService'),
161
162
  heatMarkdownService: Symbol('heatMarkdownService'),
162
163
  partialMarkdownService: Symbol('partialMarkdownService'),
164
+ outlineMarkdownService: Symbol('outlineMarkdownService'),
163
165
  };
164
166
  const validationServices$1 = {
165
167
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -7260,6 +7262,118 @@ function formatMetric(value) {
7260
7262
  }
7261
7263
  return value.toFixed(2);
7262
7264
  }
7265
+ /**
7266
+ * Creates strategy comparison columns based on metric name.
7267
+ * Dynamically builds column configuration with metric-specific header.
7268
+ *
7269
+ * @param metric - Metric being optimized
7270
+ * @returns Array of column configurations for strategy comparison table
7271
+ */
7272
+ function createStrategyColumns(metric) {
7273
+ return [
7274
+ {
7275
+ key: "rank",
7276
+ label: "Rank",
7277
+ format: (data, index) => `${index + 1}`,
7278
+ },
7279
+ {
7280
+ key: "strategy",
7281
+ label: "Strategy",
7282
+ format: (data) => data.strategyName,
7283
+ },
7284
+ {
7285
+ key: "metric",
7286
+ label: metric,
7287
+ format: (data) => formatMetric(data.metricValue),
7288
+ },
7289
+ {
7290
+ key: "totalSignals",
7291
+ label: "Total Signals",
7292
+ format: (data) => `${data.stats.totalSignals}`,
7293
+ },
7294
+ {
7295
+ key: "winRate",
7296
+ label: "Win Rate",
7297
+ format: (data) => data.stats.winRate !== null
7298
+ ? `${data.stats.winRate.toFixed(2)}%`
7299
+ : "N/A",
7300
+ },
7301
+ {
7302
+ key: "avgPnl",
7303
+ label: "Avg PNL",
7304
+ format: (data) => data.stats.avgPnl !== null
7305
+ ? `${data.stats.avgPnl > 0 ? "+" : ""}${data.stats.avgPnl.toFixed(2)}%`
7306
+ : "N/A",
7307
+ },
7308
+ {
7309
+ key: "totalPnl",
7310
+ label: "Total PNL",
7311
+ format: (data) => data.stats.totalPnl !== null
7312
+ ? `${data.stats.totalPnl > 0 ? "+" : ""}${data.stats.totalPnl.toFixed(2)}%`
7313
+ : "N/A",
7314
+ },
7315
+ {
7316
+ key: "sharpeRatio",
7317
+ label: "Sharpe Ratio",
7318
+ format: (data) => data.stats.sharpeRatio !== null
7319
+ ? `${data.stats.sharpeRatio.toFixed(3)}`
7320
+ : "N/A",
7321
+ },
7322
+ {
7323
+ key: "stdDev",
7324
+ label: "Std Dev",
7325
+ format: (data) => data.stats.stdDev !== null
7326
+ ? `${data.stats.stdDev.toFixed(3)}%`
7327
+ : "N/A",
7328
+ },
7329
+ ];
7330
+ }
7331
+ /**
7332
+ * Column configuration for PNL table.
7333
+ * Defines all columns for displaying closed signals across strategies.
7334
+ */
7335
+ const pnlColumns = [
7336
+ {
7337
+ key: "strategy",
7338
+ label: "Strategy",
7339
+ format: (data) => data.strategyName,
7340
+ },
7341
+ {
7342
+ key: "signalId",
7343
+ label: "Signal ID",
7344
+ format: (data) => data.signalId,
7345
+ },
7346
+ {
7347
+ key: "symbol",
7348
+ label: "Symbol",
7349
+ format: (data) => data.symbol,
7350
+ },
7351
+ {
7352
+ key: "position",
7353
+ label: "Position",
7354
+ format: (data) => data.position.toUpperCase(),
7355
+ },
7356
+ {
7357
+ key: "pnl",
7358
+ label: "PNL (net)",
7359
+ format: (data) => `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`,
7360
+ },
7361
+ {
7362
+ key: "closeReason",
7363
+ label: "Close Reason",
7364
+ format: (data) => data.closeReason,
7365
+ },
7366
+ {
7367
+ key: "openTime",
7368
+ label: "Open Time",
7369
+ format: (data) => new Date(data.openTime).toISOString(),
7370
+ },
7371
+ {
7372
+ key: "closeTime",
7373
+ label: "Close Time",
7374
+ format: (data) => new Date(data.closeTime).toISOString(),
7375
+ },
7376
+ ];
7263
7377
  /**
7264
7378
  * Storage class for accumulating walker results.
7265
7379
  * Maintains a list of all strategy results and provides methods to generate reports.
@@ -7272,9 +7386,12 @@ let ReportStorage$1 = class ReportStorage {
7272
7386
  this._bestStats = null;
7273
7387
  this._bestMetric = null;
7274
7388
  this._bestStrategy = null;
7389
+ /** All strategy results for comparison table */
7390
+ this._strategyResults = [];
7275
7391
  }
7276
7392
  /**
7277
7393
  * Adds a strategy result to the storage.
7394
+ * Updates best strategy tracking and accumulates result for comparison table.
7278
7395
  *
7279
7396
  * @param data - Walker contract with strategy result
7280
7397
  */
@@ -7288,6 +7405,12 @@ let ReportStorage$1 = class ReportStorage {
7288
7405
  if (data.strategyName === data.bestStrategy) {
7289
7406
  this._bestStats = data.stats;
7290
7407
  }
7408
+ // Add strategy result to comparison list
7409
+ this._strategyResults.push({
7410
+ strategyName: data.strategyName,
7411
+ stats: data.stats,
7412
+ metricValue: data.metricValue,
7413
+ });
7291
7414
  }
7292
7415
  /**
7293
7416
  * Calculates walker results from strategy results.
@@ -7312,10 +7435,79 @@ let ReportStorage$1 = class ReportStorage {
7312
7435
  bestStrategy: this._bestStrategy,
7313
7436
  bestMetric: this._bestMetric,
7314
7437
  bestStats: this._bestStats,
7438
+ strategyResults: this._strategyResults,
7315
7439
  };
7316
7440
  }
7441
+ /**
7442
+ * Generates comparison table for top N strategies (View).
7443
+ * Sorts strategies by metric value and formats as markdown table.
7444
+ *
7445
+ * @param metric - Metric being optimized
7446
+ * @param topN - Number of top strategies to include (default: 10)
7447
+ * @returns Markdown formatted comparison table
7448
+ */
7449
+ getComparisonTable(metric, topN = 10) {
7450
+ if (this._strategyResults.length === 0) {
7451
+ return "No strategy results available.";
7452
+ }
7453
+ // Sort strategies by metric value (descending)
7454
+ const sortedResults = [...this._strategyResults].sort((a, b) => {
7455
+ const aValue = a.metricValue ?? -Infinity;
7456
+ const bValue = b.metricValue ?? -Infinity;
7457
+ return bValue - aValue;
7458
+ });
7459
+ // Take top N strategies
7460
+ const topStrategies = sortedResults.slice(0, topN);
7461
+ // Get columns configuration
7462
+ const columns = createStrategyColumns(metric);
7463
+ // Build table header
7464
+ const header = columns.map((col) => col.label);
7465
+ const separator = columns.map(() => "---");
7466
+ // Build table rows
7467
+ const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7468
+ const tableData = [header, separator, ...rows];
7469
+ return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7470
+ }
7471
+ /**
7472
+ * Generates PNL table showing all closed signals across all strategies (View).
7473
+ * Collects all signals from all strategies and formats as markdown table.
7474
+ *
7475
+ * @returns Markdown formatted PNL table
7476
+ */
7477
+ getPnlTable() {
7478
+ if (this._strategyResults.length === 0) {
7479
+ return "No strategy results available.";
7480
+ }
7481
+ // Collect all closed signals from all strategies
7482
+ const allSignals = [];
7483
+ for (const result of this._strategyResults) {
7484
+ for (const signal of result.stats.signalList) {
7485
+ allSignals.push({
7486
+ strategyName: result.strategyName,
7487
+ signalId: signal.signal.id,
7488
+ symbol: signal.signal.symbol,
7489
+ position: signal.signal.position,
7490
+ pnl: signal.pnl.pnlPercentage,
7491
+ closeReason: signal.closeReason,
7492
+ openTime: signal.signal.pendingAt,
7493
+ closeTime: signal.closeTimestamp,
7494
+ });
7495
+ }
7496
+ }
7497
+ if (allSignals.length === 0) {
7498
+ return "No closed signals available.";
7499
+ }
7500
+ // Build table header
7501
+ const header = pnlColumns.map((col) => col.label);
7502
+ const separator = pnlColumns.map(() => "---");
7503
+ // Build table rows
7504
+ const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7505
+ const tableData = [header, separator, ...rows];
7506
+ return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7507
+ }
7317
7508
  /**
7318
7509
  * Generates markdown report with all strategy results (View).
7510
+ * Includes best strategy summary, comparison table, and PNL table.
7319
7511
  *
7320
7512
  * @param symbol - Trading symbol
7321
7513
  * @param metric - Metric being optimized
@@ -7324,7 +7516,9 @@ let ReportStorage$1 = class ReportStorage {
7324
7516
  */
7325
7517
  async getReport(symbol, metric, context) {
7326
7518
  const results = await this.getData(symbol, metric, context);
7327
- return str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7519
+ // Get total signals for best strategy
7520
+ const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7521
+ return str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, `**Total Signals:** ${bestStrategySignals}`, "", "## Top Strategies Comparison", "", this.getComparisonTable(metric, 10), "", "## All Signals (PNL Table)", "", this.getPnlTable(), "", "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7328
7522
  }
7329
7523
  /**
7330
7524
  * Saves walker report to disk.
@@ -10712,6 +10906,145 @@ class PartialGlobalService {
10712
10906
  }
10713
10907
  }
10714
10908
 
10909
+ /**
10910
+ * Warning threshold for message size in kilobytes.
10911
+ * Messages exceeding this size trigger console warnings.
10912
+ */
10913
+ const WARN_KB = 100;
10914
+ /**
10915
+ * Internal function for dumping signal data to markdown files.
10916
+ * Creates a directory structure with system prompts, user messages, and LLM output.
10917
+ *
10918
+ * @param signalId - Unique identifier for the result
10919
+ * @param history - Array of message models from LLM conversation
10920
+ * @param signal - Signal DTO with trade parameters
10921
+ * @param outputDir - Output directory path (default: "./dump/strategy")
10922
+ * @returns Promise that resolves when all files are written
10923
+ */
10924
+ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/strategy") => {
10925
+ // Extract system messages and system reminders from existing data
10926
+ const systemMessages = history.filter((m) => m.role === "system");
10927
+ const userMessages = history.filter((m) => m.role === "user");
10928
+ const subfolderPath = path.join(outputDir, String(signalId));
10929
+ try {
10930
+ await promises.access(subfolderPath);
10931
+ return;
10932
+ }
10933
+ catch {
10934
+ await promises.mkdir(subfolderPath, { recursive: true });
10935
+ }
10936
+ {
10937
+ let summary = "# Outline Result Summary\n";
10938
+ {
10939
+ summary += "\n";
10940
+ summary += `**ResultId**: ${String(signalId)}\n`;
10941
+ summary += "\n";
10942
+ }
10943
+ if (signal) {
10944
+ summary += "## Output Data\n\n";
10945
+ summary += "```json\n";
10946
+ summary += JSON.stringify(signal, null, 2);
10947
+ summary += "\n```\n\n";
10948
+ }
10949
+ // Add system messages to summary
10950
+ if (systemMessages.length > 0) {
10951
+ summary += "## System Messages\n\n";
10952
+ systemMessages.forEach((msg, idx) => {
10953
+ summary += `### System Message ${idx + 1}\n\n`;
10954
+ summary += msg.content;
10955
+ summary += "\n";
10956
+ });
10957
+ }
10958
+ const summaryFile = path.join(subfolderPath, "00_system_prompt.md");
10959
+ await promises.writeFile(summaryFile, summary, "utf8");
10960
+ }
10961
+ {
10962
+ await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
10963
+ const messageNum = String(idx + 1).padStart(2, "0");
10964
+ const contentFileName = `${messageNum}_user_message.md`;
10965
+ const contentFilePath = path.join(subfolderPath, contentFileName);
10966
+ {
10967
+ const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
10968
+ const messageSizeKb = Math.floor(messageSizeBytes / 1024);
10969
+ if (messageSizeKb > WARN_KB) {
10970
+ console.warn(`User message ${idx + 1} is ${messageSizeBytes} bytes (${messageSizeKb}kb), which exceeds warning limit`);
10971
+ }
10972
+ }
10973
+ let content = `# User Input ${idx + 1}\n\n`;
10974
+ content += `**ResultId**: ${String(signalId)}\n\n`;
10975
+ content += message.content;
10976
+ content += "\n";
10977
+ await promises.writeFile(contentFilePath, content, "utf8");
10978
+ }));
10979
+ }
10980
+ {
10981
+ const messageNum = String(userMessages.length + 1).padStart(2, "0");
10982
+ const contentFileName = `${messageNum}_llm_output.md`;
10983
+ const contentFilePath = path.join(subfolderPath, contentFileName);
10984
+ let content = "# Full Outline Result\n\n";
10985
+ content += `**ResultId**: ${String(signalId)}\n\n`;
10986
+ if (signal) {
10987
+ content += "## Output Data\n\n";
10988
+ content += "```json\n";
10989
+ content += JSON.stringify(signal, null, 2);
10990
+ content += "\n```\n";
10991
+ }
10992
+ await promises.writeFile(contentFilePath, content, "utf8");
10993
+ }
10994
+ };
10995
+ /**
10996
+ * Service for generating markdown documentation from LLM outline results.
10997
+ * Used by AI Strategy Optimizer to save debug logs and conversation history.
10998
+ *
10999
+ * Creates directory structure:
11000
+ * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
11001
+ * - ./dump/strategy/{signalId}/01_user_message.md - First user input
11002
+ * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
11003
+ * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
11004
+ */
11005
+ class OutlineMarkdownService {
11006
+ constructor() {
11007
+ /** Logger service injected via DI */
11008
+ this.loggerService = inject(TYPES.loggerService);
11009
+ /**
11010
+ * Dumps signal data and conversation history to markdown files.
11011
+ * Skips if directory already exists to avoid overwriting previous results.
11012
+ *
11013
+ * Generated files:
11014
+ * - 00_system_prompt.md - System messages and output summary
11015
+ * - XX_user_message.md - Each user message in separate file (numbered)
11016
+ * - XX_llm_output.md - Final LLM output with signal data
11017
+ *
11018
+ * @param signalId - Unique identifier for the result (used as directory name)
11019
+ * @param history - Array of message models from LLM conversation
11020
+ * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
11021
+ * @param outputDir - Output directory path (default: "./dump/strategy")
11022
+ * @returns Promise that resolves when all files are written
11023
+ *
11024
+ * @example
11025
+ * ```typescript
11026
+ * await outlineService.dumpSignal(
11027
+ * "strategy-1",
11028
+ * conversationHistory,
11029
+ * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
11030
+ * );
11031
+ * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
11032
+ * // ./dump/strategy/strategy-1/01_user_message.md
11033
+ * // ./dump/strategy/strategy-1/02_llm_output.md
11034
+ * ```
11035
+ */
11036
+ this.dumpSignal = async (signalId, history, signal, outputDir = "./dump/strategy") => {
11037
+ this.loggerService.log("outlineMarkdownService dumpSignal", {
11038
+ signalId,
11039
+ history,
11040
+ signal,
11041
+ outputDir,
11042
+ });
11043
+ return await DUMP_SIGNAL_FN(signalId, history, signal, outputDir);
11044
+ };
11045
+ }
11046
+ }
11047
+
10715
11048
  {
10716
11049
  provide(TYPES.loggerService, () => new LoggerService());
10717
11050
  }
@@ -10769,6 +11102,7 @@ class PartialGlobalService {
10769
11102
  provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
10770
11103
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
10771
11104
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11105
+ provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
10772
11106
  }
10773
11107
  {
10774
11108
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -10840,6 +11174,7 @@ const markdownServices = {
10840
11174
  walkerMarkdownService: inject(TYPES.walkerMarkdownService),
10841
11175
  heatMarkdownService: inject(TYPES.heatMarkdownService),
10842
11176
  partialMarkdownService: inject(TYPES.partialMarkdownService),
11177
+ outlineMarkdownService: inject(TYPES.outlineMarkdownService),
10843
11178
  };
10844
11179
  const validationServices = {
10845
11180
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12506,6 +12841,83 @@ async function getMode() {
12506
12841
  return bt ? "backtest" : "live";
12507
12842
  }
12508
12843
 
12844
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
12845
+ /**
12846
+ * Dumps signal data and LLM conversation history to markdown files.
12847
+ * Used by AI-powered strategies to save debug logs for analysis.
12848
+ *
12849
+ * Creates a directory structure with:
12850
+ * - 00_system_prompt.md - System messages and output summary
12851
+ * - XX_user_message.md - Each user message in separate file (numbered)
12852
+ * - XX_llm_output.md - Final LLM output with signal data
12853
+ *
12854
+ * Skips if directory already exists to avoid overwriting previous results.
12855
+ *
12856
+ * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
12857
+ * @param history - Array of message models from LLM conversation
12858
+ * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
12859
+ * @param outputDir - Output directory path (default: "./dump/strategy")
12860
+ * @returns Promise that resolves when all files are written
12861
+ *
12862
+ * @example
12863
+ * ```typescript
12864
+ * import { dumpSignal, getCandles } from "backtest-kit";
12865
+ * import { v4 as uuid } from "uuid";
12866
+ *
12867
+ * addStrategy({
12868
+ * strategyName: "llm-strategy",
12869
+ * interval: "5m",
12870
+ * getSignal: async (symbol) => {
12871
+ * const messages = [];
12872
+ *
12873
+ * // Build multi-timeframe analysis conversation
12874
+ * const candles1h = await getCandles(symbol, "1h", 24);
12875
+ * messages.push(
12876
+ * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
12877
+ * { role: "assistant", content: "Trend analyzed" }
12878
+ * );
12879
+ *
12880
+ * const candles5m = await getCandles(symbol, "5m", 24);
12881
+ * messages.push(
12882
+ * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
12883
+ * { role: "assistant", content: "Structure analyzed" }
12884
+ * );
12885
+ *
12886
+ * // Request signal
12887
+ * messages.push({
12888
+ * role: "user",
12889
+ * content: "Generate trading signal. Use position: 'wait' if uncertain."
12890
+ * });
12891
+ *
12892
+ * const resultId = uuid();
12893
+ * const signal = await llmRequest(messages);
12894
+ *
12895
+ * // Save conversation and result for debugging
12896
+ * await dumpSignal(resultId, messages, signal);
12897
+ *
12898
+ * return signal;
12899
+ * }
12900
+ * });
12901
+ *
12902
+ * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
12903
+ * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
12904
+ * // ./dump/strategy/{uuid}/02_assistant_message.md
12905
+ * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
12906
+ * // ./dump/strategy/{uuid}/04_assistant_message.md
12907
+ * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
12908
+ * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
12909
+ * ```
12910
+ */
12911
+ async function dumpSignal(signalId, history, signal, outputDir = "./dump/strategy") {
12912
+ backtest$1.loggerService.info(DUMP_SIGNAL_METHOD_NAME, {
12913
+ signalId,
12914
+ history,
12915
+ signal,
12916
+ outputDir,
12917
+ });
12918
+ return await backtest$1.outlineMarkdownService.dumpSignal(signalId, history, signal, outputDir);
12919
+ }
12920
+
12509
12921
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
12510
12922
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
12511
12923
  const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
@@ -13950,4 +14362,4 @@ class ConstantUtils {
13950
14362
  */
13951
14363
  const Constant = new ConstantUtils();
13952
14364
 
13953
- export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
14365
+ export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.4.10",
3
+ "version": "1.4.12",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -3616,6 +3616,74 @@ declare function getDate(): Promise<Date>;
3616
3616
  */
3617
3617
  declare function getMode(): Promise<"backtest" | "live">;
3618
3618
 
3619
+ /**
3620
+ * Dumps signal data and LLM conversation history to markdown files.
3621
+ * Used by AI-powered strategies to save debug logs for analysis.
3622
+ *
3623
+ * Creates a directory structure with:
3624
+ * - 00_system_prompt.md - System messages and output summary
3625
+ * - XX_user_message.md - Each user message in separate file (numbered)
3626
+ * - XX_llm_output.md - Final LLM output with signal data
3627
+ *
3628
+ * Skips if directory already exists to avoid overwriting previous results.
3629
+ *
3630
+ * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
3631
+ * @param history - Array of message models from LLM conversation
3632
+ * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
3633
+ * @param outputDir - Output directory path (default: "./dump/strategy")
3634
+ * @returns Promise that resolves when all files are written
3635
+ *
3636
+ * @example
3637
+ * ```typescript
3638
+ * import { dumpSignal, getCandles } from "backtest-kit";
3639
+ * import { v4 as uuid } from "uuid";
3640
+ *
3641
+ * addStrategy({
3642
+ * strategyName: "llm-strategy",
3643
+ * interval: "5m",
3644
+ * getSignal: async (symbol) => {
3645
+ * const messages = [];
3646
+ *
3647
+ * // Build multi-timeframe analysis conversation
3648
+ * const candles1h = await getCandles(symbol, "1h", 24);
3649
+ * messages.push(
3650
+ * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
3651
+ * { role: "assistant", content: "Trend analyzed" }
3652
+ * );
3653
+ *
3654
+ * const candles5m = await getCandles(symbol, "5m", 24);
3655
+ * messages.push(
3656
+ * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
3657
+ * { role: "assistant", content: "Structure analyzed" }
3658
+ * );
3659
+ *
3660
+ * // Request signal
3661
+ * messages.push({
3662
+ * role: "user",
3663
+ * content: "Generate trading signal. Use position: 'wait' if uncertain."
3664
+ * });
3665
+ *
3666
+ * const resultId = uuid();
3667
+ * const signal = await llmRequest(messages);
3668
+ *
3669
+ * // Save conversation and result for debugging
3670
+ * await dumpSignal(resultId, messages, signal);
3671
+ *
3672
+ * return signal;
3673
+ * }
3674
+ * });
3675
+ *
3676
+ * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
3677
+ * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
3678
+ * // ./dump/strategy/{uuid}/02_assistant_message.md
3679
+ * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
3680
+ * // ./dump/strategy/{uuid}/04_assistant_message.md
3681
+ * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
3682
+ * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
3683
+ * ```
3684
+ */
3685
+ declare function dumpSignal(signalId: string | number, history: MessageModel[], signal: ISignalDto, outputDir?: string): Promise<void>;
3686
+
3619
3687
  /**
3620
3688
  * Portfolio heatmap statistics for a single symbol.
3621
3689
  * Aggregated metrics across all strategies for one trading pair.
@@ -4258,8 +4326,24 @@ declare class PerformanceMarkdownService {
4258
4326
  * Alias for walker statistics result interface.
4259
4327
  * Used for clarity in markdown service context.
4260
4328
  *
4329
+ * Extends IWalkerResults with additional strategy comparison data.
4261
4330
  */
4262
- type WalkerStatistics = IWalkerResults;
4331
+ interface WalkerStatistics extends IWalkerResults {
4332
+ /** Array of all strategy results for comparison and analysis */
4333
+ strategyResults: IStrategyResult[];
4334
+ }
4335
+ /**
4336
+ * Strategy result entry for comparison table.
4337
+ * Contains strategy name, full statistics, and metric value for ranking.
4338
+ */
4339
+ interface IStrategyResult {
4340
+ /** Strategy name */
4341
+ strategyName: StrategyName;
4342
+ /** Complete backtest statistics for this strategy */
4343
+ stats: BacktestStatistics;
4344
+ /** Value of the optimization metric (null if invalid) */
4345
+ metricValue: number | null;
4346
+ }
4263
4347
  /**
4264
4348
  * Service for generating and saving walker markdown reports.
4265
4349
  *
@@ -8645,6 +8729,54 @@ declare class PartialGlobalService {
8645
8729
  clear: (symbol: string, data: ISignalRow, priceClose: number) => Promise<void>;
8646
8730
  }
8647
8731
 
8732
+ /**
8733
+ * Unique identifier for outline result.
8734
+ * Can be string or number for flexible ID formats.
8735
+ */
8736
+ type ResultId = string | number;
8737
+ /**
8738
+ * Service for generating markdown documentation from LLM outline results.
8739
+ * Used by AI Strategy Optimizer to save debug logs and conversation history.
8740
+ *
8741
+ * Creates directory structure:
8742
+ * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
8743
+ * - ./dump/strategy/{signalId}/01_user_message.md - First user input
8744
+ * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
8745
+ * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
8746
+ */
8747
+ declare class OutlineMarkdownService {
8748
+ /** Logger service injected via DI */
8749
+ private readonly loggerService;
8750
+ /**
8751
+ * Dumps signal data and conversation history to markdown files.
8752
+ * Skips if directory already exists to avoid overwriting previous results.
8753
+ *
8754
+ * Generated files:
8755
+ * - 00_system_prompt.md - System messages and output summary
8756
+ * - XX_user_message.md - Each user message in separate file (numbered)
8757
+ * - XX_llm_output.md - Final LLM output with signal data
8758
+ *
8759
+ * @param signalId - Unique identifier for the result (used as directory name)
8760
+ * @param history - Array of message models from LLM conversation
8761
+ * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
8762
+ * @param outputDir - Output directory path (default: "./dump/strategy")
8763
+ * @returns Promise that resolves when all files are written
8764
+ *
8765
+ * @example
8766
+ * ```typescript
8767
+ * await outlineService.dumpSignal(
8768
+ * "strategy-1",
8769
+ * conversationHistory,
8770
+ * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
8771
+ * );
8772
+ * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
8773
+ * // ./dump/strategy/strategy-1/01_user_message.md
8774
+ * // ./dump/strategy/strategy-1/02_llm_output.md
8775
+ * ```
8776
+ */
8777
+ dumpSignal: (signalId: ResultId, history: MessageModel[], signal: ISignalDto, outputDir?: string) => Promise<void>;
8778
+ }
8779
+
8648
8780
  declare const backtest: {
8649
8781
  optimizerTemplateService: OptimizerTemplateService;
8650
8782
  exchangeValidationService: ExchangeValidationService;
@@ -8661,6 +8793,7 @@ declare const backtest: {
8661
8793
  walkerMarkdownService: WalkerMarkdownService;
8662
8794
  heatMarkdownService: HeatMarkdownService;
8663
8795
  partialMarkdownService: PartialMarkdownService;
8796
+ outlineMarkdownService: OutlineMarkdownService;
8664
8797
  backtestLogicPublicService: BacktestLogicPublicService;
8665
8798
  liveLogicPublicService: LiveLogicPublicService;
8666
8799
  walkerLogicPublicService: WalkerLogicPublicService;
@@ -8700,4 +8833,4 @@ declare const backtest: {
8700
8833
  loggerService: LoggerService;
8701
8834
  };
8702
8835
 
8703
- export { Backtest, type BacktestStatistics, type CandleInterval, Constant, type DoneContract, type EntityId, ExecutionContextService, type FrameInterval, type GlobalConfig, Heat, type ICandleData, type IExchangeSchema, type IFrameSchema, type IHeatmapRow, type IHeatmapStatistics, type IOptimizerCallbacks, type IOptimizerData, type IOptimizerFetchArgs, type IOptimizerFilterArgs, type IOptimizerRange, type IOptimizerSchema, type IOptimizerSource, type IOptimizerStrategy, type IOptimizerTemplate, type IPersistBase, type IPositionSizeATRParams, type IPositionSizeFixedPercentageParams, type IPositionSizeKellyParams, type IRiskActivePosition, type IRiskCheckArgs, type IRiskSchema, type IRiskValidation, type IRiskValidationFn, type IRiskValidationPayload, type IScheduledSignalRow, type ISignalDto, type ISignalRow, type ISizingCalculateParams, type ISizingCalculateParamsATR, type ISizingCalculateParamsFixedPercentage, type ISizingCalculateParamsKelly, type ISizingSchema, type ISizingSchemaATR, type ISizingSchemaFixedPercentage, type ISizingSchemaKelly, type IStrategyPnL, type IStrategySchema, type IStrategyTickResult, type IStrategyTickResultActive, type IStrategyTickResultCancelled, type IStrategyTickResultClosed, type IStrategyTickResultIdle, type IStrategyTickResultOpened, type IStrategyTickResultScheduled, type IWalkerResults, type IWalkerSchema, type IWalkerStrategyResult, Live, type LiveStatistics, type MessageModel, type MessageRole, MethodContextService, Optimizer, Partial$1 as Partial, type PartialData, type PartialLossContract, type PartialProfitContract, type PartialStatistics, Performance, type PerformanceContract, type PerformanceMetricType, type PerformanceStatistics, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, type ProgressBacktestContract, type ProgressOptimizerContract, type ProgressWalkerContract, type RiskData, Schedule, type ScheduleData, type ScheduleStatistics, type SignalData, type SignalInterval, type TPersistBase, type TPersistBaseCtor, Walker, type WalkerContract, type WalkerMetric, type WalkerStatistics, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
8836
+ export { Backtest, type BacktestStatistics, type CandleInterval, Constant, type DoneContract, type EntityId, ExecutionContextService, type FrameInterval, type GlobalConfig, Heat, type ICandleData, type IExchangeSchema, type IFrameSchema, type IHeatmapRow, type IHeatmapStatistics, type IOptimizerCallbacks, type IOptimizerData, type IOptimizerFetchArgs, type IOptimizerFilterArgs, type IOptimizerRange, type IOptimizerSchema, type IOptimizerSource, type IOptimizerStrategy, type IOptimizerTemplate, type IPersistBase, type IPositionSizeATRParams, type IPositionSizeFixedPercentageParams, type IPositionSizeKellyParams, type IRiskActivePosition, type IRiskCheckArgs, type IRiskSchema, type IRiskValidation, type IRiskValidationFn, type IRiskValidationPayload, type IScheduledSignalRow, type ISignalDto, type ISignalRow, type ISizingCalculateParams, type ISizingCalculateParamsATR, type ISizingCalculateParamsFixedPercentage, type ISizingCalculateParamsKelly, type ISizingSchema, type ISizingSchemaATR, type ISizingSchemaFixedPercentage, type ISizingSchemaKelly, type IStrategyPnL, type IStrategySchema, type IStrategyTickResult, type IStrategyTickResultActive, type IStrategyTickResultCancelled, type IStrategyTickResultClosed, type IStrategyTickResultIdle, type IStrategyTickResultOpened, type IStrategyTickResultScheduled, type IWalkerResults, type IWalkerSchema, type IWalkerStrategyResult, Live, type LiveStatistics, type MessageModel, type MessageRole, MethodContextService, Optimizer, Partial$1 as Partial, type PartialData, type PartialLossContract, type PartialProfitContract, type PartialStatistics, Performance, type PerformanceContract, type PerformanceMetricType, type PerformanceStatistics, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, type ProgressBacktestContract, type ProgressOptimizerContract, type ProgressWalkerContract, type RiskData, Schedule, type ScheduleData, type ScheduleStatistics, type SignalData, type SignalInterval, type TPersistBase, type TPersistBaseCtor, Walker, type WalkerContract, type WalkerMetric, type WalkerStatistics, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };