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