@backtest-kit/ollama 0.1.4 → 0.2.0

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.
Files changed (4) hide show
  1. package/build/index.cjs +2300 -104
  2. package/build/index.mjs +2294 -108
  3. package/package.json +1 -1
  4. package/types.d.ts +1353 -1
package/build/index.mjs CHANGED
@@ -1,16 +1,19 @@
1
- import { json, event, validateToolArguments, RoundRobin, addCompletion, addOutline, validate, overrideOutline } from 'agent-swarm-kit';
1
+ import { json, event, validateToolArguments, RoundRobin, addCompletion, addOutline, validate as validate$1, overrideOutline } from 'agent-swarm-kit';
2
2
  import { scoped } from 'di-scoped';
3
3
  import { createActivator } from 'di-kit';
4
+ import { Markdown, getContext, getMode } from 'backtest-kit';
5
+ import path, { join } from 'path';
4
6
  import MarkdownIt from 'markdown-it';
5
7
  import sanitizeHtml from 'sanitize-html';
6
8
  import { lint } from 'markdownlint/promise';
7
9
  import { applyFixes } from 'markdownlint';
8
- import { ToolRegistry, memoize, singleshot, randomString, fetchApi, str, timeout } from 'functools-kit';
10
+ import { ToolRegistry, memoize, singleshot, Subject, trycatch, errorData, getErrorMessage, iterateDocuments, distinctDocuments, resolveDocuments, randomString, fetchApi, str, timeout } from 'functools-kit';
11
+ import { createRequire } from 'module';
12
+ import fs, { mkdir, writeFile } from 'fs/promises';
9
13
  import OpenAI from 'openai';
10
14
  import { AIMessage, SystemMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
11
15
  import { ChatXAI } from '@langchain/xai';
12
16
  import { jsonrepair } from 'jsonrepair';
13
- import fs from 'fs/promises';
14
17
  import { InferenceClient } from '@huggingface/inference';
15
18
  import { ChatOpenAI } from '@langchain/openai';
16
19
  import { get, set } from 'lodash-es';
@@ -251,6 +254,19 @@ const publicServices$1 = {
251
254
  /** Outline public service for simplified structured completions */
252
255
  outlinePublicService: Symbol('outlinePublicService'),
253
256
  };
257
+ const promptServices$1 = {
258
+ signalPromptService: Symbol('signalPromptService'),
259
+ };
260
+ const markdownServices$1 = {
261
+ outlineMarkdownService: Symbol('outlineMarkdownService'),
262
+ };
263
+ const optimizerServices$1 = {
264
+ optimizerTemplateService: Symbol('optimizerTemplateService'),
265
+ optimizerSchemaService: Symbol('optimizerSchemaService'),
266
+ optimizerValidationService: Symbol('optimizerValidationService'),
267
+ optimizerConnectionService: Symbol('optimizerConnectionService'),
268
+ optimizerGlobalService: Symbol('optimizerGlobalService'),
269
+ };
254
270
  /**
255
271
  * Service type identifier registry for dependency injection.
256
272
  *
@@ -269,10 +285,169 @@ const publicServices$1 = {
269
285
  const TYPES = {
270
286
  ...commonServices$1,
271
287
  ...baseServices$1,
288
+ ...promptServices$1,
289
+ ...markdownServices$1,
290
+ ...optimizerServices$1,
272
291
  ...privateServices$1,
273
292
  ...publicServices$1,
274
293
  };
275
294
 
295
+ /**
296
+ * Warning threshold for message size in kilobytes.
297
+ * Messages exceeding this size trigger console warnings.
298
+ */
299
+ const WARN_KB = 30;
300
+ /**
301
+ * Internal function for dumping signal data to markdown files.
302
+ * Creates a directory structure with system prompts, user messages, and LLM output.
303
+ *
304
+ * @param signalId - Unique identifier for the result
305
+ * @param history - Array of message models from LLM conversation
306
+ * @param signal - Signal DTO with trade parameters
307
+ * @param outputDir - Output directory path (default: "./dump/strategy")
308
+ * @returns Promise that resolves when all files are written
309
+ */
310
+ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/outline") => {
311
+ // Extract system messages and system reminders from existing data
312
+ const systemMessages = history.filter((m) => m.role === "system");
313
+ const userMessages = history.filter((m) => m.role === "user");
314
+ const subfolderPath = path.join(outputDir, String(signalId));
315
+ // Generate system prompt markdown
316
+ {
317
+ let summary = "# Outline Result Summary\n";
318
+ {
319
+ summary += "\n";
320
+ summary += `**ResultId**: ${String(signalId)}\n`;
321
+ summary += "\n";
322
+ }
323
+ if (signal) {
324
+ summary += "## Output Data\n\n";
325
+ summary += "```json\n";
326
+ summary += JSON.stringify(signal, null, 2);
327
+ summary += "\n```\n\n";
328
+ }
329
+ // Add system messages to summary
330
+ if (systemMessages.length > 0) {
331
+ summary += "## System Messages\n\n";
332
+ systemMessages.forEach((msg, idx) => {
333
+ summary += `### System Message ${idx + 1}\n\n`;
334
+ summary += msg.content;
335
+ summary += "\n";
336
+ });
337
+ }
338
+ await Markdown.writeData("outline", summary, {
339
+ path: subfolderPath,
340
+ file: "00_system_prompt.md",
341
+ symbol: "",
342
+ signalId: String(signalId),
343
+ strategyName: "",
344
+ exchangeName: "",
345
+ frameName: ""
346
+ });
347
+ }
348
+ // Generate user messages
349
+ {
350
+ await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
351
+ const messageNum = String(idx + 1).padStart(2, "0");
352
+ const contentFileName = `${messageNum}_user_message.md`;
353
+ {
354
+ const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
355
+ const messageSizeKb = Math.floor(messageSizeBytes / 1024);
356
+ if (messageSizeKb > WARN_KB) {
357
+ console.warn(`User message ${idx + 1} is ${messageSizeBytes} bytes (${messageSizeKb}kb), which exceeds warning limit`);
358
+ }
359
+ }
360
+ let content = `# User Input ${idx + 1}\n\n`;
361
+ content += `**ResultId**: ${String(signalId)}\n\n`;
362
+ content += message.content;
363
+ content += "\n";
364
+ await Markdown.writeData("outline", content, {
365
+ path: subfolderPath,
366
+ file: contentFileName,
367
+ signalId: String(signalId),
368
+ symbol: "",
369
+ strategyName: "",
370
+ exchangeName: "",
371
+ frameName: ""
372
+ });
373
+ }));
374
+ }
375
+ // Generate LLM output
376
+ {
377
+ const messageNum = String(userMessages.length + 1).padStart(2, "0");
378
+ const contentFileName = `${messageNum}_llm_output.md`;
379
+ let content = "# Full Outline Result\n\n";
380
+ content += `**ResultId**: ${String(signalId)}\n\n`;
381
+ if (signal) {
382
+ content += "## Output Data\n\n";
383
+ content += "```json\n";
384
+ content += JSON.stringify(signal, null, 2);
385
+ content += "\n```\n";
386
+ }
387
+ await Markdown.writeData("outline", content, {
388
+ path: subfolderPath,
389
+ file: contentFileName,
390
+ symbol: "",
391
+ signalId: String(signalId),
392
+ strategyName: "",
393
+ exchangeName: "",
394
+ frameName: ""
395
+ });
396
+ }
397
+ };
398
+ /**
399
+ * Service for generating markdown documentation from LLM outline results.
400
+ * Used by AI Strategy Optimizer to save debug logs and conversation history.
401
+ *
402
+ * Creates directory structure:
403
+ * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
404
+ * - ./dump/strategy/{signalId}/01_user_message.md - First user input
405
+ * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
406
+ * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
407
+ */
408
+ class OutlineMarkdownService {
409
+ constructor() {
410
+ /** Logger service injected via DI */
411
+ this.loggerService = inject(TYPES.loggerService);
412
+ /**
413
+ * Dumps signal data and conversation history to markdown files.
414
+ * Skips if directory already exists to avoid overwriting previous results.
415
+ *
416
+ * Generated files:
417
+ * - 00_system_prompt.md - System messages and output summary
418
+ * - XX_user_message.md - Each user message in separate file (numbered)
419
+ * - XX_llm_output.md - Final LLM output with signal data
420
+ *
421
+ * @param signalId - Unique identifier for the result (used as directory name)
422
+ * @param history - Array of message models from LLM conversation
423
+ * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
424
+ * @param outputDir - Output directory path (default: "./dump/strategy")
425
+ * @returns Promise that resolves when all files are written
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * await outlineService.dumpSignal(
430
+ * "strategy-1",
431
+ * conversationHistory,
432
+ * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
433
+ * );
434
+ * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
435
+ * // ./dump/strategy/strategy-1/01_user_message.md
436
+ * // ./dump/strategy/strategy-1/02_llm_output.md
437
+ * ```
438
+ */
439
+ this.dumpSignal = async (signalId, history, signal, outputDir = "./dump/strategy") => {
440
+ this.loggerService.log("outlineMarkdownService dumpSignal", {
441
+ signalId,
442
+ history,
443
+ signal,
444
+ outputDir,
445
+ });
446
+ return await DUMP_SIGNAL_FN(signalId, history, signal, outputDir);
447
+ };
448
+ }
449
+ }
450
+
276
451
  /**
277
452
  * Enumeration of supported JSON schema outlines.
278
453
  *
@@ -590,6 +765,111 @@ class RunnerPrivateService {
590
765
  }
591
766
  }
592
767
 
768
+ const require = createRequire(import.meta.url);
769
+ /**
770
+ * Default fallback prompt configuration.
771
+ * Used when signal.prompt.cjs file is not found.
772
+ */
773
+ const DEFAULT_PROMPT = {
774
+ user: "",
775
+ system: [],
776
+ };
777
+ /**
778
+ * Lazy-loads and caches signal prompt configuration.
779
+ * Attempts to load from config/prompt/signal.prompt.cjs, falls back to DEFAULT_PROMPT if not found.
780
+ * Uses singleshot pattern to ensure configuration is loaded only once.
781
+ * @returns Prompt configuration with system and user prompts
782
+ */
783
+ const GET_PROMPT_FN = singleshot(() => {
784
+ try {
785
+ const modulePath = require.resolve(path.join(process.cwd(), `./config/prompt/signal.prompt.cjs`));
786
+ console.log(`Using ${modulePath} implementation as signal.prompt.cjs`);
787
+ return require(modulePath);
788
+ }
789
+ catch (error) {
790
+ console.log(`Using empty fallback for signal.prompt.cjs`, error);
791
+ return DEFAULT_PROMPT;
792
+ }
793
+ });
794
+ /**
795
+ * Service for managing signal prompts for AI/LLM integrations.
796
+ *
797
+ * Provides access to system and user prompts configured in signal.prompt.cjs.
798
+ * Supports both static prompt arrays and dynamic prompt functions.
799
+ *
800
+ * Key responsibilities:
801
+ * - Lazy-loads prompt configuration from config/prompt/signal.prompt.cjs
802
+ * - Resolves system prompts (static arrays or async functions)
803
+ * - Provides user prompt strings
804
+ * - Falls back to empty prompts if configuration is missing
805
+ *
806
+ * Used for AI-powered signal analysis and strategy recommendations.
807
+ */
808
+ class SignalPromptService {
809
+ constructor() {
810
+ this.loggerService = inject(TYPES.loggerService);
811
+ /**
812
+ * Retrieves system prompts for AI context.
813
+ *
814
+ * System prompts can be:
815
+ * - Static array of strings (returned directly)
816
+ * - Async/sync function returning string array (executed and awaited)
817
+ * - Undefined (returns empty array)
818
+ *
819
+ * @param symbol - Trading symbol (e.g., "BTCUSDT")
820
+ * @param strategyName - Strategy identifier
821
+ * @param exchangeName - Exchange identifier
822
+ * @param frameName - Timeframe identifier
823
+ * @param backtest - Whether running in backtest mode
824
+ * @returns Promise resolving to array of system prompt strings
825
+ */
826
+ this.getSystemPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
827
+ this.loggerService.log("signalPromptService getSystemPrompt", {
828
+ symbol,
829
+ strategyName,
830
+ exchangeName,
831
+ frameName,
832
+ backtest,
833
+ });
834
+ const { system } = GET_PROMPT_FN();
835
+ if (Array.isArray(system)) {
836
+ return system;
837
+ }
838
+ if (typeof system === "function") {
839
+ return await system(symbol, strategyName, exchangeName, frameName, backtest);
840
+ }
841
+ return [];
842
+ };
843
+ /**
844
+ * Retrieves user prompt string for AI input.
845
+ *
846
+ * @param symbol - Trading symbol (e.g., "BTCUSDT")
847
+ * @param strategyName - Strategy identifier
848
+ * @param exchangeName - Exchange identifier
849
+ * @param frameName - Timeframe identifier
850
+ * @param backtest - Whether running in backtest mode
851
+ * @returns Promise resolving to user prompt string
852
+ */
853
+ this.getUserPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
854
+ this.loggerService.log("signalPromptService getUserPrompt", {
855
+ symbol,
856
+ strategyName,
857
+ exchangeName,
858
+ frameName,
859
+ backtest,
860
+ });
861
+ const { user } = GET_PROMPT_FN();
862
+ if (typeof user === "string") {
863
+ return user;
864
+ }
865
+ if (typeof user === "function") {
866
+ return await user(symbol, strategyName, exchangeName, frameName, backtest);
867
+ }
868
+ return "";
869
+ };
870
+ }
871
+ }
872
+
593
873
  /**
594
874
  * Public-facing service for structured AI outline completions.
595
875
  *
@@ -717,98 +997,1456 @@ class OutlinePublicService {
717
997
  * }, context);
718
998
  * ```
719
999
  */
720
- class RunnerPublicService {
1000
+ class RunnerPublicService {
1001
+ constructor() {
1002
+ /** Private service handling AI provider operations */
1003
+ this.runnerPrivateService = inject(TYPES.runnerPrivateService);
1004
+ /** Logger service for operation tracking */
1005
+ this.loggerService = inject(TYPES.loggerService);
1006
+ /**
1007
+ * Executes a standard AI completion within the specified context.
1008
+ *
1009
+ * @param params - Completion parameters including messages and options
1010
+ * @param context - Execution context with inference provider, model, and API key
1011
+ * @returns Promise resolving to AI response message
1012
+ *
1013
+ * @example
1014
+ * ```typescript
1015
+ * const result = await runnerPublicService.getCompletion({
1016
+ * messages: [
1017
+ * { role: "system", content: "You are a trading analyst" },
1018
+ * { role: "user", content: "Analyze BTC/USDT" }
1019
+ * ]
1020
+ * }, {
1021
+ * inference: InferenceName.ClaudeInference,
1022
+ * model: "claude-3-5-sonnet-20240620",
1023
+ * apiKey: "sk-ant-..."
1024
+ * });
1025
+ * ```
1026
+ */
1027
+ this.getCompletion = async (params, context) => {
1028
+ this.loggerService.log("runnerPublicService getCompletion");
1029
+ return await ContextService.runInContext(async () => {
1030
+ return await this.runnerPrivateService.getCompletion(params);
1031
+ }, context);
1032
+ };
1033
+ /**
1034
+ * Executes a streaming AI completion within the specified context.
1035
+ *
1036
+ * Similar to getCompletion but enables streaming mode where supported by the provider.
1037
+ * The response is accumulated and returned as a complete message once streaming finishes.
1038
+ *
1039
+ * @param params - Completion parameters including messages and options
1040
+ * @param context - Execution context with inference provider, model, and API key
1041
+ * @returns Promise resolving to accumulated AI response message
1042
+ *
1043
+ * @example
1044
+ * ```typescript
1045
+ * const result = await runnerPublicService.getStreamCompletion({
1046
+ * messages: [
1047
+ * { role: "user", content: "Generate trading signal for ETH/USDT" }
1048
+ * ]
1049
+ * }, {
1050
+ * inference: InferenceName.GPT5Inference,
1051
+ * model: "gpt-5o-mini",
1052
+ * apiKey: "sk-..."
1053
+ * });
1054
+ * ```
1055
+ */
1056
+ this.getStreamCompletion = async (params, context) => {
1057
+ this.loggerService.log("runnerPublicService getStreamCompletion");
1058
+ return await ContextService.runInContext(async () => {
1059
+ return await this.runnerPrivateService.getStreamCompletion(params);
1060
+ }, context);
1061
+ };
1062
+ /**
1063
+ * Executes a structured outline completion within the specified context.
1064
+ *
1065
+ * Uses structured output (JSON schema validation) to ensure the AI response
1066
+ * conforms to a predefined format. Ideal for extracting structured data
1067
+ * from AI responses (e.g., trading signals with specific fields).
1068
+ *
1069
+ * @param params - Outline completion parameters including messages and schema
1070
+ * @param context - Execution context with inference provider, model, and API key
1071
+ * @returns Promise resolving to structured AI response
1072
+ *
1073
+ * @example
1074
+ * ```typescript
1075
+ * const signal = await runnerPublicService.getOutlineCompletion({
1076
+ * messages: [
1077
+ * { role: "user", content: "Decide position for BTC/USDT" }
1078
+ * ]
1079
+ * }, {
1080
+ * inference: InferenceName.DeepseekInference,
1081
+ * model: "deepseek-chat",
1082
+ * apiKey: "sk-..."
1083
+ * });
1084
+ * // Returns: { position: "long", price_open: 50000, ... }
1085
+ * ```
1086
+ */
1087
+ this.getOutlineCompletion = async (params, context) => {
1088
+ this.loggerService.log("runnerPublicService getOutlineCompletion");
1089
+ return await ContextService.runInContext(async () => {
1090
+ return await this.runnerPrivateService.getOutlineCompletion(params);
1091
+ }, context);
1092
+ };
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Default template service for generating optimizer code snippets.
1098
+ * Implements all IOptimizerTemplate methods with Ollama LLM integration.
1099
+ *
1100
+ * Features:
1101
+ * - Multi-timeframe analysis (1m, 5m, 15m, 1h)
1102
+ * - JSON structured output for signals
1103
+ * - Debug logging to ./dump/strategy
1104
+ * - CCXT exchange integration
1105
+ * - Walker-based strategy comparison
1106
+ *
1107
+ * Can be partially overridden in optimizer schema configuration.
1108
+ */
1109
+ class OptimizerTemplateService {
1110
+ constructor() {
1111
+ this.loggerService = inject(TYPES.loggerService);
1112
+ /**
1113
+ * Generates the top banner with imports and constants.
1114
+ *
1115
+ * @param symbol - Trading pair symbol
1116
+ * @returns Shebang, imports, and WARN_KB constant
1117
+ */
1118
+ this.getTopBanner = async (symbol) => {
1119
+ this.loggerService.log("optimizerTemplateService getTopBanner", {
1120
+ symbol,
1121
+ });
1122
+ return [
1123
+ "#!/usr/bin/env node",
1124
+ "",
1125
+ `import { Ollama } from "ollama";`,
1126
+ `import ccxt from "ccxt";`,
1127
+ `import {`,
1128
+ ` addExchangeSchema,`,
1129
+ ` addStrategySchema,`,
1130
+ ` addFrameSchema,`,
1131
+ ` addWalkerSchema,`,
1132
+ ` Walker,`,
1133
+ ` Backtest,`,
1134
+ ` getCandles,`,
1135
+ ` listenSignalBacktest,`,
1136
+ ` listenWalkerComplete,`,
1137
+ ` listenDoneBacktest,`,
1138
+ ` listenBacktestProgress,`,
1139
+ ` listenWalkerProgress,`,
1140
+ ` listenError,`,
1141
+ ` Markdown,`,
1142
+ `} from "backtest-kit";`,
1143
+ `import { promises as fs } from "fs";`,
1144
+ `import { v4 as uuid } from "uuid";`,
1145
+ `import path from "path";`,
1146
+ ``,
1147
+ `const WARN_KB = 100;`,
1148
+ ``,
1149
+ `Markdown.enable()`,
1150
+ ].join("\n");
1151
+ };
1152
+ /**
1153
+ * Generates default user message for LLM conversation.
1154
+ * Simple prompt to read and acknowledge data.
1155
+ *
1156
+ * @param symbol - Trading pair symbol
1157
+ * @param data - Fetched data array
1158
+ * @param name - Source name
1159
+ * @returns User message with JSON data
1160
+ */
1161
+ this.getUserMessage = async (symbol, data, name) => {
1162
+ this.loggerService.log("optimizerTemplateService getUserMessage", {
1163
+ symbol,
1164
+ data,
1165
+ name,
1166
+ });
1167
+ return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
1168
+ };
1169
+ /**
1170
+ * Generates default assistant message for LLM conversation.
1171
+ * Simple acknowledgment response.
1172
+ *
1173
+ * @param symbol - Trading pair symbol
1174
+ * @param data - Fetched data array
1175
+ * @param name - Source name
1176
+ * @returns Assistant acknowledgment message
1177
+ */
1178
+ this.getAssistantMessage = async (symbol, data, name) => {
1179
+ this.loggerService.log("optimizerTemplateService getAssistantMessage", {
1180
+ symbol,
1181
+ data,
1182
+ name,
1183
+ });
1184
+ return "ОК";
1185
+ };
1186
+ /**
1187
+ * Generates Walker configuration code.
1188
+ * Compares multiple strategies on test frame.
1189
+ *
1190
+ * @param walkerName - Unique walker identifier
1191
+ * @param exchangeName - Exchange to use for backtesting
1192
+ * @param frameName - Test frame name
1193
+ * @param strategies - Array of strategy names to compare
1194
+ * @returns Generated addWalker() call
1195
+ */
1196
+ this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
1197
+ this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
1198
+ walkerName,
1199
+ exchangeName,
1200
+ frameName,
1201
+ strategies,
1202
+ });
1203
+ // Escape special characters to prevent code injection
1204
+ const escapedWalkerName = String(walkerName)
1205
+ .replace(/\\/g, '\\\\')
1206
+ .replace(/"/g, '\\"');
1207
+ const escapedExchangeName = String(exchangeName)
1208
+ .replace(/\\/g, '\\\\')
1209
+ .replace(/"/g, '\\"');
1210
+ const escapedFrameName = String(frameName)
1211
+ .replace(/\\/g, '\\\\')
1212
+ .replace(/"/g, '\\"');
1213
+ const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
1214
+ return [
1215
+ `addWalkerSchema({`,
1216
+ ` walkerName: "${escapedWalkerName}",`,
1217
+ ` exchangeName: "${escapedExchangeName}",`,
1218
+ ` frameName: "${escapedFrameName}",`,
1219
+ ` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
1220
+ `});`
1221
+ ].join("\n");
1222
+ };
1223
+ /**
1224
+ * Generates Strategy configuration with LLM integration.
1225
+ * Includes multi-timeframe analysis and signal generation.
1226
+ *
1227
+ * @param strategyName - Unique strategy identifier
1228
+ * @param interval - Signal throttling interval (e.g., "5m")
1229
+ * @param prompt - Strategy logic from getPrompt()
1230
+ * @returns Generated addStrategy() call with getSignal() function
1231
+ */
1232
+ this.getStrategyTemplate = async (strategyName, interval, prompt) => {
1233
+ this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
1234
+ strategyName,
1235
+ interval,
1236
+ prompt,
1237
+ });
1238
+ // Convert prompt to plain text first
1239
+ const plainPrompt = await toPlainString(prompt);
1240
+ // Escape special characters to prevent code injection
1241
+ const escapedStrategyName = String(strategyName)
1242
+ .replace(/\\/g, '\\\\')
1243
+ .replace(/"/g, '\\"');
1244
+ const escapedInterval = String(interval)
1245
+ .replace(/\\/g, '\\\\')
1246
+ .replace(/"/g, '\\"');
1247
+ const escapedPrompt = String(plainPrompt)
1248
+ .replace(/\\/g, '\\\\')
1249
+ .replace(/`/g, '\\`')
1250
+ .replace(/\$/g, '\\$');
1251
+ return [
1252
+ `addStrategySchema({`,
1253
+ ` strategyName: "${escapedStrategyName}",`,
1254
+ ` interval: "${escapedInterval}",`,
1255
+ ` getSignal: async (symbol) => {`,
1256
+ ` const messages = [];`,
1257
+ ``,
1258
+ ` // Загружаем данные всех таймфреймов`,
1259
+ ` const microTermCandles = await getCandles(symbol, "1m", 30);`,
1260
+ ` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
1261
+ ` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
1262
+ ` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
1263
+ ``,
1264
+ ` function formatCandles(candles, timeframe) {`,
1265
+ ` return candles.map((c) =>`,
1266
+ ` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
1267
+ ` ).join("\\n");`,
1268
+ ` }`,
1269
+ ``,
1270
+ ` // Сообщение 1: Среднесрочный тренд`,
1271
+ ` messages.push(`,
1272
+ ` {`,
1273
+ ` role: "user",`,
1274
+ ` content: [`,
1275
+ ` \`\${symbol}\`,`,
1276
+ ` "Проанализируй свечи 1h:",`,
1277
+ ` "",`,
1278
+ ` formatCandles(mediumTermCandles, "1h")`,
1279
+ ` ].join("\\n"),`,
1280
+ ` },`,
1281
+ ` {`,
1282
+ ` role: "assistant",`,
1283
+ ` content: "Тренд 1h проанализирован",`,
1284
+ ` }`,
1285
+ ` );`,
1286
+ ``,
1287
+ ` // Сообщение 2: Краткосрочный тренд`,
1288
+ ` messages.push(`,
1289
+ ` {`,
1290
+ ` role: "user",`,
1291
+ ` content: [`,
1292
+ ` "Проанализируй свечи 15m:",`,
1293
+ ` "",`,
1294
+ ` formatCandles(shortTermCandles, "15m")`,
1295
+ ` ].join("\\n"),`,
1296
+ ` },`,
1297
+ ` {`,
1298
+ ` role: "assistant",`,
1299
+ ` content: "Тренд 15m проанализирован",`,
1300
+ ` }`,
1301
+ ` );`,
1302
+ ``,
1303
+ ` // Сообщение 3: Основной таймфрейм`,
1304
+ ` messages.push(`,
1305
+ ` {`,
1306
+ ` role: "user",`,
1307
+ ` content: [`,
1308
+ ` "Проанализируй свечи 5m:",`,
1309
+ ` "",`,
1310
+ ` formatCandles(mainTermCandles, "5m")`,
1311
+ ` ].join("\\n")`,
1312
+ ` },`,
1313
+ ` {`,
1314
+ ` role: "assistant",`,
1315
+ ` content: "Таймфрейм 5m проанализирован",`,
1316
+ ` }`,
1317
+ ` );`,
1318
+ ``,
1319
+ ` // Сообщение 4: Микро-структура`,
1320
+ ` messages.push(`,
1321
+ ` {`,
1322
+ ` role: "user",`,
1323
+ ` content: [`,
1324
+ ` "Проанализируй свечи 1m:",`,
1325
+ ` "",`,
1326
+ ` formatCandles(microTermCandles, "1m")`,
1327
+ ` ].join("\\n")`,
1328
+ ` },`,
1329
+ ` {`,
1330
+ ` role: "assistant",`,
1331
+ ` content: "Микроструктура 1m проанализирована",`,
1332
+ ` }`,
1333
+ ` );`,
1334
+ ``,
1335
+ ` // Сообщение 5: Запрос сигнала`,
1336
+ ` messages.push(`,
1337
+ ` {`,
1338
+ ` role: "user",`,
1339
+ ` content: [`,
1340
+ ` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
1341
+ ` "",`,
1342
+ ` \`${escapedPrompt}\`,`,
1343
+ ` "",`,
1344
+ ` "Если сигналы противоречивы или тренд слабый то position: wait"`,
1345
+ ` ].join("\\n"),`,
1346
+ ` }`,
1347
+ ` );`,
1348
+ ``,
1349
+ ` const resultId = uuid();`,
1350
+ ``,
1351
+ ` const result = await json(messages);`,
1352
+ ``,
1353
+ ` await dumpJson(resultId, messages, result);`,
1354
+ ``,
1355
+ ` result.id = resultId;`,
1356
+ ``,
1357
+ ` return result;`,
1358
+ ` },`,
1359
+ `});`
1360
+ ].join("\n");
1361
+ };
1362
+ /**
1363
+ * Generates Exchange configuration code.
1364
+ * Uses CCXT Binance with standard formatters.
1365
+ *
1366
+ * @param symbol - Trading pair symbol (unused, for consistency)
1367
+ * @param exchangeName - Unique exchange identifier
1368
+ * @returns Generated addExchange() call with CCXT integration
1369
+ */
1370
+ this.getExchangeTemplate = async (symbol, exchangeName) => {
1371
+ this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
1372
+ exchangeName,
1373
+ symbol,
1374
+ });
1375
+ // Escape special characters to prevent code injection
1376
+ const escapedExchangeName = String(exchangeName)
1377
+ .replace(/\\/g, '\\\\')
1378
+ .replace(/"/g, '\\"');
1379
+ return [
1380
+ `addExchangeSchema({`,
1381
+ ` exchangeName: "${escapedExchangeName}",`,
1382
+ ` getCandles: async (symbol, interval, since, limit) => {`,
1383
+ ` const exchange = new ccxt.binance();`,
1384
+ ` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
1385
+ ` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
1386
+ ` timestamp, open, high, low, close, volume`,
1387
+ ` }));`,
1388
+ ` },`,
1389
+ ` formatPrice: async (symbol, price) => price.toFixed(2),`,
1390
+ ` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
1391
+ `});`
1392
+ ].join("\n");
1393
+ };
1394
+ /**
1395
+ * Generates Frame (timeframe) configuration code.
1396
+ *
1397
+ * @param symbol - Trading pair symbol (unused, for consistency)
1398
+ * @param frameName - Unique frame identifier
1399
+ * @param interval - Candle interval (e.g., "1m")
1400
+ * @param startDate - Frame start date
1401
+ * @param endDate - Frame end date
1402
+ * @returns Generated addFrame() call
1403
+ */
1404
+ this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
1405
+ this.loggerService.log("optimizerTemplateService getFrameTemplate", {
1406
+ symbol,
1407
+ frameName,
1408
+ interval,
1409
+ startDate,
1410
+ endDate,
1411
+ });
1412
+ // Escape special characters to prevent code injection
1413
+ const escapedFrameName = String(frameName)
1414
+ .replace(/\\/g, '\\\\')
1415
+ .replace(/"/g, '\\"');
1416
+ const escapedInterval = String(interval)
1417
+ .replace(/\\/g, '\\\\')
1418
+ .replace(/"/g, '\\"');
1419
+ return [
1420
+ `addFrameSchema({`,
1421
+ ` frameName: "${escapedFrameName}",`,
1422
+ ` interval: "${escapedInterval}",`,
1423
+ ` startDate: new Date("${startDate.toISOString()}"),`,
1424
+ ` endDate: new Date("${endDate.toISOString()}"),`,
1425
+ `});`
1426
+ ].join("\n");
1427
+ };
1428
+ /**
1429
+ * Generates launcher code to run Walker with event listeners.
1430
+ * Includes progress tracking and completion handlers.
1431
+ *
1432
+ * @param symbol - Trading pair symbol
1433
+ * @param walkerName - Walker name to launch
1434
+ * @returns Generated Walker.background() call with listeners
1435
+ */
1436
+ this.getLauncherTemplate = async (symbol, walkerName) => {
1437
+ this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
1438
+ symbol,
1439
+ walkerName,
1440
+ });
1441
+ // Escape special characters to prevent code injection
1442
+ const escapedSymbol = String(symbol)
1443
+ .replace(/\\/g, '\\\\')
1444
+ .replace(/"/g, '\\"');
1445
+ const escapedWalkerName = String(walkerName)
1446
+ .replace(/\\/g, '\\\\')
1447
+ .replace(/"/g, '\\"');
1448
+ return [
1449
+ `Walker.background("${escapedSymbol}", {`,
1450
+ ` walkerName: "${escapedWalkerName}"`,
1451
+ `});`,
1452
+ ``,
1453
+ `listenSignalBacktest((event) => {`,
1454
+ ` console.log(event);`,
1455
+ `});`,
1456
+ ``,
1457
+ `listenBacktestProgress((event) => {`,
1458
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
1459
+ ` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
1460
+ `});`,
1461
+ ``,
1462
+ `listenWalkerProgress((event) => {`,
1463
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
1464
+ ` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
1465
+ ` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
1466
+ `});`,
1467
+ ``,
1468
+ `listenWalkerComplete((results) => {`,
1469
+ ` console.log("Walker completed:", results.bestStrategy);`,
1470
+ ` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
1471
+ `});`,
1472
+ ``,
1473
+ `listenDoneBacktest((event) => {`,
1474
+ ` console.log("Backtest completed:", event.symbol);`,
1475
+ ` Backtest.dump(event.symbol, {`,
1476
+ ` strategyName: event.strategyName,`,
1477
+ ` exchangeName: event.exchangeName,`,
1478
+ ` frameName: event.frameName`,
1479
+ ` });`,
1480
+ `});`,
1481
+ ``,
1482
+ `listenError((error) => {`,
1483
+ ` console.error("Error occurred:", error);`,
1484
+ `});`
1485
+ ].join("\n");
1486
+ };
1487
+ /**
1488
+ * Generates dumpJson() helper function for debug output.
1489
+ * Saves LLM conversations and results to ./dump/strategy/{resultId}/
1490
+ *
1491
+ * @param symbol - Trading pair symbol (unused, for consistency)
1492
+ * @returns Generated async dumpJson() function
1493
+ */
1494
+ this.getJsonDumpTemplate = async (symbol) => {
1495
+ this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
1496
+ symbol,
1497
+ });
1498
+ return [
1499
+ `async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
1500
+ ` // Extract system messages and system reminders from existing data`,
1501
+ ` const systemMessages = history.filter((m) => m.role === "system");`,
1502
+ ` const userMessages = history.filter((m) => m.role === "user");`,
1503
+ ` const subfolderPath = path.join(outputDir, resultId);`,
1504
+ ``,
1505
+ ` try {`,
1506
+ ` await fs.access(subfolderPath);`,
1507
+ ` return;`,
1508
+ ` } catch {`,
1509
+ ` await fs.mkdir(subfolderPath, { recursive: true });`,
1510
+ ` }`,
1511
+ ``,
1512
+ ` {`,
1513
+ ` let summary = "# Outline Result Summary\\n\\n";`,
1514
+ ``,
1515
+ ` {`,
1516
+ ` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
1517
+ ` }`,
1518
+ ``,
1519
+ ` if (result) {`,
1520
+ ` summary += "## Output Data\\n\\n";`,
1521
+ ` summary += "\`\`\`json\\n";`,
1522
+ ` summary += JSON.stringify(result, null, 2);`,
1523
+ ` summary += "\\n\`\`\`\\n\\n";`,
1524
+ ` }`,
1525
+ ``,
1526
+ ` // Add system messages to summary`,
1527
+ ` if (systemMessages.length > 0) {`,
1528
+ ` summary += "## System Messages\\n\\n";`,
1529
+ ` systemMessages.forEach((msg, idx) => {`,
1530
+ ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
1531
+ ` summary += msg.content;`,
1532
+ ` summary += "\\n\\n";`,
1533
+ ` });`,
1534
+ ` }`,
1535
+ ``,
1536
+ ` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
1537
+ ` await fs.writeFile(summaryFile, summary, "utf8");`,
1538
+ ` }`,
1539
+ ``,
1540
+ ` {`,
1541
+ ` await Promise.all(`,
1542
+ ` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
1543
+ ` const messageNum = String(idx + 1).padStart(2, "0");`,
1544
+ ` const contentFileName = \`\${messageNum}_user_message.md\`;`,
1545
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
1546
+ ``,
1547
+ ` {`,
1548
+ ` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
1549
+ ` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
1550
+ ` if (messageSizeKb > WARN_KB) {`,
1551
+ ` console.warn(`,
1552
+ ` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
1553
+ ` );`,
1554
+ ` }`,
1555
+ ` }`,
1556
+ ``,
1557
+ ` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
1558
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
1559
+ ` content += message.content;`,
1560
+ ` content += "\\n";`,
1561
+ ``,
1562
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
1563
+ ` })`,
1564
+ ` );`,
1565
+ ` }`,
1566
+ ``,
1567
+ ` {`,
1568
+ ` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
1569
+ ` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
1570
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
1571
+ ``,
1572
+ ` let content = "# Full Outline Result\\n\\n";`,
1573
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
1574
+ ``,
1575
+ ` if (result) {`,
1576
+ ` content += "## Output Data\\n\\n";`,
1577
+ ` content += "\`\`\`json\\n";`,
1578
+ ` content += JSON.stringify(result, null, 2);`,
1579
+ ` content += "\\n\`\`\`\\n";`,
1580
+ ` }`,
1581
+ ``,
1582
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
1583
+ ` }`,
1584
+ `}`
1585
+ ].join("\n");
1586
+ };
1587
+ /**
1588
+ * Generates text() helper for LLM text generation.
1589
+ * Uses Ollama deepseek-v3.1:671b model for market analysis.
1590
+ *
1591
+ * @param symbol - Trading pair symbol (used in prompt)
1592
+ * @returns Generated async text() function
1593
+ */
1594
+ this.getTextTemplate = async (symbol) => {
1595
+ this.loggerService.log("optimizerTemplateService getTextTemplate", {
1596
+ symbol,
1597
+ });
1598
+ // Escape special characters in symbol to prevent code injection
1599
+ const escapedSymbol = String(symbol)
1600
+ .replace(/\\/g, '\\\\')
1601
+ .replace(/`/g, '\\`')
1602
+ .replace(/\$/g, '\\$')
1603
+ .toUpperCase();
1604
+ return [
1605
+ `async function text(messages) {`,
1606
+ ` const ollama = new Ollama({`,
1607
+ ` host: "https://ollama.com",`,
1608
+ ` headers: {`,
1609
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
1610
+ ` },`,
1611
+ ` });`,
1612
+ ``,
1613
+ ` const response = await ollama.chat({`,
1614
+ ` model: "deepseek-v3.1:671b",`,
1615
+ ` messages: [`,
1616
+ ` {`,
1617
+ ` role: "system",`,
1618
+ ` content: [`,
1619
+ ` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
1620
+ ` "только отчёт готовый для копипасты целиком",`,
1621
+ ` "",`,
1622
+ ` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
1623
+ ` ].join("\\n"),`,
1624
+ ` },`,
1625
+ ` ...messages,`,
1626
+ ` {`,
1627
+ ` role: "user",`,
1628
+ ` content: [`,
1629
+ ` "На каких условиях мне купить ${escapedSymbol}?",`,
1630
+ ` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
1631
+ ` "Какой RR ставить для позиций?",`,
1632
+ ` "Предпочтительны LONG или SHORT позиции?",`,
1633
+ ` "",`,
1634
+ ` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
1635
+ ` ].join("\\n")`,
1636
+ ` }`,
1637
+ ` ]`,
1638
+ ` });`,
1639
+ ``,
1640
+ ` const content = response.message.content.trim();`,
1641
+ ` return content`,
1642
+ ` .replace(/\\\\/g, '\\\\\\\\')`,
1643
+ ` .replace(/\`/g, '\\\\\`')`,
1644
+ ` .replace(/\\$/g, '\\\\$')`,
1645
+ ` .replace(/"/g, '\\\\"')`,
1646
+ ` .replace(/'/g, "\\\\'");`,
1647
+ `}`
1648
+ ].join("\n");
1649
+ };
1650
+ /**
1651
+ * Generates json() helper for structured LLM output.
1652
+ * Uses Ollama with JSON schema for trading signals.
1653
+ *
1654
+ * Signal schema:
1655
+ * - position: "wait" | "long" | "short"
1656
+ * - note: strategy explanation
1657
+ * - priceOpen: entry price
1658
+ * - priceTakeProfit: target price
1659
+ * - priceStopLoss: stop price
1660
+ * - minuteEstimatedTime: expected duration (max 360 min)
1661
+ *
1662
+ * @param symbol - Trading pair symbol (unused, for consistency)
1663
+ * @returns Generated async json() function with signal schema
1664
+ */
1665
+ this.getJsonTemplate = async (symbol) => {
1666
+ this.loggerService.log("optimizerTemplateService getJsonTemplate", {
1667
+ symbol,
1668
+ });
1669
+ return [
1670
+ `async function json(messages) {`,
1671
+ ` const ollama = new Ollama({`,
1672
+ ` host: "https://ollama.com",`,
1673
+ ` headers: {`,
1674
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
1675
+ ` },`,
1676
+ ` });`,
1677
+ ``,
1678
+ ` const response = await ollama.chat({`,
1679
+ ` model: "deepseek-v3.1:671b",`,
1680
+ ` messages: [`,
1681
+ ` {`,
1682
+ ` role: "system",`,
1683
+ ` content: [`,
1684
+ ` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
1685
+ ` "",`,
1686
+ ` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
1687
+ ` "",`,
1688
+ ` "1. ТИПЫ ПОЗИЦИЙ:",`,
1689
+ ` " - position='wait': нет четкого сигнала, жди лучших условий",`,
1690
+ ` " - position='long': бычий сигнал, цена будет расти",`,
1691
+ ` " - position='short': медвежий сигнал, цена будет падать",`,
1692
+ ` "",`,
1693
+ ` "2. ЦЕНА ВХОДА (priceOpen):",`,
1694
+ ` " - Может быть текущей рыночной ценой для немедленного входа",`,
1695
+ ` " - Может быть отложенной ценой для входа при достижении уровня",`,
1696
+ ` " - Укажи оптимальную цену входа согласно технического анализа",`,
1697
+ ` "",`,
1698
+ ` "3. УРОВНИ ВЫХОДА:",`,
1699
+ ` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
1700
+ ` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
1701
+ ` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
1702
+ ` "",`,
1703
+ ` "4. ВРЕМЕННЫЕ РАМКИ:",`,
1704
+ ` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
1705
+ ` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
1706
+ ` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
1707
+ ` ].join("\\n"),`,
1708
+ ` },`,
1709
+ ` ...messages,`,
1710
+ ` ],`,
1711
+ ` format: {`,
1712
+ ` type: "object",`,
1713
+ ` properties: {`,
1714
+ ` position: {`,
1715
+ ` type: "string",`,
1716
+ ` enum: ["wait", "long", "short"],`,
1717
+ ` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
1718
+ ` },`,
1719
+ ` note: {`,
1720
+ ` type: "string",`,
1721
+ ` description: "Professional trading recommendation with price levels",`,
1722
+ ` },`,
1723
+ ` priceOpen: {`,
1724
+ ` type: "number",`,
1725
+ ` description: "Entry price (current market price or limit order price)",`,
1726
+ ` },`,
1727
+ ` priceTakeProfit: {`,
1728
+ ` type: "number",`,
1729
+ ` description: "Take profit target price",`,
1730
+ ` },`,
1731
+ ` priceStopLoss: {`,
1732
+ ` type: "number",`,
1733
+ ` description: "Stop loss exit price",`,
1734
+ ` },`,
1735
+ ` minuteEstimatedTime: {`,
1736
+ ` type: "number",`,
1737
+ ` description: "Expected time to reach TP in minutes (max 360)",`,
1738
+ ` },`,
1739
+ ` },`,
1740
+ ` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
1741
+ ` },`,
1742
+ ` });`,
1743
+ ``,
1744
+ ` const jsonResponse = JSON.parse(response.message.content.trim());`,
1745
+ ` return jsonResponse;`,
1746
+ `}`
1747
+ ].join("\n");
1748
+ };
1749
+ }
1750
+ }
1751
+
1752
+ /**
1753
+ * Service for managing optimizer schema registration and retrieval.
1754
+ * Provides validation and registry management for optimizer configurations.
1755
+ *
1756
+ * Uses ToolRegistry for immutable schema storage.
1757
+ */
1758
+ class OptimizerSchemaService {
1759
+ constructor() {
1760
+ this.loggerService = inject(TYPES.loggerService);
1761
+ this._registry = new ToolRegistry("optimizerSchema");
1762
+ /**
1763
+ * Registers a new optimizer schema.
1764
+ * Validates required fields before registration.
1765
+ *
1766
+ * @param key - Unique optimizer name
1767
+ * @param value - Optimizer schema configuration
1768
+ * @throws Error if schema validation fails
1769
+ */
1770
+ this.register = (key, value) => {
1771
+ this.loggerService.log(`optimizerSchemaService register`, { key });
1772
+ this.validateShallow(value);
1773
+ this._registry = this._registry.register(key, value);
1774
+ };
1775
+ /**
1776
+ * Validates optimizer schema structure.
1777
+ * Checks required fields: optimizerName, rangeTrain, source, getPrompt.
1778
+ *
1779
+ * @param optimizerSchema - Schema to validate
1780
+ * @throws Error if validation fails
1781
+ */
1782
+ this.validateShallow = (optimizerSchema) => {
1783
+ this.loggerService.log(`optimizerTemplateService validateShallow`, {
1784
+ optimizerSchema,
1785
+ });
1786
+ if (typeof optimizerSchema.optimizerName !== "string") {
1787
+ throw new Error(`optimizer template validation failed: missing optimizerName`);
1788
+ }
1789
+ if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
1790
+ throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
1791
+ }
1792
+ if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
1793
+ throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
1794
+ }
1795
+ if (typeof optimizerSchema.getPrompt !== "function") {
1796
+ throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
1797
+ }
1798
+ };
1799
+ /**
1800
+ * Partially overrides an existing optimizer schema.
1801
+ * Merges provided values with existing schema.
1802
+ *
1803
+ * @param key - Optimizer name to override
1804
+ * @param value - Partial schema values to merge
1805
+ * @returns Updated complete schema
1806
+ * @throws Error if optimizer not found
1807
+ */
1808
+ this.override = (key, value) => {
1809
+ this.loggerService.log(`optimizerSchemaService override`, { key });
1810
+ this._registry = this._registry.override(key, value);
1811
+ return this._registry.get(key);
1812
+ };
1813
+ /**
1814
+ * Retrieves optimizer schema by name.
1815
+ *
1816
+ * @param key - Optimizer name
1817
+ * @returns Complete optimizer schema
1818
+ * @throws Error if optimizer not found
1819
+ */
1820
+ this.get = (key) => {
1821
+ this.loggerService.log(`optimizerSchemaService get`, { key });
1822
+ return this._registry.get(key);
1823
+ };
1824
+ }
1825
+ }
1826
+
1827
+ /**
1828
+ * Service for validating optimizer existence and managing optimizer registry.
1829
+ * Maintains a Map of registered optimizers for validation purposes.
1830
+ *
1831
+ * Uses memoization for efficient repeated validation checks.
1832
+ */
1833
+ class OptimizerValidationService {
1834
+ constructor() {
1835
+ this.loggerService = inject(TYPES.loggerService);
1836
+ this._optimizerMap = new Map();
1837
+ /**
1838
+ * Adds optimizer to validation registry.
1839
+ * Prevents duplicate optimizer names.
1840
+ *
1841
+ * @param optimizerName - Unique optimizer identifier
1842
+ * @param optimizerSchema - Complete optimizer schema
1843
+ * @throws Error if optimizer with same name already exists
1844
+ */
1845
+ this.addOptimizer = (optimizerName, optimizerSchema) => {
1846
+ this.loggerService.log("optimizerValidationService addOptimizer", {
1847
+ optimizerName,
1848
+ optimizerSchema,
1849
+ });
1850
+ if (this._optimizerMap.has(optimizerName)) {
1851
+ throw new Error(`optimizer ${optimizerName} already exist`);
1852
+ }
1853
+ this._optimizerMap.set(optimizerName, optimizerSchema);
1854
+ };
1855
+ /**
1856
+ * Validates that optimizer exists in registry.
1857
+ * Memoized for performance on repeated checks.
1858
+ *
1859
+ * @param optimizerName - Optimizer name to validate
1860
+ * @param source - Source method name for error messages
1861
+ * @throws Error if optimizer not found
1862
+ */
1863
+ this.validate = memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
1864
+ this.loggerService.log("optimizerValidationService validate", {
1865
+ optimizerName,
1866
+ source,
1867
+ });
1868
+ const optimizer = this._optimizerMap.get(optimizerName);
1869
+ if (!optimizer) {
1870
+ throw new Error(`optimizer ${optimizerName} not found source=${source}`);
1871
+ }
1872
+ return true;
1873
+ });
1874
+ /**
1875
+ * Lists all registered optimizer schemas.
1876
+ *
1877
+ * @returns Array of all optimizer schemas
1878
+ */
1879
+ this.list = async () => {
1880
+ this.loggerService.log("optimizerValidationService list");
1881
+ return Array.from(this._optimizerMap.values());
1882
+ };
1883
+ }
1884
+ }
1885
+
1886
+ /**
1887
+ * Emitter for optimizer progress events.
1888
+ */
1889
+ const progressOptimizerEmitter = new Subject();
1890
+ /**
1891
+ * Emitter for error events.
1892
+ */
1893
+ const errorEmitter = new Subject();
1894
+
1895
+ const ITERATION_LIMIT = 25;
1896
+ const DEFAULT_SOURCE_NAME = "unknown";
1897
+ const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
1898
+ /**
1899
+ * Wrapper to call onSourceData callback with error handling.
1900
+ * Catches and logs any errors thrown by the user-provided callback.
1901
+ */
1902
+ const CALL_SOURCE_DATA_CALLBACKS_FN = trycatch(async (self, symbol, name, data, startDate, endDate) => {
1903
+ if (self.params.callbacks?.onSourceData) {
1904
+ await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
1905
+ }
1906
+ }, {
1907
+ fallback: (error) => {
1908
+ const message = "ClientOptimizer CALL_SOURCE_DATA_CALLBACKS_FN thrown";
1909
+ const payload = {
1910
+ error: errorData(error),
1911
+ message: getErrorMessage(error),
1912
+ };
1913
+ engine$1.loggerService.warn(message, payload);
1914
+ console.warn(message, payload);
1915
+ errorEmitter.next(error);
1916
+ },
1917
+ });
1918
+ /**
1919
+ * Wrapper to call onData callback with error handling.
1920
+ * Catches and logs any errors thrown by the user-provided callback.
1921
+ */
1922
+ const CALL_DATA_CALLBACKS_FN = trycatch(async (self, symbol, strategyList) => {
1923
+ if (self.params.callbacks?.onData) {
1924
+ await self.params.callbacks.onData(symbol, strategyList);
1925
+ }
1926
+ }, {
1927
+ fallback: (error) => {
1928
+ const message = "ClientOptimizer CALL_DATA_CALLBACKS_FN thrown";
1929
+ const payload = {
1930
+ error: errorData(error),
1931
+ message: getErrorMessage(error),
1932
+ };
1933
+ engine$1.loggerService.warn(message, payload);
1934
+ console.warn(message, payload);
1935
+ errorEmitter.next(error);
1936
+ },
1937
+ });
1938
+ /**
1939
+ * Wrapper to call onCode callback with error handling.
1940
+ * Catches and logs any errors thrown by the user-provided callback.
1941
+ */
1942
+ const CALL_CODE_CALLBACKS_FN = trycatch(async (self, symbol, code) => {
1943
+ if (self.params.callbacks?.onCode) {
1944
+ await self.params.callbacks.onCode(symbol, code);
1945
+ }
1946
+ }, {
1947
+ fallback: (error) => {
1948
+ const message = "ClientOptimizer CALL_CODE_CALLBACKS_FN thrown";
1949
+ const payload = {
1950
+ error: errorData(error),
1951
+ message: getErrorMessage(error),
1952
+ };
1953
+ engine$1.loggerService.warn(message, payload);
1954
+ console.warn(message, payload);
1955
+ errorEmitter.next(error);
1956
+ },
1957
+ });
1958
+ /**
1959
+ * Wrapper to call onDump callback with error handling.
1960
+ * Catches and logs any errors thrown by the user-provided callback.
1961
+ */
1962
+ const CALL_DUMP_CALLBACKS_FN = trycatch(async (self, symbol, filepath) => {
1963
+ if (self.params.callbacks?.onDump) {
1964
+ await self.params.callbacks.onDump(symbol, filepath);
1965
+ }
1966
+ }, {
1967
+ fallback: (error) => {
1968
+ const message = "ClientOptimizer CALL_DUMP_CALLBACKS_FN thrown";
1969
+ const payload = {
1970
+ error: errorData(error),
1971
+ message: getErrorMessage(error),
1972
+ };
1973
+ engine$1.loggerService.warn(message, payload);
1974
+ console.warn(message, payload);
1975
+ errorEmitter.next(error);
1976
+ },
1977
+ });
1978
+ /**
1979
+ * Default user message formatter.
1980
+ * Delegates to template's getUserMessage method.
1981
+ *
1982
+ * @param symbol - Trading pair symbol
1983
+ * @param data - Fetched data array
1984
+ * @param name - Source name
1985
+ * @param self - ClientOptimizer instance
1986
+ * @returns Formatted user message content
1987
+ */
1988
+ const DEFAULT_USER_FN = async (symbol, data, name, self) => {
1989
+ return await self.params.template.getUserMessage(symbol, data, name);
1990
+ };
1991
+ /**
1992
+ * Default assistant message formatter.
1993
+ * Delegates to template's getAssistantMessage method.
1994
+ *
1995
+ * @param symbol - Trading pair symbol
1996
+ * @param data - Fetched data array
1997
+ * @param name - Source name
1998
+ * @param self - ClientOptimizer instance
1999
+ * @returns Formatted assistant message content
2000
+ */
2001
+ const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
2002
+ return await self.params.template.getAssistantMessage(symbol, data, name);
2003
+ };
2004
+ /**
2005
+ * Resolves paginated data from source with deduplication.
2006
+ * Uses iterateDocuments to handle pagination automatically.
2007
+ *
2008
+ * @param fetch - Source fetch function
2009
+ * @param filterData - Filter arguments (symbol, dates)
2010
+ * @returns Deduplicated array of all fetched data
2011
+ */
2012
+ const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
2013
+ const iterator = iterateDocuments({
2014
+ limit: ITERATION_LIMIT,
2015
+ async createRequest({ limit, offset }) {
2016
+ return await fetch({
2017
+ symbol: filterData.symbol,
2018
+ startDate: filterData.startDate,
2019
+ endDate: filterData.endDate,
2020
+ limit,
2021
+ offset,
2022
+ });
2023
+ },
2024
+ });
2025
+ const distinct = distinctDocuments(iterator, (data) => data.id);
2026
+ return await resolveDocuments(distinct);
2027
+ };
2028
+ /**
2029
+ * Collects data from all sources and generates strategy metadata.
2030
+ * Iterates through training ranges, fetches data from each source,
2031
+ * builds LLM conversation history, and generates strategy prompts.
2032
+ *
2033
+ * @param symbol - Trading pair symbol
2034
+ * @param self - ClientOptimizer instance
2035
+ * @returns Array of generated strategies with conversation context
2036
+ */
2037
+ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
2038
+ const strategyList = [];
2039
+ const totalSources = self.params.rangeTrain.length * self.params.source.length;
2040
+ let processedSources = 0;
2041
+ for (const { startDate, endDate } of self.params.rangeTrain) {
2042
+ const messageList = [];
2043
+ for (const source of self.params.source) {
2044
+ // Emit progress event at the start of processing each source
2045
+ await self.onProgress({
2046
+ optimizerName: self.params.optimizerName,
2047
+ symbol,
2048
+ totalSources,
2049
+ processedSources,
2050
+ progress: totalSources > 0 ? processedSources / totalSources : 0,
2051
+ });
2052
+ if (typeof source === "function") {
2053
+ const data = await RESOLVE_PAGINATION_FN(source, {
2054
+ symbol,
2055
+ startDate,
2056
+ endDate,
2057
+ });
2058
+ await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
2059
+ const [userContent, assistantContent] = await Promise.all([
2060
+ DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
2061
+ DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
2062
+ ]);
2063
+ messageList.push({
2064
+ role: "user",
2065
+ content: userContent,
2066
+ }, {
2067
+ role: "assistant",
2068
+ content: assistantContent,
2069
+ });
2070
+ processedSources++;
2071
+ }
2072
+ else {
2073
+ const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
2074
+ const data = await RESOLVE_PAGINATION_FN(fetch, {
2075
+ symbol,
2076
+ startDate,
2077
+ endDate,
2078
+ });
2079
+ await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, name, data, startDate, endDate);
2080
+ const [userContent, assistantContent] = await Promise.all([
2081
+ user(symbol, data, name, self),
2082
+ assistant(symbol, data, name, self),
2083
+ ]);
2084
+ messageList.push({
2085
+ role: "user",
2086
+ content: userContent,
2087
+ }, {
2088
+ role: "assistant",
2089
+ content: assistantContent,
2090
+ });
2091
+ processedSources++;
2092
+ }
2093
+ const name = "name" in source
2094
+ ? source.name || DEFAULT_SOURCE_NAME
2095
+ : DEFAULT_SOURCE_NAME;
2096
+ strategyList.push({
2097
+ symbol,
2098
+ name,
2099
+ messages: messageList,
2100
+ strategy: await self.params.getPrompt(symbol, messageList),
2101
+ });
2102
+ }
2103
+ }
2104
+ // Emit final progress event (100%)
2105
+ await self.onProgress({
2106
+ optimizerName: self.params.optimizerName,
2107
+ symbol,
2108
+ totalSources,
2109
+ processedSources: totalSources,
2110
+ progress: 1.0,
2111
+ });
2112
+ await CALL_DATA_CALLBACKS_FN(self, symbol, strategyList);
2113
+ return strategyList;
2114
+ };
2115
+ /**
2116
+ * Generates complete executable strategy code.
2117
+ * Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
2118
+ *
2119
+ * @param symbol - Trading pair symbol
2120
+ * @param self - ClientOptimizer instance
2121
+ * @returns Generated TypeScript/JavaScript code as string
2122
+ */
2123
+ const GET_STRATEGY_CODE_FN = async (symbol, self) => {
2124
+ const strategyData = await self.getData(symbol);
2125
+ const prefix = CREATE_PREFIX_FN();
2126
+ const sections = [];
2127
+ const exchangeName = `${prefix}_exchange`;
2128
+ // 1. Top banner with imports
2129
+ {
2130
+ sections.push(await self.params.template.getTopBanner(symbol));
2131
+ sections.push("");
2132
+ }
2133
+ // 2. JSON dump helper function
2134
+ {
2135
+ sections.push(await self.params.template.getJsonDumpTemplate(symbol));
2136
+ sections.push("");
2137
+ }
2138
+ // 3. Helper functions (text and json)
2139
+ {
2140
+ sections.push(await self.params.template.getTextTemplate(symbol));
2141
+ sections.push("");
2142
+ }
2143
+ {
2144
+ sections.push(await self.params.template.getJsonTemplate(symbol));
2145
+ sections.push("");
2146
+ }
2147
+ // 4. Exchange template (assuming first strategy has exchange info)
2148
+ {
2149
+ sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
2150
+ sections.push("");
2151
+ }
2152
+ // 5. Train frame templates
2153
+ {
2154
+ for (let i = 0; i < self.params.rangeTrain.length; i++) {
2155
+ const range = self.params.rangeTrain[i];
2156
+ const frameName = `${prefix}_train_frame-${i + 1}`;
2157
+ sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
2158
+ range.startDate, range.endDate));
2159
+ sections.push("");
2160
+ }
2161
+ }
2162
+ // 6. Test frame template
2163
+ {
2164
+ const testFrameName = `${prefix}_test_frame`;
2165
+ sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
2166
+ self.params.rangeTest.startDate, self.params.rangeTest.endDate));
2167
+ sections.push("");
2168
+ }
2169
+ // 7. Strategy templates for each generated strategy
2170
+ {
2171
+ for (let i = 0; i < strategyData.length; i++) {
2172
+ const strategy = strategyData[i];
2173
+ const strategyName = `${prefix}_strategy-${i + 1}`;
2174
+ const interval = "5m"; // default interval
2175
+ sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
2176
+ sections.push("");
2177
+ }
2178
+ }
2179
+ // 8. Walker template (uses test frame for validation)
2180
+ {
2181
+ const walkerName = `${prefix}_walker`;
2182
+ const testFrameName = `${prefix}_test_frame`;
2183
+ const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
2184
+ sections.push(await self.params.template.getWalkerTemplate(walkerName, `${exchangeName}`, testFrameName, strategies));
2185
+ sections.push("");
2186
+ }
2187
+ // 9. Launcher template
2188
+ {
2189
+ const walkerName = `${prefix}_walker`;
2190
+ sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
2191
+ sections.push("");
2192
+ }
2193
+ const code = sections.join("\n");
2194
+ await CALL_CODE_CALLBACKS_FN(self, symbol, code);
2195
+ return code;
2196
+ };
2197
+ /**
2198
+ * Saves generated strategy code to file.
2199
+ * Creates directory if needed, writes .mjs file with generated code.
2200
+ *
2201
+ * @param symbol - Trading pair symbol
2202
+ * @param path - Output directory path
2203
+ * @param self - ClientOptimizer instance
2204
+ */
2205
+ const GET_STRATEGY_DUMP_FN = async (symbol, path, self) => {
2206
+ const report = await self.getCode(symbol);
2207
+ try {
2208
+ const dir = join(process.cwd(), path);
2209
+ await mkdir(dir, { recursive: true });
2210
+ const filename = `${self.params.optimizerName}_${symbol}.mjs`;
2211
+ const filepath = join(dir, filename);
2212
+ await writeFile(filepath, report, "utf-8");
2213
+ self.params.logger.info(`Optimizer report saved: ${filepath}`);
2214
+ await CALL_DUMP_CALLBACKS_FN(self, symbol, filepath);
2215
+ }
2216
+ catch (error) {
2217
+ self.params.logger.warn(`Failed to save optimizer report:`, error);
2218
+ throw error;
2219
+ }
2220
+ };
2221
+ /**
2222
+ * Client implementation for optimizer operations.
2223
+ *
2224
+ * Features:
2225
+ * - Data collection from multiple sources with pagination
2226
+ * - LLM conversation history building
2227
+ * - Strategy code generation with templates
2228
+ * - File export with callbacks
2229
+ *
2230
+ * Used by OptimizerConnectionService to create optimizer instances.
2231
+ */
2232
+ class ClientOptimizer {
2233
+ constructor(params, onProgress) {
2234
+ this.params = params;
2235
+ this.onProgress = onProgress;
2236
+ /**
2237
+ * Fetches data from all sources and generates strategy metadata.
2238
+ * Processes each training range and builds LLM conversation history.
2239
+ *
2240
+ * @param symbol - Trading pair symbol
2241
+ * @returns Array of generated strategies with conversation context
2242
+ */
2243
+ this.getData = async (symbol) => {
2244
+ this.params.logger.debug("ClientOptimizer getData", {
2245
+ symbol,
2246
+ });
2247
+ return await GET_STRATEGY_DATA_FN(symbol, this);
2248
+ };
2249
+ /**
2250
+ * Generates complete executable strategy code.
2251
+ * Includes imports, helpers, strategies, walker, and launcher.
2252
+ *
2253
+ * @param symbol - Trading pair symbol
2254
+ * @returns Generated TypeScript/JavaScript code as string
2255
+ */
2256
+ this.getCode = async (symbol) => {
2257
+ this.params.logger.debug("ClientOptimizer getCode", {
2258
+ symbol,
2259
+ });
2260
+ return await GET_STRATEGY_CODE_FN(symbol, this);
2261
+ };
2262
+ /**
2263
+ * Generates and saves strategy code to file.
2264
+ * Creates directory if needed, writes .mjs file.
2265
+ *
2266
+ * @param symbol - Trading pair symbol
2267
+ * @param path - Output directory path (default: "./")
2268
+ */
2269
+ this.dump = async (symbol, path = "./") => {
2270
+ this.params.logger.debug("ClientOptimizer dump", {
2271
+ symbol,
2272
+ path,
2273
+ });
2274
+ return await GET_STRATEGY_DUMP_FN(symbol, path, this);
2275
+ };
2276
+ }
2277
+ }
2278
+
2279
+ /**
2280
+ * Callback function for emitting progress events to progressOptimizerEmitter.
2281
+ */
2282
+ const COMMIT_PROGRESS_FN = async (progress) => progressOptimizerEmitter.next(progress);
2283
+ /**
2284
+ * Service for creating and caching optimizer client instances.
2285
+ * Handles dependency injection and template merging.
2286
+ *
2287
+ * Features:
2288
+ * - Memoized optimizer instances (one per optimizerName)
2289
+ * - Template merging (custom + defaults)
2290
+ * - Logger injection
2291
+ * - Delegates to ClientOptimizer for actual operations
2292
+ */
2293
+ class OptimizerConnectionService {
2294
+ constructor() {
2295
+ this.loggerService = inject(TYPES.loggerService);
2296
+ this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
2297
+ this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
2298
+ /**
2299
+ * Creates or retrieves cached optimizer instance.
2300
+ * Memoized by optimizerName for performance.
2301
+ *
2302
+ * Merges custom templates from schema with defaults from OptimizerTemplateService.
2303
+ *
2304
+ * @param optimizerName - Unique optimizer identifier
2305
+ * @returns ClientOptimizer instance with resolved dependencies
2306
+ */
2307
+ this.getOptimizer = memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
2308
+ const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks, } = this.optimizerSchemaService.get(optimizerName);
2309
+ const { getAssistantMessage = this.optimizerTemplateService.getAssistantMessage, getExchangeTemplate = this.optimizerTemplateService.getExchangeTemplate, getFrameTemplate = this.optimizerTemplateService.getFrameTemplate, getJsonDumpTemplate = this.optimizerTemplateService.getJsonDumpTemplate, getJsonTemplate = this.optimizerTemplateService.getJsonTemplate, getLauncherTemplate = this.optimizerTemplateService.getLauncherTemplate, getStrategyTemplate = this.optimizerTemplateService.getStrategyTemplate, getTextTemplate = this.optimizerTemplateService.getTextTemplate, getWalkerTemplate = this.optimizerTemplateService.getWalkerTemplate, getTopBanner = this.optimizerTemplateService.getTopBanner, getUserMessage = this.optimizerTemplateService.getUserMessage, } = rawTemplate;
2310
+ const template = {
2311
+ getAssistantMessage,
2312
+ getExchangeTemplate,
2313
+ getFrameTemplate,
2314
+ getJsonDumpTemplate,
2315
+ getJsonTemplate,
2316
+ getLauncherTemplate,
2317
+ getStrategyTemplate,
2318
+ getTextTemplate,
2319
+ getWalkerTemplate,
2320
+ getTopBanner,
2321
+ getUserMessage,
2322
+ };
2323
+ return new ClientOptimizer({
2324
+ optimizerName,
2325
+ logger: this.loggerService,
2326
+ getPrompt,
2327
+ rangeTest,
2328
+ rangeTrain,
2329
+ source,
2330
+ template,
2331
+ callbacks,
2332
+ }, COMMIT_PROGRESS_FN);
2333
+ });
2334
+ /**
2335
+ * Fetches data from all sources and generates strategy metadata.
2336
+ *
2337
+ * @param symbol - Trading pair symbol
2338
+ * @param optimizerName - Optimizer identifier
2339
+ * @returns Array of generated strategies with conversation context
2340
+ */
2341
+ this.getData = async (symbol, optimizerName) => {
2342
+ this.loggerService.log("optimizerConnectionService getData", {
2343
+ symbol,
2344
+ optimizerName,
2345
+ });
2346
+ const optimizer = this.getOptimizer(optimizerName);
2347
+ return await optimizer.getData(symbol);
2348
+ };
2349
+ /**
2350
+ * Generates complete executable strategy code.
2351
+ *
2352
+ * @param symbol - Trading pair symbol
2353
+ * @param optimizerName - Optimizer identifier
2354
+ * @returns Generated TypeScript/JavaScript code as string
2355
+ */
2356
+ this.getCode = async (symbol, optimizerName) => {
2357
+ this.loggerService.log("optimizerConnectionService getCode", {
2358
+ symbol,
2359
+ optimizerName,
2360
+ });
2361
+ const optimizer = this.getOptimizer(optimizerName);
2362
+ return await optimizer.getCode(symbol);
2363
+ };
2364
+ /**
2365
+ * Generates and saves strategy code to file.
2366
+ *
2367
+ * @param symbol - Trading pair symbol
2368
+ * @param optimizerName - Optimizer identifier
2369
+ * @param path - Output directory path (optional)
2370
+ */
2371
+ this.dump = async (symbol, optimizerName, path) => {
2372
+ this.loggerService.log("optimizerConnectionService getCode", {
2373
+ symbol,
2374
+ optimizerName,
2375
+ });
2376
+ const optimizer = this.getOptimizer(optimizerName);
2377
+ return await optimizer.dump(symbol, path);
2378
+ };
2379
+ }
2380
+ }
2381
+
2382
+ const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
2383
+ const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
2384
+ const METHOD_NAME_DUMP = "optimizerGlobalService dump";
2385
+ /**
2386
+ * Global service for optimizer operations with validation.
2387
+ * Entry point for public API, performs validation before delegating to ConnectionService.
2388
+ *
2389
+ * Workflow:
2390
+ * 1. Log operation
2391
+ * 2. Validate optimizer exists
2392
+ * 3. Delegate to OptimizerConnectionService
2393
+ */
2394
+ class OptimizerGlobalService {
721
2395
  constructor() {
722
- /** Private service handling AI provider operations */
723
- this.runnerPrivateService = inject(TYPES.runnerPrivateService);
724
- /** Logger service for operation tracking */
725
2396
  this.loggerService = inject(TYPES.loggerService);
2397
+ this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
2398
+ this.optimizerValidationService = inject(TYPES.optimizerValidationService);
726
2399
  /**
727
- * Executes a standard AI completion within the specified context.
728
- *
729
- * @param params - Completion parameters including messages and options
730
- * @param context - Execution context with inference provider, model, and API key
731
- * @returns Promise resolving to AI response message
2400
+ * Fetches data from all sources and generates strategy metadata.
2401
+ * Validates optimizer existence before execution.
732
2402
  *
733
- * @example
734
- * ```typescript
735
- * const result = await runnerPublicService.getCompletion({
736
- * messages: [
737
- * { role: "system", content: "You are a trading analyst" },
738
- * { role: "user", content: "Analyze BTC/USDT" }
739
- * ]
740
- * }, {
741
- * inference: InferenceName.ClaudeInference,
742
- * model: "claude-3-5-sonnet-20240620",
743
- * apiKey: "sk-ant-..."
744
- * });
745
- * ```
2403
+ * @param symbol - Trading pair symbol
2404
+ * @param optimizerName - Optimizer identifier
2405
+ * @returns Array of generated strategies with conversation context
2406
+ * @throws Error if optimizer not found
746
2407
  */
747
- this.getCompletion = async (params, context) => {
748
- this.loggerService.log("runnerPublicService getCompletion");
749
- return await ContextService.runInContext(async () => {
750
- return await this.runnerPrivateService.getCompletion(params);
751
- }, context);
2408
+ this.getData = async (symbol, optimizerName) => {
2409
+ this.loggerService.log(METHOD_NAME_GET_DATA, {
2410
+ symbol,
2411
+ optimizerName,
2412
+ });
2413
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
2414
+ return await this.optimizerConnectionService.getData(symbol, optimizerName);
752
2415
  };
753
2416
  /**
754
- * Executes a streaming AI completion within the specified context.
755
- *
756
- * Similar to getCompletion but enables streaming mode where supported by the provider.
757
- * The response is accumulated and returned as a complete message once streaming finishes.
758
- *
759
- * @param params - Completion parameters including messages and options
760
- * @param context - Execution context with inference provider, model, and API key
761
- * @returns Promise resolving to accumulated AI response message
2417
+ * Generates complete executable strategy code.
2418
+ * Validates optimizer existence before execution.
762
2419
  *
763
- * @example
764
- * ```typescript
765
- * const result = await runnerPublicService.getStreamCompletion({
766
- * messages: [
767
- * { role: "user", content: "Generate trading signal for ETH/USDT" }
768
- * ]
769
- * }, {
770
- * inference: InferenceName.GPT5Inference,
771
- * model: "gpt-5o-mini",
772
- * apiKey: "sk-..."
773
- * });
774
- * ```
2420
+ * @param symbol - Trading pair symbol
2421
+ * @param optimizerName - Optimizer identifier
2422
+ * @returns Generated TypeScript/JavaScript code as string
2423
+ * @throws Error if optimizer not found
775
2424
  */
776
- this.getStreamCompletion = async (params, context) => {
777
- this.loggerService.log("runnerPublicService getStreamCompletion");
778
- return await ContextService.runInContext(async () => {
779
- return await this.runnerPrivateService.getStreamCompletion(params);
780
- }, context);
2425
+ this.getCode = async (symbol, optimizerName) => {
2426
+ this.loggerService.log(METHOD_NAME_GET_CODE, {
2427
+ symbol,
2428
+ optimizerName,
2429
+ });
2430
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
2431
+ return await this.optimizerConnectionService.getCode(symbol, optimizerName);
781
2432
  };
782
2433
  /**
783
- * Executes a structured outline completion within the specified context.
784
- *
785
- * Uses structured output (JSON schema validation) to ensure the AI response
786
- * conforms to a predefined format. Ideal for extracting structured data
787
- * from AI responses (e.g., trading signals with specific fields).
788
- *
789
- * @param params - Outline completion parameters including messages and schema
790
- * @param context - Execution context with inference provider, model, and API key
791
- * @returns Promise resolving to structured AI response
2434
+ * Generates and saves strategy code to file.
2435
+ * Validates optimizer existence before execution.
792
2436
  *
793
- * @example
794
- * ```typescript
795
- * const signal = await runnerPublicService.getOutlineCompletion({
796
- * messages: [
797
- * { role: "user", content: "Decide position for BTC/USDT" }
798
- * ]
799
- * }, {
800
- * inference: InferenceName.DeepseekInference,
801
- * model: "deepseek-chat",
802
- * apiKey: "sk-..."
803
- * });
804
- * // Returns: { position: "long", price_open: 50000, ... }
805
- * ```
2437
+ * @param symbol - Trading pair symbol
2438
+ * @param optimizerName - Optimizer identifier
2439
+ * @param path - Output directory path (optional)
2440
+ * @throws Error if optimizer not found
806
2441
  */
807
- this.getOutlineCompletion = async (params, context) => {
808
- this.loggerService.log("runnerPublicService getOutlineCompletion");
809
- return await ContextService.runInContext(async () => {
810
- return await this.runnerPrivateService.getOutlineCompletion(params);
811
- }, context);
2442
+ this.dump = async (symbol, optimizerName, path) => {
2443
+ this.loggerService.log(METHOD_NAME_DUMP, {
2444
+ symbol,
2445
+ optimizerName,
2446
+ path,
2447
+ });
2448
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
2449
+ return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
812
2450
  };
813
2451
  }
814
2452
  }
@@ -855,6 +2493,22 @@ class RunnerPublicService {
855
2493
  provide(TYPES.runnerPublicService, () => new RunnerPublicService());
856
2494
  provide(TYPES.outlinePublicService, () => new OutlinePublicService());
857
2495
  }
2496
+ {
2497
+ provide(TYPES.signalPromptService, () => new SignalPromptService());
2498
+ }
2499
+ {
2500
+ provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
2501
+ }
2502
+ /**
2503
+ * Register optimizer services.
2504
+ */
2505
+ {
2506
+ provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
2507
+ provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
2508
+ provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
2509
+ provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
2510
+ provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
2511
+ }
858
2512
 
859
2513
  /**
860
2514
  * Enumeration of supported LLM inference providers.
@@ -924,7 +2578,7 @@ var InferenceName$1 = InferenceName;
924
2578
  * ```
925
2579
  */
926
2580
  const getGrok = singleshot(() => {
927
- const apiKey = lib.contextService.context.apiKey;
2581
+ const apiKey = engine$1.contextService.context.apiKey;
928
2582
  if (Array.isArray(apiKey)) {
929
2583
  getGrok.clear();
930
2584
  throw new Error("Grok provider does not support token rotation");
@@ -1654,7 +3308,7 @@ class OllamaWrapper {
1654
3308
  constructor(_config) {
1655
3309
  this._config = _config;
1656
3310
  /** Round-robin chat function factory */
1657
- this._chatFn = RoundRobin.create(lib.contextService.context.apiKey, (token) => {
3311
+ this._chatFn = RoundRobin.create(engine$1.contextService.context.apiKey, (token) => {
1658
3312
  const ollama = new Ollama({
1659
3313
  ...this._config,
1660
3314
  headers: {
@@ -1670,7 +3324,7 @@ class OllamaWrapper {
1670
3324
  }
1671
3325
  };
1672
3326
  });
1673
- if (!lib.contextService.context.apiKey) {
3327
+ if (!engine$1.contextService.context.apiKey) {
1674
3328
  throw new Error("OllamaRotate required apiKey[] to process token rotation");
1675
3329
  }
1676
3330
  }
@@ -1755,7 +3409,7 @@ const getOllamaRotate = singleshot(() => new OllamaWrapper({
1755
3409
  * ```
1756
3410
  */
1757
3411
  const getOllama = singleshot(() => {
1758
- const apiKey = lib.contextService.context.apiKey;
3412
+ const apiKey = engine$1.contextService.context.apiKey;
1759
3413
  if (Array.isArray(apiKey)) {
1760
3414
  return getOllamaRotate();
1761
3415
  }
@@ -2149,7 +3803,7 @@ class OllamaProvider {
2149
3803
  * ```
2150
3804
  */
2151
3805
  const getClaude = singleshot(() => {
2152
- const apiKey = lib.contextService.context.apiKey;
3806
+ const apiKey = engine$1.contextService.context.apiKey;
2153
3807
  if (Array.isArray(apiKey)) {
2154
3808
  getClaude.clear();
2155
3809
  throw new Error("Claude provider does not support token rotation");
@@ -2492,7 +4146,7 @@ class ClaudeProvider {
2492
4146
  * ```
2493
4147
  */
2494
4148
  const getOpenAi = singleshot(() => {
2495
- const apiKey = lib.contextService.context.apiKey;
4149
+ const apiKey = engine$1.contextService.context.apiKey;
2496
4150
  if (Array.isArray(apiKey)) {
2497
4151
  getOpenAi.clear();
2498
4152
  throw new Error("OpenAI provider does not support token rotation");
@@ -2843,7 +4497,7 @@ class GPT5Provider {
2843
4497
  * ```
2844
4498
  */
2845
4499
  const getDeepseek = singleshot(() => {
2846
- const apiKey = lib.contextService.context.apiKey;
4500
+ const apiKey = engine$1.contextService.context.apiKey;
2847
4501
  if (Array.isArray(apiKey)) {
2848
4502
  getDeepseek.clear();
2849
4503
  throw new Error("Deepseek provider does not support token rotation");
@@ -3165,7 +4819,7 @@ class DeepseekProvider {
3165
4819
  * ```
3166
4820
  */
3167
4821
  const getMistral = singleshot(() => {
3168
- const apiKey = lib.contextService.context.apiKey;
4822
+ const apiKey = engine$1.contextService.context.apiKey;
3169
4823
  if (Array.isArray(apiKey)) {
3170
4824
  getMistral.clear();
3171
4825
  throw new Error("Mistral provider does not support token rotation");
@@ -3487,7 +5141,7 @@ class MistralProvider {
3487
5141
  * ```
3488
5142
  */
3489
5143
  const getPerplexity = singleshot(() => {
3490
- const apiKey = lib.contextService.context.apiKey;
5144
+ const apiKey = engine$1.contextService.context.apiKey;
3491
5145
  if (Array.isArray(apiKey)) {
3492
5146
  getPerplexity.clear();
3493
5147
  throw new Error("Perplexity provider does not support token rotation");
@@ -3767,7 +5421,7 @@ class PerplexityProvider {
3767
5421
  * ```
3768
5422
  */
3769
5423
  const getCohere = singleshot(() => {
3770
- const apiKey = lib.contextService.context.apiKey;
5424
+ const apiKey = engine$1.contextService.context.apiKey;
3771
5425
  if (Array.isArray(apiKey)) {
3772
5426
  getCohere.clear();
3773
5427
  throw new Error("Cohere provider does not support token rotation");
@@ -4437,7 +6091,7 @@ class AlibabaProvider {
4437
6091
  * ```
4438
6092
  */
4439
6093
  const getZAi = singleshot(() => {
4440
- const apiKey = lib.contextService.context.apiKey;
6094
+ const apiKey = engine$1.contextService.context.apiKey;
4441
6095
  if (Array.isArray(apiKey)) {
4442
6096
  getZAi.clear();
4443
6097
  throw new Error("Z.ai provider does not support token rotation");
@@ -4855,6 +6509,19 @@ const publicServices = {
4855
6509
  runnerPublicService: inject(TYPES.runnerPublicService),
4856
6510
  outlinePublicService: inject(TYPES.outlinePublicService),
4857
6511
  };
6512
+ const promptServices = {
6513
+ signalPromptService: inject(TYPES.signalPromptService),
6514
+ };
6515
+ const markdownServices = {
6516
+ outlineMarkdownService: inject(TYPES.outlineMarkdownService),
6517
+ };
6518
+ const optimizerServices = {
6519
+ optimizerTemplateService: inject(TYPES.optimizerTemplateService),
6520
+ optimizerSchemaService: inject(TYPES.optimizerSchemaService),
6521
+ optimizerValidationService: inject(TYPES.optimizerValidationService),
6522
+ optimizerConnectionService: inject(TYPES.optimizerConnectionService),
6523
+ optimizerGlobalService: inject(TYPES.optimizerGlobalService),
6524
+ };
4858
6525
  /**
4859
6526
  * Main engine object containing all services.
4860
6527
  * Provides unified access to the entire service layer.
@@ -4864,6 +6531,9 @@ const engine = {
4864
6531
  ...baseServices,
4865
6532
  ...privateServices,
4866
6533
  ...publicServices,
6534
+ ...promptServices,
6535
+ ...markdownServices,
6536
+ ...optimizerServices,
4867
6537
  };
4868
6538
  // Initialize DI container
4869
6539
  init();
@@ -4885,7 +6555,7 @@ init();
4885
6555
  }
4886
6556
  // Make engine globally accessible for debugging
4887
6557
  Object.assign(globalThis, { engine });
4888
- var lib = engine;
6558
+ var engine$1 = engine;
4889
6559
 
4890
6560
  const INFERENCE_TIMEOUT$2 = 35000;
4891
6561
  const LOCAL_RUNNER_FN$2 = timeout(async (params) => {
@@ -5217,7 +6887,7 @@ addOutline({
5217
6887
  *
5218
6888
  * @throws Error if validation fails (missing or duplicate registrations)
5219
6889
  */
5220
- validate({
6890
+ validate$1({
5221
6891
  CompletionName: CompletionName$1,
5222
6892
  OutlineName: OutlineName$1,
5223
6893
  });
@@ -5242,7 +6912,7 @@ validate({
5242
6912
  * ```
5243
6913
  */
5244
6914
  const ollama = async (messages, model, apiKey) => {
5245
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.OllamaInference, model, apiKey);
6915
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.OllamaInference, model, apiKey);
5246
6916
  };
5247
6917
  /**
5248
6918
  * Generate structured trading signal from Grok models.
@@ -5263,7 +6933,7 @@ const ollama = async (messages, model, apiKey) => {
5263
6933
  * ```
5264
6934
  */
5265
6935
  const grok = async (messages, model, apiKey) => {
5266
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.GrokInference, model, apiKey);
6936
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.GrokInference, model, apiKey);
5267
6937
  };
5268
6938
  /**
5269
6939
  * Generate structured trading signal from Hugging Face models.
@@ -5283,7 +6953,7 @@ const grok = async (messages, model, apiKey) => {
5283
6953
  * ```
5284
6954
  */
5285
6955
  const hf = async (messages, model, apiKey) => {
5286
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.HfInference, model, apiKey);
6956
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.HfInference, model, apiKey);
5287
6957
  };
5288
6958
  /**
5289
6959
  * Generate structured trading signal from Claude models.
@@ -5304,7 +6974,7 @@ const hf = async (messages, model, apiKey) => {
5304
6974
  * ```
5305
6975
  */
5306
6976
  const claude = async (messages, model, apiKey) => {
5307
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.ClaudeInference, model, apiKey);
6977
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.ClaudeInference, model, apiKey);
5308
6978
  };
5309
6979
  /**
5310
6980
  * Generate structured trading signal from OpenAI GPT models.
@@ -5325,7 +6995,7 @@ const claude = async (messages, model, apiKey) => {
5325
6995
  * ```
5326
6996
  */
5327
6997
  const gpt5 = async (messages, model, apiKey) => {
5328
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.GPT5Inference, model, apiKey);
6998
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.GPT5Inference, model, apiKey);
5329
6999
  };
5330
7000
  /**
5331
7001
  * Generate structured trading signal from DeepSeek models.
@@ -5346,7 +7016,7 @@ const gpt5 = async (messages, model, apiKey) => {
5346
7016
  * ```
5347
7017
  */
5348
7018
  const deepseek = async (messages, model, apiKey) => {
5349
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.DeepseekInference, model, apiKey);
7019
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.DeepseekInference, model, apiKey);
5350
7020
  };
5351
7021
  /**
5352
7022
  * Generate structured trading signal from Mistral AI models.
@@ -5367,7 +7037,7 @@ const deepseek = async (messages, model, apiKey) => {
5367
7037
  * ```
5368
7038
  */
5369
7039
  const mistral = async (messages, model, apiKey) => {
5370
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.MistralInference, model, apiKey);
7040
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.MistralInference, model, apiKey);
5371
7041
  };
5372
7042
  /**
5373
7043
  * Generate structured trading signal from Perplexity AI models.
@@ -5388,7 +7058,7 @@ const mistral = async (messages, model, apiKey) => {
5388
7058
  * ```
5389
7059
  */
5390
7060
  const perplexity = async (messages, model, apiKey) => {
5391
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.PerplexityInference, model, apiKey);
7061
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.PerplexityInference, model, apiKey);
5392
7062
  };
5393
7063
  /**
5394
7064
  * Generate structured trading signal from Cohere models.
@@ -5409,7 +7079,7 @@ const perplexity = async (messages, model, apiKey) => {
5409
7079
  * ```
5410
7080
  */
5411
7081
  const cohere = async (messages, model, apiKey) => {
5412
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.CohereInference, model, apiKey);
7082
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.CohereInference, model, apiKey);
5413
7083
  };
5414
7084
  /**
5415
7085
  * Generate structured trading signal from Alibaba Cloud Qwen models.
@@ -5430,7 +7100,7 @@ const cohere = async (messages, model, apiKey) => {
5430
7100
  * ```
5431
7101
  */
5432
7102
  const alibaba = async (messages, model, apiKey) => {
5433
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.AlibabaInference, model, apiKey);
7103
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.AlibabaInference, model, apiKey);
5434
7104
  };
5435
7105
  /**
5436
7106
  * Generate structured trading signal from Zhipu AI GLM-4 models.
@@ -5454,7 +7124,7 @@ const alibaba = async (messages, model, apiKey) => {
5454
7124
  * ```
5455
7125
  */
5456
7126
  const glm4 = async (messages, model, apiKey) => {
5457
- return await lib.outlinePublicService.getCompletion(messages, InferenceName$1.GLM4Inference, model, apiKey);
7127
+ return await engine$1.outlinePublicService.getCompletion(messages, InferenceName$1.GLM4Inference, model, apiKey);
5458
7128
  };
5459
7129
 
5460
7130
  /**
@@ -5475,7 +7145,7 @@ const glm4 = async (messages, model, apiKey) => {
5475
7145
  * ```
5476
7146
  */
5477
7147
  const setLogger = (logger) => {
5478
- lib.loggerService.setLogger(logger);
7148
+ engine$1.loggerService.setLogger(logger);
5479
7149
  };
5480
7150
 
5481
7151
  /**
@@ -5536,4 +7206,520 @@ function overrideSignalFormat(format) {
5536
7206
  });
5537
7207
  }
5538
7208
 
5539
- export { alibaba, claude, cohere, deepseek, glm4, gpt5, grok, hf, engine as lib, mistral, ollama, overrideSignalFormat, perplexity, setLogger };
7209
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
7210
+ /**
7211
+ * Dumps signal data and LLM conversation history to markdown files.
7212
+ * Used by AI-powered strategies to save debug logs for analysis.
7213
+ *
7214
+ * Creates a directory structure with:
7215
+ * - 00_system_prompt.md - System messages and output summary
7216
+ * - XX_user_message.md - Each user message in separate file (numbered)
7217
+ * - XX_llm_output.md - Final LLM output with signal data
7218
+ *
7219
+ * Skips if directory already exists to avoid overwriting previous results.
7220
+ *
7221
+ * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
7222
+ * @param history - Array of message models from LLM conversation
7223
+ * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
7224
+ * @param outputDir - Output directory path (default: "./dump/strategy")
7225
+ * @returns Promise that resolves when all files are written
7226
+ *
7227
+ * @example
7228
+ * ```typescript
7229
+ * import { dumpSignal, getCandles } from "backtest-kit";
7230
+ * import { v4 as uuid } from "uuid";
7231
+ *
7232
+ * addStrategy({
7233
+ * strategyName: "llm-strategy",
7234
+ * interval: "5m",
7235
+ * getSignal: async (symbol) => {
7236
+ * const messages = [];
7237
+ *
7238
+ * // Build multi-timeframe analysis conversation
7239
+ * const candles1h = await getCandles(symbol, "1h", 24);
7240
+ * messages.push(
7241
+ * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
7242
+ * { role: "assistant", content: "Trend analyzed" }
7243
+ * );
7244
+ *
7245
+ * const candles5m = await getCandles(symbol, "5m", 24);
7246
+ * messages.push(
7247
+ * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
7248
+ * { role: "assistant", content: "Structure analyzed" }
7249
+ * );
7250
+ *
7251
+ * // Request signal
7252
+ * messages.push({
7253
+ * role: "user",
7254
+ * content: "Generate trading signal. Use position: 'wait' if uncertain."
7255
+ * });
7256
+ *
7257
+ * const resultId = uuid();
7258
+ * const signal = await llmRequest(messages);
7259
+ *
7260
+ * // Save conversation and result for debugging
7261
+ * await dumpSignal(resultId, messages, signal);
7262
+ *
7263
+ * return signal;
7264
+ * }
7265
+ * });
7266
+ *
7267
+ * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
7268
+ * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
7269
+ * // ./dump/strategy/{uuid}/02_assistant_message.md
7270
+ * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
7271
+ * // ./dump/strategy/{uuid}/04_assistant_message.md
7272
+ * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
7273
+ * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
7274
+ * ```
7275
+ */
7276
+ async function dumpSignalData(signalId, history, signal, outputDir = "./dump/strategy") {
7277
+ engine$1.loggerService.info(DUMP_SIGNAL_METHOD_NAME, {
7278
+ signalId,
7279
+ history,
7280
+ signal,
7281
+ outputDir,
7282
+ });
7283
+ return await engine$1.outlineMarkdownService.dumpSignal(signalId, history, signal, outputDir);
7284
+ }
7285
+
7286
+ const METHOD_NAME = "validate.validate";
7287
+ /**
7288
+ * Retrieves all registered optimizers as a map
7289
+ * @private
7290
+ * @returns Map of optimizer names
7291
+ */
7292
+ const getOptimizerMap = async () => {
7293
+ const optimizerMap = {};
7294
+ for (const { optimizerName } of await engine$1.optimizerValidationService.list()) {
7295
+ Object.assign(optimizerMap, { [optimizerName]: optimizerName });
7296
+ }
7297
+ return optimizerMap;
7298
+ };
7299
+ /**
7300
+ * Internal validation function that processes all provided entity enums.
7301
+ *
7302
+ * Iterates through each enum's values and validates them against their
7303
+ * respective validation services. Uses memoized validation for performance.
7304
+ *
7305
+ * If entity enums are not provided, fetches all registered entities from
7306
+ * their respective validation services and validates them.
7307
+ *
7308
+ * @private
7309
+ * @param args - Validation arguments containing entity name enums
7310
+ * @throws {Error} If any entity name is not found in its registry
7311
+ */
7312
+ const validateInternal = async (args) => {
7313
+ const { OptimizerName = await getOptimizerMap(), } = args;
7314
+ for (const optimizerName of Object.values(OptimizerName)) {
7315
+ engine$1.optimizerValidationService.validate(optimizerName, METHOD_NAME);
7316
+ }
7317
+ };
7318
+ /**
7319
+ * Validates the existence of all provided entity names across validation services.
7320
+ *
7321
+ * This function accepts enum objects for various entity types (exchanges, frames,
7322
+ * strategies, risks, sizings, optimizers, walkers) and validates that each entity
7323
+ * name exists in its respective registry. Validation results are memoized for performance.
7324
+ *
7325
+ * If no arguments are provided (or specific entity types are omitted), the function
7326
+ * automatically fetches and validates ALL registered entities from their respective
7327
+ * validation services. This is useful for comprehensive validation of the entire setup.
7328
+ *
7329
+ * Use this before running backtests or optimizations to ensure all referenced
7330
+ * entities are properly registered and configured.
7331
+ *
7332
+ * @public
7333
+ * @param args - Partial validation arguments containing entity name enums to validate.
7334
+ * If empty or omitted, validates all registered entities.
7335
+ * @throws {Error} If any entity name is not found in its validation service
7336
+ *
7337
+ * @example
7338
+ * ```typescript
7339
+ * // Validate ALL registered entities (exchanges, frames, strategies, etc.)
7340
+ * await validate({});
7341
+ * ```
7342
+ *
7343
+ * @example
7344
+ * ```typescript
7345
+ * // Define your entity name enums
7346
+ * enum ExchangeName {
7347
+ * BINANCE = "binance",
7348
+ * BYBIT = "bybit"
7349
+ * }
7350
+ *
7351
+ * enum StrategyName {
7352
+ * MOMENTUM_BTC = "momentum-btc"
7353
+ * }
7354
+ *
7355
+ * // Validate specific entities before running backtest
7356
+ * await validate({
7357
+ * ExchangeName,
7358
+ * StrategyName,
7359
+ * });
7360
+ * ```
7361
+ *
7362
+ * @example
7363
+ * ```typescript
7364
+ * // Validate specific entity types
7365
+ * await validate({
7366
+ * RiskName: { CONSERVATIVE: "conservative" },
7367
+ * SizingName: { FIXED_1000: "fixed-1000" },
7368
+ * });
7369
+ * ```
7370
+ */
7371
+ async function validate(args = {}) {
7372
+ engine$1.loggerService.log(METHOD_NAME);
7373
+ return await validateInternal(args);
7374
+ }
7375
+
7376
+ const METHOD_NAME_SIGNAL = "history.commitSignalPromptHistory";
7377
+ /**
7378
+ * Commits signal prompt history to the message array.
7379
+ *
7380
+ * Extracts trading context from ExecutionContext and MethodContext,
7381
+ * then adds signal-specific system prompts at the beginning and user prompt
7382
+ * at the end of the history array if they are not empty.
7383
+ *
7384
+ * Context extraction:
7385
+ * - symbol: Provided as parameter for debugging convenience
7386
+ * - backtest mode: From ExecutionContext
7387
+ * - strategyName, exchangeName, frameName: From MethodContext
7388
+ *
7389
+ * @param symbol - Trading symbol (e.g., "BTCUSDT") for debugging convenience
7390
+ * @param history - Message array to append prompts to
7391
+ * @returns Promise that resolves when prompts are added
7392
+ * @throws Error if ExecutionContext or MethodContext is not active
7393
+ *
7394
+ * @example
7395
+ * ```typescript
7396
+ * const messages: MessageModel[] = [];
7397
+ * await commitSignalPromptHistory("BTCUSDT", messages);
7398
+ * // messages now contains system prompts at start and user prompt at end
7399
+ * ```
7400
+ */
7401
+ async function commitSignalPromptHistory(symbol, history) {
7402
+ engine$1.loggerService.log(METHOD_NAME_SIGNAL, {
7403
+ symbol,
7404
+ });
7405
+ const { strategyName, exchangeName, frameName } = await getContext();
7406
+ const mode = await getMode();
7407
+ const isBacktest = mode === "backtest";
7408
+ const systemPrompts = await engine$1.signalPromptService.getSystemPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
7409
+ const userPrompt = await engine$1.signalPromptService.getUserPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
7410
+ if (systemPrompts.length > 0) {
7411
+ for (const content of systemPrompts) {
7412
+ history.unshift({
7413
+ role: "system",
7414
+ content,
7415
+ });
7416
+ }
7417
+ }
7418
+ if (userPrompt && userPrompt.trim() !== "") {
7419
+ history.push({
7420
+ role: "user",
7421
+ content: userPrompt,
7422
+ });
7423
+ }
7424
+ }
7425
+
7426
+ const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizerSchema";
7427
+ /**
7428
+ * Registers an optimizer configuration in the framework.
7429
+ *
7430
+ * The optimizer generates trading strategies by:
7431
+ * - Collecting data from multiple sources across training periods
7432
+ * - Building LLM conversation history with fetched data
7433
+ * - Generating strategy prompts using getPrompt()
7434
+ * - Creating executable backtest code with templates
7435
+ *
7436
+ * The optimizer produces a complete .mjs file containing:
7437
+ * - Exchange, Frame, Strategy, and Walker configurations
7438
+ * - Multi-timeframe analysis logic
7439
+ * - LLM integration for signal generation
7440
+ * - Event listeners for progress tracking
7441
+ *
7442
+ * @param optimizerSchema - Optimizer configuration object
7443
+ * @param optimizerSchema.optimizerName - Unique optimizer identifier
7444
+ * @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
7445
+ * @param optimizerSchema.rangeTest - Testing time range for strategy validation
7446
+ * @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
7447
+ * @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
7448
+ * @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
7449
+ * @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
7450
+ *
7451
+ * @example
7452
+ * ```typescript
7453
+ * // Basic optimizer with single data source
7454
+ * addOptimizerSchema({
7455
+ * optimizerName: "llm-strategy-generator",
7456
+ * rangeTrain: [
7457
+ * {
7458
+ * note: "Bull market period",
7459
+ * startDate: new Date("2024-01-01"),
7460
+ * endDate: new Date("2024-01-31"),
7461
+ * },
7462
+ * {
7463
+ * note: "Bear market period",
7464
+ * startDate: new Date("2024-02-01"),
7465
+ * endDate: new Date("2024-02-28"),
7466
+ * },
7467
+ * ],
7468
+ * rangeTest: {
7469
+ * note: "Validation period",
7470
+ * startDate: new Date("2024-03-01"),
7471
+ * endDate: new Date("2024-03-31"),
7472
+ * },
7473
+ * source: [
7474
+ * {
7475
+ * name: "historical-backtests",
7476
+ * fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
7477
+ * // Fetch historical backtest results from database
7478
+ * return await db.backtests.find({
7479
+ * symbol,
7480
+ * date: { $gte: startDate, $lte: endDate },
7481
+ * })
7482
+ * .skip(offset)
7483
+ * .limit(limit);
7484
+ * },
7485
+ * user: async (symbol, data, name) => {
7486
+ * return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
7487
+ * },
7488
+ * assistant: async (symbol, data, name) => {
7489
+ * return "Historical data analyzed successfully";
7490
+ * },
7491
+ * },
7492
+ * ],
7493
+ * getPrompt: async (symbol, messages) => {
7494
+ * // Generate strategy prompt from conversation
7495
+ * return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
7496
+ * },
7497
+ * callbacks: {
7498
+ * onData: (symbol, strategyData) => {
7499
+ * console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
7500
+ * },
7501
+ * onCode: (symbol, code) => {
7502
+ * console.log(`Generated ${code.length} characters of code for ${symbol}`);
7503
+ * },
7504
+ * onDump: (symbol, filepath) => {
7505
+ * console.log(`Saved strategy to ${filepath}`);
7506
+ * },
7507
+ * onSourceData: (symbol, sourceName, data, startDate, endDate) => {
7508
+ * console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
7509
+ * },
7510
+ * },
7511
+ * });
7512
+ * ```
7513
+ */
7514
+ function addOptimizerSchema(optimizerSchema) {
7515
+ engine$1.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
7516
+ optimizerSchema,
7517
+ });
7518
+ engine$1.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
7519
+ engine$1.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
7520
+ }
7521
+
7522
+ /**
7523
+ * Subscribe to optimizer progress events.
7524
+ * Receives updates during optimizer execution with progress percentage.
7525
+ *
7526
+ * @param callback - Function called on each progress update
7527
+ * @returns Unsubscribe function
7528
+ *
7529
+ * @example
7530
+ * ```typescript
7531
+ * const unsub = listenOptimizerProgress((event) => {
7532
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
7533
+ * console.log(`Processed: ${event.processedSources} / ${event.totalSources}`);
7534
+ * });
7535
+ * // Later: unsub();
7536
+ * ```
7537
+ */
7538
+ function listenOptimizerProgress(callback) {
7539
+ return progressOptimizerEmitter.subscribe(callback);
7540
+ }
7541
+ /**
7542
+ * Subscribe to error events.
7543
+ * Receives errors from optimizer operations.
7544
+ *
7545
+ * @param callback - Function called on each error
7546
+ * @returns Unsubscribe function
7547
+ *
7548
+ * @example
7549
+ * ```typescript
7550
+ * const unsub = listenError((error) => {
7551
+ * console.error("Error occurred:", error);
7552
+ * });
7553
+ * // Later: unsub();
7554
+ * ```
7555
+ */
7556
+ function listenError(callback) {
7557
+ return errorEmitter.subscribe(callback);
7558
+ }
7559
+
7560
+ const GET_OPTIMIZER_METHOD_NAME = "get.getOptimizerSchema";
7561
+ /**
7562
+ * Retrieves a registered optimizer schema by name.
7563
+ *
7564
+ * @param optimizerName - Unique optimizer identifier
7565
+ * @returns The optimizer schema configuration object
7566
+ * @throws Error if optimizer is not registered
7567
+ *
7568
+ * @example
7569
+ * ```typescript
7570
+ * const optimizer = getOptimizer("llm-strategy-generator");
7571
+ * console.log(optimizer.rangeTrain); // Array of training ranges
7572
+ * console.log(optimizer.rangeTest); // Testing range
7573
+ * console.log(optimizer.source); // Array of data sources
7574
+ * console.log(optimizer.getPrompt); // async function
7575
+ * ```
7576
+ */
7577
+ function getOptimizerSchema(optimizerName) {
7578
+ engine$1.loggerService.log(GET_OPTIMIZER_METHOD_NAME, {
7579
+ optimizerName,
7580
+ });
7581
+ engine$1.optimizerValidationService.validate(optimizerName, GET_OPTIMIZER_METHOD_NAME);
7582
+ return engine$1.optimizerSchemaService.get(optimizerName);
7583
+ }
7584
+
7585
+ const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizerSchema";
7586
+ /**
7587
+ * Returns a list of all registered optimizer schemas.
7588
+ *
7589
+ * Retrieves all optimizers that have been registered via addOptimizer().
7590
+ * Useful for debugging, documentation, or building dynamic UIs.
7591
+ *
7592
+ * @returns Array of optimizer schemas with their configurations
7593
+ *
7594
+ * @example
7595
+ * ```typescript
7596
+ * import { listOptimizers, addOptimizer } from "backtest-kit";
7597
+ *
7598
+ * addOptimizer({
7599
+ * optimizerName: "llm-strategy-generator",
7600
+ * note: "Generates trading strategies using LLM",
7601
+ * rangeTrain: [
7602
+ * {
7603
+ * note: "Training period 1",
7604
+ * startDate: new Date("2024-01-01"),
7605
+ * endDate: new Date("2024-01-31"),
7606
+ * },
7607
+ * ],
7608
+ * rangeTest: {
7609
+ * note: "Testing period",
7610
+ * startDate: new Date("2024-02-01"),
7611
+ * endDate: new Date("2024-02-28"),
7612
+ * },
7613
+ * source: [],
7614
+ * getPrompt: async (symbol, messages) => "Generate strategy",
7615
+ * });
7616
+ *
7617
+ * const optimizers = listOptimizers();
7618
+ * console.log(optimizers);
7619
+ * // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
7620
+ * ```
7621
+ */
7622
+ async function listOptimizerSchema() {
7623
+ engine$1.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
7624
+ return await engine$1.optimizerValidationService.list();
7625
+ }
7626
+
7627
+ const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
7628
+ const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
7629
+ const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
7630
+ /**
7631
+ * Public API utilities for optimizer operations.
7632
+ * Provides high-level methods for strategy generation and code export.
7633
+ *
7634
+ * Usage:
7635
+ * ```typescript
7636
+ * import { Optimizer } from "@backtest-kit/ollama";
7637
+ *
7638
+ * // Get strategy data
7639
+ * const strategies = await Optimizer.getData("BTCUSDT", {
7640
+ * optimizerName: "my-optimizer"
7641
+ * });
7642
+ *
7643
+ * // Generate code
7644
+ * const code = await Optimizer.getCode("BTCUSDT", {
7645
+ * optimizerName: "my-optimizer"
7646
+ * });
7647
+ *
7648
+ * // Save to file
7649
+ * await Optimizer.dump("BTCUSDT", {
7650
+ * optimizerName: "my-optimizer"
7651
+ * }, "./output");
7652
+ * ```
7653
+ */
7654
+ class OptimizerUtils {
7655
+ constructor() {
7656
+ /**
7657
+ * Fetches data from all sources and generates strategy metadata.
7658
+ * Processes each training range and builds LLM conversation history.
7659
+ *
7660
+ * @param symbol - Trading pair symbol
7661
+ * @param context - Context with optimizerName
7662
+ * @returns Array of generated strategies with conversation context
7663
+ * @throws Error if optimizer not found
7664
+ */
7665
+ this.getData = async (symbol, context) => {
7666
+ engine$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
7667
+ symbol,
7668
+ context,
7669
+ });
7670
+ engine$1.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_DATA);
7671
+ return await engine$1.optimizerGlobalService.getData(symbol, context.optimizerName);
7672
+ };
7673
+ /**
7674
+ * Generates complete executable strategy code.
7675
+ * Includes imports, helpers, strategies, walker, and launcher.
7676
+ *
7677
+ * @param symbol - Trading pair symbol
7678
+ * @param context - Context with optimizerName
7679
+ * @returns Generated TypeScript/JavaScript code as string
7680
+ * @throws Error if optimizer not found
7681
+ */
7682
+ this.getCode = async (symbol, context) => {
7683
+ engine$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
7684
+ symbol,
7685
+ context,
7686
+ });
7687
+ engine$1.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_CODE);
7688
+ return await engine$1.optimizerGlobalService.getCode(symbol, context.optimizerName);
7689
+ };
7690
+ /**
7691
+ * Generates and saves strategy code to file.
7692
+ * Creates directory if needed, writes .mjs file.
7693
+ *
7694
+ * Format: `{optimizerName}_{symbol}.mjs`
7695
+ *
7696
+ * @param symbol - Trading pair symbol
7697
+ * @param context - Context with optimizerName
7698
+ * @param path - Output directory path (default: "./")
7699
+ * @throws Error if optimizer not found or file write fails
7700
+ */
7701
+ this.dump = async (symbol, context, path) => {
7702
+ engine$1.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
7703
+ symbol,
7704
+ context,
7705
+ path,
7706
+ });
7707
+ engine$1.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_DUMP);
7708
+ await engine$1.optimizerGlobalService.dump(symbol, context.optimizerName, path);
7709
+ };
7710
+ }
7711
+ }
7712
+ /**
7713
+ * Singleton instance of OptimizerUtils.
7714
+ * Public API for optimizer operations.
7715
+ *
7716
+ * @example
7717
+ * ```typescript
7718
+ * import { Optimizer } from "@backtest-kit/ollama";
7719
+ *
7720
+ * await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
7721
+ * ```
7722
+ */
7723
+ const Optimizer = new OptimizerUtils();
7724
+
7725
+ export { Optimizer, addOptimizerSchema, alibaba, claude, cohere, commitSignalPromptHistory, deepseek, dumpSignalData, getOptimizerSchema, glm4, gpt5, grok, hf, engine as lib, listOptimizerSchema, listenError, listenOptimizerProgress, mistral, ollama, overrideSignalFormat, perplexity, setLogger, validate };