backtest-kit 1.5.12 → 1.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
+ import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, and, resolveDocuments, str, iterateDocuments, distinctDocuments, queued, singlerun } from 'functools-kit';
1
2
  import { createActivator } from 'di-kit';
2
3
  import { scoped } from 'di-scoped';
3
- import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, and, resolveDocuments, str, iterateDocuments, distinctDocuments, queued, singlerun } from 'functools-kit';
4
4
  import fs, { mkdir, writeFile } from 'fs/promises';
5
5
  import path, { join } from 'path';
6
6
  import crypto from 'crypto';
@@ -18,12 +18,32 @@ const GLOBAL_CONFIG = {
18
18
  * Default: 5 candles (last 5 minutes when using 1m interval)
19
19
  */
20
20
  CC_AVG_PRICE_CANDLES_COUNT: 5,
21
+ /**
22
+ * Slippage percentage applied to entry and exit prices.
23
+ * Simulates market impact and order book depth.
24
+ * Applied twice (entry and exit) for realistic execution simulation.
25
+ * Default: 0.1% per transaction
26
+ */
27
+ CC_PERCENT_SLIPPAGE: 0.1,
28
+ /**
29
+ * Fee percentage charged per transaction.
30
+ * Applied twice (entry and exit) for total fee calculation.
31
+ * Default: 0.1% per transaction (total 0.2%)
32
+ */
33
+ CC_PERCENT_FEE: 0.1,
21
34
  /**
22
35
  * Minimum TakeProfit distance from priceOpen (percentage)
23
- * Must be greater than trading fees to ensure profitable trades
24
- * Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
36
+ * Must be greater than (slippage + fees) to ensure profitable trades
37
+ *
38
+ * Calculation:
39
+ * - Slippage effect: ~0.2% (0.1% × 2 transactions)
40
+ * - Fees: 0.2% (0.1% × 2 transactions)
41
+ * - Minimum profit buffer: 0.1%
42
+ * - Total: 0.5%
43
+ *
44
+ * Default: 0.5% (covers all costs + minimum profit margin)
25
45
  */
26
- CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
46
+ CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.5,
27
47
  /**
28
48
  * Minimum StopLoss distance from priceOpen (percentage)
29
49
  * Prevents signals from being immediately stopped out due to price volatility
@@ -90,6 +110,7 @@ const GLOBAL_CONFIG = {
90
110
  */
91
111
  CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
92
112
  };
113
+ const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
93
114
 
94
115
  const { init, inject, provide } = createActivator("backtest");
95
116
 
@@ -144,10 +165,12 @@ const schemaServices$1 = {
144
165
  riskSchemaService: Symbol('riskSchemaService'),
145
166
  optimizerSchemaService: Symbol('optimizerSchemaService'),
146
167
  };
168
+ const coreServices$1 = {
169
+ exchangeCoreService: Symbol('exchangeCoreService'),
170
+ strategyCoreService: Symbol('strategyCoreService'),
171
+ frameCoreService: Symbol('frameCoreService'),
172
+ };
147
173
  const globalServices$1 = {
148
- exchangeGlobalService: Symbol('exchangeGlobalService'),
149
- strategyGlobalService: Symbol('strategyGlobalService'),
150
- frameGlobalService: Symbol('frameGlobalService'),
151
174
  sizingGlobalService: Symbol('sizingGlobalService'),
152
175
  riskGlobalService: Symbol('riskGlobalService'),
153
176
  optimizerGlobalService: Symbol('optimizerGlobalService'),
@@ -186,6 +209,7 @@ const validationServices$1 = {
186
209
  sizingValidationService: Symbol('sizingValidationService'),
187
210
  riskValidationService: Symbol('riskValidationService'),
188
211
  optimizerValidationService: Symbol('optimizerValidationService'),
212
+ configValidationService: Symbol('configValidationService'),
189
213
  };
190
214
  const templateServices$1 = {
191
215
  optimizerTemplateService: Symbol('optimizerTemplateService'),
@@ -195,6 +219,7 @@ const TYPES = {
195
219
  ...contextServices$1,
196
220
  ...connectionServices$1,
197
221
  ...schemaServices$1,
222
+ ...coreServices$1,
198
223
  ...globalServices$1,
199
224
  ...commandServices$1,
200
225
  ...logicPrivateServices$1,
@@ -715,16 +740,6 @@ class ExchangeConnectionService {
715
740
  }
716
741
  }
717
742
 
718
- /**
719
- * Slippage percentage applied to entry and exit prices.
720
- * Simulates market impact and order book depth.
721
- */
722
- const PERCENT_SLIPPAGE = 0.1;
723
- /**
724
- * Fee percentage charged per transaction.
725
- * Applied twice (entry and exit) for total fee calculation.
726
- */
727
- const PERCENT_FEE = 0.1;
728
743
  /**
729
744
  * Calculates profit/loss for a closed signal with slippage and fees.
730
745
  *
@@ -760,16 +775,16 @@ const toProfitLossDto = (signal, priceClose) => {
760
775
  let priceCloseWithSlippage;
761
776
  if (signal.position === "long") {
762
777
  // LONG: покупаем дороже, продаем дешевле
763
- priceOpenWithSlippage = priceOpen * (1 + PERCENT_SLIPPAGE / 100);
764
- priceCloseWithSlippage = priceClose * (1 - PERCENT_SLIPPAGE / 100);
778
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
779
+ priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
765
780
  }
766
781
  else {
767
782
  // SHORT: продаем дешевле, покупаем дороже
768
- priceOpenWithSlippage = priceOpen * (1 - PERCENT_SLIPPAGE / 100);
769
- priceCloseWithSlippage = priceClose * (1 + PERCENT_SLIPPAGE / 100);
783
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
784
+ priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
770
785
  }
771
786
  // Применяем комиссию дважды (при открытии и закрытии)
772
- const totalFee = PERCENT_FEE * 2;
787
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
773
788
  let pnlPercentage;
774
789
  if (signal.position === "long") {
775
790
  // LONG: прибыль при росте цены
@@ -1987,15 +2002,6 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1987
2002
  self._lastSignalTimestamp = currentTime;
1988
2003
  }
1989
2004
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1990
- if (await not(self.params.risk.checkSignal({
1991
- symbol: self.params.execution.context.symbol,
1992
- strategyName: self.params.method.context.strategyName,
1993
- exchangeName: self.params.method.context.exchangeName,
1994
- currentPrice,
1995
- timestamp: currentTime,
1996
- }))) {
1997
- return null;
1998
- }
1999
2005
  const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
2000
2006
  const signal = await Promise.race([
2001
2007
  self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
@@ -2010,6 +2016,16 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
2010
2016
  if (self._isStopped) {
2011
2017
  return null;
2012
2018
  }
2019
+ if (await not(self.params.risk.checkSignal({
2020
+ pendingSignal: signal,
2021
+ symbol: self.params.execution.context.symbol,
2022
+ strategyName: self.params.method.context.strategyName,
2023
+ exchangeName: self.params.method.context.exchangeName,
2024
+ currentPrice,
2025
+ timestamp: currentTime,
2026
+ }))) {
2027
+ return null;
2028
+ }
2013
2029
  // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
2014
2030
  if (signal.priceOpen !== undefined) {
2015
2031
  // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
@@ -2241,6 +2257,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2241
2257
  });
2242
2258
  if (await not(self.params.risk.checkSignal({
2243
2259
  symbol: self.params.execution.context.symbol,
2260
+ pendingSignal: scheduled,
2244
2261
  strategyName: self.params.method.context.strategyName,
2245
2262
  exchangeName: self.params.method.context.exchangeName,
2246
2263
  currentPrice: scheduled.priceOpen,
@@ -2324,6 +2341,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2324
2341
  };
2325
2342
  const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2326
2343
  if (await not(self.params.risk.checkSignal({
2344
+ pendingSignal: signal,
2327
2345
  symbol: self.params.execution.context.symbol,
2328
2346
  strategyName: self.params.method.context.strategyName,
2329
2347
  exchangeName: self.params.method.context.exchangeName,
@@ -2550,6 +2568,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2550
2568
  pendingAt: activationTime,
2551
2569
  });
2552
2570
  if (await not(self.params.risk.checkSignal({
2571
+ pendingSignal: scheduled,
2553
2572
  symbol: self.params.execution.context.symbol,
2554
2573
  strategyName: self.params.method.context.strategyName,
2555
2574
  exchangeName: self.params.method.context.exchangeName,
@@ -4011,7 +4030,7 @@ class RiskConnectionService {
4011
4030
  }
4012
4031
  }
4013
4032
 
4014
- const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
4033
+ const METHOD_NAME_VALIDATE$1 = "exchangeCoreService validate";
4015
4034
  /**
4016
4035
  * Global service for exchange operations with execution context injection.
4017
4036
  *
@@ -4020,7 +4039,7 @@ const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
4020
4039
  *
4021
4040
  * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
4022
4041
  */
4023
- class ExchangeGlobalService {
4042
+ class ExchangeCoreService {
4024
4043
  constructor() {
4025
4044
  this.loggerService = inject(TYPES.loggerService);
4026
4045
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
@@ -4050,13 +4069,16 @@ class ExchangeGlobalService {
4050
4069
  * @returns Promise resolving to array of candles
4051
4070
  */
4052
4071
  this.getCandles = async (symbol, interval, limit, when, backtest) => {
4053
- this.loggerService.log("exchangeGlobalService getCandles", {
4072
+ this.loggerService.log("exchangeCoreService getCandles", {
4054
4073
  symbol,
4055
4074
  interval,
4056
4075
  limit,
4057
4076
  when,
4058
4077
  backtest,
4059
4078
  });
4079
+ if (!MethodContextService.hasContext()) {
4080
+ throw new Error("exchangeCoreService getCandles requires a method context");
4081
+ }
4060
4082
  await this.validate(this.methodContextService.context.exchangeName);
4061
4083
  return await ExecutionContextService.runInContext(async () => {
4062
4084
  return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
@@ -4077,13 +4099,16 @@ class ExchangeGlobalService {
4077
4099
  * @returns Promise resolving to array of future candles
4078
4100
  */
4079
4101
  this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
4080
- this.loggerService.log("exchangeGlobalService getNextCandles", {
4102
+ this.loggerService.log("exchangeCoreService getNextCandles", {
4081
4103
  symbol,
4082
4104
  interval,
4083
4105
  limit,
4084
4106
  when,
4085
4107
  backtest,
4086
4108
  });
4109
+ if (!MethodContextService.hasContext()) {
4110
+ throw new Error("exchangeCoreService getNextCandles requires a method context");
4111
+ }
4087
4112
  await this.validate(this.methodContextService.context.exchangeName);
4088
4113
  return await ExecutionContextService.runInContext(async () => {
4089
4114
  return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
@@ -4102,11 +4127,14 @@ class ExchangeGlobalService {
4102
4127
  * @returns Promise resolving to VWAP price
4103
4128
  */
4104
4129
  this.getAveragePrice = async (symbol, when, backtest) => {
4105
- this.loggerService.log("exchangeGlobalService getAveragePrice", {
4130
+ this.loggerService.log("exchangeCoreService getAveragePrice", {
4106
4131
  symbol,
4107
4132
  when,
4108
4133
  backtest,
4109
4134
  });
4135
+ if (!MethodContextService.hasContext()) {
4136
+ throw new Error("exchangeCoreService getAveragePrice requires a method context");
4137
+ }
4110
4138
  await this.validate(this.methodContextService.context.exchangeName);
4111
4139
  return await ExecutionContextService.runInContext(async () => {
4112
4140
  return await this.exchangeConnectionService.getAveragePrice(symbol);
@@ -4126,12 +4154,15 @@ class ExchangeGlobalService {
4126
4154
  * @returns Promise resolving to formatted price string
4127
4155
  */
4128
4156
  this.formatPrice = async (symbol, price, when, backtest) => {
4129
- this.loggerService.log("exchangeGlobalService formatPrice", {
4157
+ this.loggerService.log("exchangeCoreService formatPrice", {
4130
4158
  symbol,
4131
4159
  price,
4132
4160
  when,
4133
4161
  backtest,
4134
4162
  });
4163
+ if (!MethodContextService.hasContext()) {
4164
+ throw new Error("exchangeCoreService formatPrice requires a method context");
4165
+ }
4135
4166
  await this.validate(this.methodContextService.context.exchangeName);
4136
4167
  return await ExecutionContextService.runInContext(async () => {
4137
4168
  return await this.exchangeConnectionService.formatPrice(symbol, price);
@@ -4151,12 +4182,15 @@ class ExchangeGlobalService {
4151
4182
  * @returns Promise resolving to formatted quantity string
4152
4183
  */
4153
4184
  this.formatQuantity = async (symbol, quantity, when, backtest) => {
4154
- this.loggerService.log("exchangeGlobalService formatQuantity", {
4185
+ this.loggerService.log("exchangeCoreService formatQuantity", {
4155
4186
  symbol,
4156
4187
  quantity,
4157
4188
  when,
4158
4189
  backtest,
4159
4190
  });
4191
+ if (!MethodContextService.hasContext()) {
4192
+ throw new Error("exchangeCoreService formatQuantity requires a method context");
4193
+ }
4160
4194
  await this.validate(this.methodContextService.context.exchangeName);
4161
4195
  return await ExecutionContextService.runInContext(async () => {
4162
4196
  return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
@@ -4169,7 +4203,7 @@ class ExchangeGlobalService {
4169
4203
  }
4170
4204
  }
4171
4205
 
4172
- const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
4206
+ const METHOD_NAME_VALIDATE = "strategyCoreService validate";
4173
4207
  /**
4174
4208
  * Global service for strategy operations with execution context injection.
4175
4209
  *
@@ -4178,7 +4212,7 @@ const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
4178
4212
  *
4179
4213
  * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
4180
4214
  */
4181
- class StrategyGlobalService {
4215
+ class StrategyCoreService {
4182
4216
  constructor() {
4183
4217
  this.loggerService = inject(TYPES.loggerService);
4184
4218
  this.strategyConnectionService = inject(TYPES.strategyConnectionService);
@@ -4216,10 +4250,13 @@ class StrategyGlobalService {
4216
4250
  * @returns Promise resolving to pending signal or null
4217
4251
  */
4218
4252
  this.getPendingSignal = async (symbol, strategyName) => {
4219
- this.loggerService.log("strategyGlobalService getPendingSignal", {
4253
+ this.loggerService.log("strategyCoreService getPendingSignal", {
4220
4254
  symbol,
4221
4255
  strategyName,
4222
4256
  });
4257
+ if (!MethodContextService.hasContext()) {
4258
+ throw new Error("strategyCoreService getPendingSignal requires a method context");
4259
+ }
4223
4260
  await this.validate(symbol, strategyName);
4224
4261
  return await this.strategyConnectionService.getPendingSignal(symbol, strategyName);
4225
4262
  };
@@ -4234,10 +4271,13 @@ class StrategyGlobalService {
4234
4271
  * @returns Promise resolving to true if strategy is stopped, false otherwise
4235
4272
  */
4236
4273
  this.getStopped = async (symbol, strategyName) => {
4237
- this.loggerService.log("strategyGlobalService getStopped", {
4274
+ this.loggerService.log("strategyCoreService getStopped", {
4238
4275
  symbol,
4239
4276
  strategyName,
4240
4277
  });
4278
+ if (!MethodContextService.hasContext()) {
4279
+ throw new Error("strategyCoreService getStopped requires a method context");
4280
+ }
4241
4281
  await this.validate(symbol, strategyName);
4242
4282
  return await this.strategyConnectionService.getStopped(symbol, strategyName);
4243
4283
  };
@@ -4253,11 +4293,14 @@ class StrategyGlobalService {
4253
4293
  * @returns Discriminated union of tick result (idle, opened, active, closed)
4254
4294
  */
4255
4295
  this.tick = async (symbol, when, backtest) => {
4256
- this.loggerService.log("strategyGlobalService tick", {
4296
+ this.loggerService.log("strategyCoreService tick", {
4257
4297
  symbol,
4258
4298
  when,
4259
4299
  backtest,
4260
4300
  });
4301
+ if (!MethodContextService.hasContext()) {
4302
+ throw new Error("strategyCoreService tick requires a method context");
4303
+ }
4261
4304
  const strategyName = this.methodContextService.context.strategyName;
4262
4305
  await this.validate(symbol, strategyName);
4263
4306
  return await ExecutionContextService.runInContext(async () => {
@@ -4281,12 +4324,15 @@ class StrategyGlobalService {
4281
4324
  * @returns Closed signal result with PNL
4282
4325
  */
4283
4326
  this.backtest = async (symbol, candles, when, backtest) => {
4284
- this.loggerService.log("strategyGlobalService backtest", {
4327
+ this.loggerService.log("strategyCoreService backtest", {
4285
4328
  symbol,
4286
4329
  candleCount: candles.length,
4287
4330
  when,
4288
4331
  backtest,
4289
4332
  });
4333
+ if (!MethodContextService.hasContext()) {
4334
+ throw new Error("strategyCoreService backtest requires a method context");
4335
+ }
4290
4336
  const strategyName = this.methodContextService.context.strategyName;
4291
4337
  await this.validate(symbol, strategyName);
4292
4338
  return await ExecutionContextService.runInContext(async () => {
@@ -4308,7 +4354,7 @@ class StrategyGlobalService {
4308
4354
  * @returns Promise that resolves when stop flag is set
4309
4355
  */
4310
4356
  this.stop = async (ctx, backtest) => {
4311
- this.loggerService.log("strategyGlobalService stop", {
4357
+ this.loggerService.log("strategyCoreService stop", {
4312
4358
  ctx,
4313
4359
  backtest,
4314
4360
  });
@@ -4324,7 +4370,7 @@ class StrategyGlobalService {
4324
4370
  * @param ctx - Optional context with symbol and strategyName (clears all if not provided)
4325
4371
  */
4326
4372
  this.clear = async (ctx) => {
4327
- this.loggerService.log("strategyGlobalService clear", {
4373
+ this.loggerService.log("strategyCoreService clear", {
4328
4374
  ctx,
4329
4375
  });
4330
4376
  if (ctx) {
@@ -4335,14 +4381,14 @@ class StrategyGlobalService {
4335
4381
  }
4336
4382
  }
4337
4383
 
4338
- const METHOD_NAME_GET_TIMEFRAME = "frameGlobalService getTimeframe";
4384
+ const METHOD_NAME_GET_TIMEFRAME = "frameCoreService getTimeframe";
4339
4385
  /**
4340
4386
  * Global service for frame operations.
4341
4387
  *
4342
4388
  * Wraps FrameConnectionService for timeframe generation.
4343
4389
  * Used internally by BacktestLogicPrivateService.
4344
4390
  */
4345
- class FrameGlobalService {
4391
+ class FrameCoreService {
4346
4392
  constructor() {
4347
4393
  this.loggerService = inject(TYPES.loggerService);
4348
4394
  this.frameConnectionService = inject(TYPES.frameConnectionService);
@@ -4358,6 +4404,9 @@ class FrameGlobalService {
4358
4404
  frameName,
4359
4405
  symbol,
4360
4406
  });
4407
+ if (!MethodContextService.hasContext()) {
4408
+ throw new Error("frameCoreService getTimeframe requires a method context");
4409
+ }
4361
4410
  this.frameValidationService.validate(frameName, METHOD_NAME_GET_TIMEFRAME);
4362
4411
  return await this.frameConnectionService.getTimeframe(symbol, frameName);
4363
4412
  };
@@ -4963,9 +5012,9 @@ class WalkerSchemaService {
4963
5012
  class BacktestLogicPrivateService {
4964
5013
  constructor() {
4965
5014
  this.loggerService = inject(TYPES.loggerService);
4966
- this.strategyGlobalService = inject(TYPES.strategyGlobalService);
4967
- this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
4968
- this.frameGlobalService = inject(TYPES.frameGlobalService);
5015
+ this.strategyCoreService = inject(TYPES.strategyCoreService);
5016
+ this.exchangeCoreService = inject(TYPES.exchangeCoreService);
5017
+ this.frameCoreService = inject(TYPES.frameCoreService);
4969
5018
  this.methodContextService = inject(TYPES.methodContextService);
4970
5019
  }
4971
5020
  /**
@@ -4987,7 +5036,7 @@ class BacktestLogicPrivateService {
4987
5036
  symbol,
4988
5037
  });
4989
5038
  const backtestStartTime = performance.now();
4990
- const timeframes = await this.frameGlobalService.getTimeframe(symbol, this.methodContextService.context.frameName);
5039
+ const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
4991
5040
  const totalFrames = timeframes.length;
4992
5041
  let i = 0;
4993
5042
  let previousEventTimestamp = null;
@@ -5006,7 +5055,7 @@ class BacktestLogicPrivateService {
5006
5055
  });
5007
5056
  }
5008
5057
  // Check if strategy should stop before processing next frame
5009
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5058
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5010
5059
  this.loggerService.info("backtestLogicPrivateService stopped by user request (before tick)", {
5011
5060
  symbol,
5012
5061
  when: when.toISOString(),
@@ -5017,7 +5066,7 @@ class BacktestLogicPrivateService {
5017
5066
  }
5018
5067
  let result;
5019
5068
  try {
5020
- result = await this.strategyGlobalService.tick(symbol, when, true);
5069
+ result = await this.strategyCoreService.tick(symbol, when, true);
5021
5070
  }
5022
5071
  catch (error) {
5023
5072
  console.warn(`backtestLogicPrivateService tick failed, skipping timeframe when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5031,7 +5080,7 @@ class BacktestLogicPrivateService {
5031
5080
  continue;
5032
5081
  }
5033
5082
  // Check if strategy should stop when idle (no active signal)
5034
- if (await and(Promise.resolve(result.action === "idle"), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5083
+ if (await and(Promise.resolve(result.action === "idle"), this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5035
5084
  this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
5036
5085
  symbol,
5037
5086
  when: when.toISOString(),
@@ -5061,7 +5110,7 @@ class BacktestLogicPrivateService {
5061
5110
  const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
5062
5111
  let candles;
5063
5112
  try {
5064
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
5113
+ candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
5065
5114
  }
5066
5115
  catch (error) {
5067
5116
  console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5090,7 +5139,7 @@ class BacktestLogicPrivateService {
5090
5139
  // и если активируется - продолжит с TP/SL мониторингом
5091
5140
  let backtestResult;
5092
5141
  try {
5093
- backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
5142
+ backtestResult = await this.strategyCoreService.backtest(symbol, candles, when, true);
5094
5143
  }
5095
5144
  catch (error) {
5096
5145
  console.warn(`backtestLogicPrivateService backtest failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5133,7 +5182,7 @@ class BacktestLogicPrivateService {
5133
5182
  }
5134
5183
  yield backtestResult;
5135
5184
  // Check if strategy should stop after signal is closed
5136
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5185
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5137
5186
  this.loggerService.info("backtestLogicPrivateService stopped by user request (after scheduled signal closed)", {
5138
5187
  symbol,
5139
5188
  signalId: backtestResult.signal.id,
@@ -5160,7 +5209,7 @@ class BacktestLogicPrivateService {
5160
5209
  const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
5161
5210
  let candles;
5162
5211
  try {
5163
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
5212
+ candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
5164
5213
  }
5165
5214
  catch (error) {
5166
5215
  console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5187,7 +5236,7 @@ class BacktestLogicPrivateService {
5187
5236
  // Вызываем backtest - всегда возвращает closed
5188
5237
  let backtestResult;
5189
5238
  try {
5190
- backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
5239
+ backtestResult = await this.strategyCoreService.backtest(symbol, candles, when, true);
5191
5240
  }
5192
5241
  catch (error) {
5193
5242
  console.warn(`backtestLogicPrivateService backtest failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5226,7 +5275,7 @@ class BacktestLogicPrivateService {
5226
5275
  }
5227
5276
  yield backtestResult;
5228
5277
  // Check if strategy should stop after signal is closed
5229
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5278
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5230
5279
  this.loggerService.info("backtestLogicPrivateService stopped by user request (after signal closed)", {
5231
5280
  symbol,
5232
5281
  signalId: backtestResult.signal.id,
@@ -5300,7 +5349,7 @@ const TICK_TTL = 1 * 60 * 1000 + 1;
5300
5349
  class LiveLogicPrivateService {
5301
5350
  constructor() {
5302
5351
  this.loggerService = inject(TYPES.loggerService);
5303
- this.strategyGlobalService = inject(TYPES.strategyGlobalService);
5352
+ this.strategyCoreService = inject(TYPES.strategyCoreService);
5304
5353
  this.methodContextService = inject(TYPES.methodContextService);
5305
5354
  }
5306
5355
  /**
@@ -5335,7 +5384,7 @@ class LiveLogicPrivateService {
5335
5384
  const when = new Date();
5336
5385
  let result;
5337
5386
  try {
5338
- result = await this.strategyGlobalService.tick(symbol, when, false);
5387
+ result = await this.strategyCoreService.tick(symbol, when, false);
5339
5388
  }
5340
5389
  catch (error) {
5341
5390
  console.warn(`backtestLogicPrivateService tick failed when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5369,7 +5418,7 @@ class LiveLogicPrivateService {
5369
5418
  previousEventTimestamp = currentTimestamp;
5370
5419
  // Check if strategy should stop when idle (no active signal)
5371
5420
  if (result.action === "idle") {
5372
- if (await and(Promise.resolve(true), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5421
+ if (await and(Promise.resolve(true), this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5373
5422
  this.loggerService.info("liveLogicPrivateService stopped by user request (idle state)", {
5374
5423
  symbol,
5375
5424
  when: when.toISOString(),
@@ -5391,7 +5440,7 @@ class LiveLogicPrivateService {
5391
5440
  yield result;
5392
5441
  // Check if strategy should stop after signal is closed
5393
5442
  if (result.action === "closed") {
5394
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5443
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5395
5444
  this.loggerService.info("liveLogicPrivateService stopped by user request (after signal closed)", {
5396
5445
  symbol,
5397
5446
  signalId: result.signal.id,
@@ -8682,8 +8731,27 @@ class HeatMarkdownService {
8682
8731
  }
8683
8732
 
8684
8733
  /**
8685
- * @class ExchangeValidationService
8686
- * Service for managing and validating exchange configurations
8734
+ * Service for managing and validating exchange configurations.
8735
+ *
8736
+ * Maintains a registry of all configured exchanges and validates
8737
+ * their existence before operations. Uses memoization for performance.
8738
+ *
8739
+ * Key features:
8740
+ * - Registry management: addExchange() to register new exchanges
8741
+ * - Validation: validate() ensures exchange exists before use
8742
+ * - Memoization: validation results are cached for performance
8743
+ * - Listing: list() returns all registered exchanges
8744
+ *
8745
+ * @throws {Error} If duplicate exchange name is added
8746
+ * @throws {Error} If unknown exchange is referenced
8747
+ *
8748
+ * @example
8749
+ * ```typescript
8750
+ * const exchangeValidation = new ExchangeValidationService();
8751
+ * exchangeValidation.addExchange("binance", binanceSchema);
8752
+ * exchangeValidation.validate("binance", "backtest"); // OK
8753
+ * exchangeValidation.validate("unknown", "live"); // Throws error
8754
+ * ```
8687
8755
  */
8688
8756
  class ExchangeValidationService {
8689
8757
  constructor() {
@@ -8743,8 +8811,29 @@ class ExchangeValidationService {
8743
8811
  }
8744
8812
 
8745
8813
  /**
8746
- * @class StrategyValidationService
8747
- * Service for managing and validating strategy configurations
8814
+ * Service for managing and validating trading strategy configurations.
8815
+ *
8816
+ * Maintains a registry of all configured strategies, validates their existence
8817
+ * before operations, and ensures associated risk profiles are valid.
8818
+ * Uses memoization for performance.
8819
+ *
8820
+ * Key features:
8821
+ * - Registry management: addStrategy() to register new strategies
8822
+ * - Dual validation: validates both strategy existence and risk profile (if configured)
8823
+ * - Memoization: validation results are cached for performance
8824
+ * - Listing: list() returns all registered strategies
8825
+ *
8826
+ * @throws {Error} If duplicate strategy name is added
8827
+ * @throws {Error} If unknown strategy is referenced
8828
+ * @throws {Error} If strategy's risk profile doesn't exist
8829
+ *
8830
+ * @example
8831
+ * ```typescript
8832
+ * const strategyValidation = new StrategyValidationService();
8833
+ * strategyValidation.addStrategy("momentum-btc", { ...schema, riskName: "conservative" });
8834
+ * strategyValidation.validate("momentum-btc", "backtest"); // Validates strategy + risk
8835
+ * strategyValidation.validate("unknown", "live"); // Throws error
8836
+ * ```
8748
8837
  */
8749
8838
  class StrategyValidationService {
8750
8839
  constructor() {
@@ -8815,8 +8904,27 @@ class StrategyValidationService {
8815
8904
  }
8816
8905
 
8817
8906
  /**
8818
- * @class FrameValidationService
8819
- * Service for managing and validating frame configurations
8907
+ * Service for managing and validating frame (timeframe) configurations.
8908
+ *
8909
+ * Maintains a registry of all configured frames and validates
8910
+ * their existence before operations. Uses memoization for performance.
8911
+ *
8912
+ * Key features:
8913
+ * - Registry management: addFrame() to register new timeframes
8914
+ * - Validation: validate() ensures frame exists before use
8915
+ * - Memoization: validation results are cached for performance
8916
+ * - Listing: list() returns all registered frames
8917
+ *
8918
+ * @throws {Error} If duplicate frame name is added
8919
+ * @throws {Error} If unknown frame is referenced
8920
+ *
8921
+ * @example
8922
+ * ```typescript
8923
+ * const frameValidation = new FrameValidationService();
8924
+ * frameValidation.addFrame("2024-Q1", frameSchema);
8925
+ * frameValidation.validate("2024-Q1", "backtest"); // OK
8926
+ * frameValidation.validate("unknown", "live"); // Throws error
8927
+ * ```
8820
8928
  */
8821
8929
  class FrameValidationService {
8822
8930
  constructor() {
@@ -8876,8 +8984,29 @@ class FrameValidationService {
8876
8984
  }
8877
8985
 
8878
8986
  /**
8879
- * @class WalkerValidationService
8880
- * Service for managing and validating walker configurations
8987
+ * Service for managing and validating walker (parameter sweep) configurations.
8988
+ *
8989
+ * Maintains a registry of all configured walkers and validates
8990
+ * their existence before operations. Uses memoization for performance.
8991
+ *
8992
+ * Walkers define parameter ranges for optimization and hyperparameter tuning.
8993
+ *
8994
+ * Key features:
8995
+ * - Registry management: addWalker() to register new walker configurations
8996
+ * - Validation: validate() ensures walker exists before use
8997
+ * - Memoization: validation results are cached for performance
8998
+ * - Listing: list() returns all registered walkers
8999
+ *
9000
+ * @throws {Error} If duplicate walker name is added
9001
+ * @throws {Error} If unknown walker is referenced
9002
+ *
9003
+ * @example
9004
+ * ```typescript
9005
+ * const walkerValidation = new WalkerValidationService();
9006
+ * walkerValidation.addWalker("rsi-sweep", walkerSchema);
9007
+ * walkerValidation.validate("rsi-sweep", "optimizer"); // OK
9008
+ * walkerValidation.validate("unknown", "optimizer"); // Throws error
9009
+ * ```
8881
9010
  */
8882
9011
  class WalkerValidationService {
8883
9012
  constructor() {
@@ -8937,8 +9066,27 @@ class WalkerValidationService {
8937
9066
  }
8938
9067
 
8939
9068
  /**
8940
- * @class SizingValidationService
8941
- * Service for managing and validating sizing configurations
9069
+ * Service for managing and validating position sizing configurations.
9070
+ *
9071
+ * Maintains a registry of all configured sizing strategies and validates
9072
+ * their existence before operations. Uses memoization for performance.
9073
+ *
9074
+ * Key features:
9075
+ * - Registry management: addSizing() to register new sizing strategies
9076
+ * - Validation: validate() ensures sizing strategy exists before use
9077
+ * - Memoization: validation results are cached for performance
9078
+ * - Listing: list() returns all registered sizing strategies
9079
+ *
9080
+ * @throws {Error} If duplicate sizing name is added
9081
+ * @throws {Error} If unknown sizing strategy is referenced
9082
+ *
9083
+ * @example
9084
+ * ```typescript
9085
+ * const sizingValidation = new SizingValidationService();
9086
+ * sizingValidation.addSizing("fixed-1000", fixedSizingSchema);
9087
+ * sizingValidation.validate("fixed-1000", "strategy-1"); // OK
9088
+ * sizingValidation.validate("unknown", "strategy-2"); // Throws error
9089
+ * ```
8942
9090
  */
8943
9091
  class SizingValidationService {
8944
9092
  constructor() {
@@ -9003,8 +9151,27 @@ class SizingValidationService {
9003
9151
  }
9004
9152
 
9005
9153
  /**
9006
- * @class RiskValidationService
9007
- * Service for managing and validating risk configurations
9154
+ * Service for managing and validating risk management configurations.
9155
+ *
9156
+ * Maintains a registry of all configured risk profiles and validates
9157
+ * their existence before operations. Uses memoization for performance.
9158
+ *
9159
+ * Key features:
9160
+ * - Registry management: addRisk() to register new risk profiles
9161
+ * - Validation: validate() ensures risk profile exists before use
9162
+ * - Memoization: validation results are cached by riskName:source for performance
9163
+ * - Listing: list() returns all registered risk profiles
9164
+ *
9165
+ * @throws {Error} If duplicate risk name is added
9166
+ * @throws {Error} If unknown risk profile is referenced
9167
+ *
9168
+ * @example
9169
+ * ```typescript
9170
+ * const riskValidation = new RiskValidationService();
9171
+ * riskValidation.addRisk("conservative", conservativeSchema);
9172
+ * riskValidation.validate("conservative", "strategy-1"); // OK
9173
+ * riskValidation.validate("unknown", "strategy-2"); // Throws error
9174
+ * ```
9008
9175
  */
9009
9176
  class RiskValidationService {
9010
9177
  constructor() {
@@ -11556,6 +11723,133 @@ class OutlineMarkdownService {
11556
11723
  }
11557
11724
  }
11558
11725
 
11726
+ /**
11727
+ * Service for validating GLOBAL_CONFIG parameters to ensure mathematical correctness
11728
+ * and prevent unprofitable trading configurations.
11729
+ *
11730
+ * Performs comprehensive validation on:
11731
+ * - **Percentage parameters**: Slippage, fees, and profit margins must be non-negative
11732
+ * - **Economic viability**: Ensures CC_MIN_TAKEPROFIT_DISTANCE_PERCENT covers all trading costs
11733
+ * (slippage + fees) to guarantee profitable trades when TakeProfit is hit
11734
+ * - **Range constraints**: Validates MIN < MAX relationships (e.g., StopLoss distances)
11735
+ * - **Time-based parameters**: Ensures positive integer values for timeouts and lifetimes
11736
+ * - **Candle parameters**: Validates retry counts, delays, and anomaly detection thresholds
11737
+ *
11738
+ * @throws {Error} If any validation fails, throws with detailed breakdown of all errors
11739
+ *
11740
+ * @example
11741
+ * ```typescript
11742
+ * const validator = new ConfigValidationService();
11743
+ * validator.validate(); // Throws if config is invalid
11744
+ * ```
11745
+ *
11746
+ * @example Validation failure output:
11747
+ * ```
11748
+ * GLOBAL_CONFIG validation failed:
11749
+ * 1. CC_MIN_TAKEPROFIT_DISTANCE_PERCENT (0.3%) is too low to cover trading costs.
11750
+ * Required minimum: 0.40%
11751
+ * Breakdown:
11752
+ * - Slippage effect: 0.20% (0.1% × 2 transactions)
11753
+ * - Fees: 0.20% (0.1% × 2 transactions)
11754
+ * All TakeProfit signals will be unprofitable with current settings!
11755
+ * ```
11756
+ */
11757
+ class ConfigValidationService {
11758
+ constructor() {
11759
+ /**
11760
+ * @private
11761
+ * @readonly
11762
+ * Injected logger service instance
11763
+ */
11764
+ this.loggerService = inject(TYPES.loggerService);
11765
+ /**
11766
+ * Validates GLOBAL_CONFIG parameters for mathematical correctness.
11767
+ *
11768
+ * Checks:
11769
+ * 1. CC_MIN_TAKEPROFIT_DISTANCE_PERCENT must cover slippage + fees
11770
+ * 2. All percentage values must be positive
11771
+ * 3. Time/count values must be positive integers
11772
+ *
11773
+ * @throws Error if configuration is invalid
11774
+ */
11775
+ this.validate = () => {
11776
+ this.loggerService.log("configValidationService validate");
11777
+ const errors = [];
11778
+ // Validate slippage and fee percentages
11779
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE) || GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE < 0) {
11780
+ errors.push(`CC_PERCENT_SLIPPAGE must be a non-negative number, got ${GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE}`);
11781
+ }
11782
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_PERCENT_FEE) || GLOBAL_CONFIG.CC_PERCENT_FEE < 0) {
11783
+ errors.push(`CC_PERCENT_FEE must be a non-negative number, got ${GLOBAL_CONFIG.CC_PERCENT_FEE}`);
11784
+ }
11785
+ // Calculate minimum required TP distance to cover costs
11786
+ const slippageEffect = GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE * 2; // Applied twice (entry + exit)
11787
+ const feesTotal = GLOBAL_CONFIG.CC_PERCENT_FEE * 2; // Applied twice (entry + exit)
11788
+ const minRequiredTpDistance = slippageEffect + feesTotal;
11789
+ // Validate CC_MIN_TAKEPROFIT_DISTANCE_PERCENT
11790
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT <= 0) {
11791
+ errors.push(`CC_MIN_TAKEPROFIT_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}`);
11792
+ }
11793
+ else if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT < minRequiredTpDistance) {
11794
+ errors.push(`CC_MIN_TAKEPROFIT_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}%) is too low to cover trading costs.\n` +
11795
+ ` Required minimum: ${minRequiredTpDistance.toFixed(2)}%\n` +
11796
+ ` Breakdown:\n` +
11797
+ ` - Slippage effect: ${slippageEffect.toFixed(2)}% (${GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE}% × 2 transactions)\n` +
11798
+ ` - Fees: ${feesTotal.toFixed(2)}% (${GLOBAL_CONFIG.CC_PERCENT_FEE}% × 2 transactions)\n` +
11799
+ ` All TakeProfit signals will be unprofitable with current settings!`);
11800
+ }
11801
+ // Validate CC_MIN_STOPLOSS_DISTANCE_PERCENT
11802
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT <= 0) {
11803
+ errors.push(`CC_MIN_STOPLOSS_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}`);
11804
+ }
11805
+ // Validate CC_MAX_STOPLOSS_DISTANCE_PERCENT
11806
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT <= 0) {
11807
+ errors.push(`CC_MAX_STOPLOSS_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}`);
11808
+ }
11809
+ // Validate that MIN < MAX for StopLoss
11810
+ if (Number.isFinite(GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) &&
11811
+ Number.isFinite(GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) &&
11812
+ GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT >= GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
11813
+ errors.push(`CC_MIN_STOPLOSS_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}%) must be less than ` +
11814
+ `CC_MAX_STOPLOSS_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}%)`);
11815
+ }
11816
+ // Validate time-based parameters
11817
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES) || GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES <= 0) {
11818
+ errors.push(`CC_SCHEDULE_AWAIT_MINUTES must be a positive integer, got ${GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES}`);
11819
+ }
11820
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) || GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES <= 0) {
11821
+ errors.push(`CC_MAX_SIGNAL_LIFETIME_MINUTES must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES}`);
11822
+ }
11823
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS) || GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS <= 0) {
11824
+ errors.push(`CC_MAX_SIGNAL_GENERATION_SECONDS must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS}`);
11825
+ }
11826
+ // Validate candle-based parameters
11827
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT) || GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT <= 0) {
11828
+ errors.push(`CC_AVG_PRICE_CANDLES_COUNT must be a positive integer, got ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT}`);
11829
+ }
11830
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT) || GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT < 0) {
11831
+ errors.push(`CC_GET_CANDLES_RETRY_COUNT must be a non-negative integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT}`);
11832
+ }
11833
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS) || GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS < 0) {
11834
+ errors.push(`CC_GET_CANDLES_RETRY_DELAY_MS must be a non-negative integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS}`);
11835
+ }
11836
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR) || GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR <= 0) {
11837
+ errors.push(`CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR}`);
11838
+ }
11839
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) || GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN <= 0) {
11840
+ errors.push(`CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN}`);
11841
+ }
11842
+ // Throw aggregated errors if any
11843
+ if (errors.length > 0) {
11844
+ const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
11845
+ this.loggerService.warn(errorMessage);
11846
+ throw new Error(errorMessage);
11847
+ }
11848
+ this.loggerService.log("configValidationService validation passed");
11849
+ };
11850
+ }
11851
+ }
11852
+
11559
11853
  {
11560
11854
  provide(TYPES.loggerService, () => new LoggerService());
11561
11855
  }
@@ -11582,9 +11876,11 @@ class OutlineMarkdownService {
11582
11876
  provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
11583
11877
  }
11584
11878
  {
11585
- provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
11586
- provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
11587
- provide(TYPES.frameGlobalService, () => new FrameGlobalService());
11879
+ provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
11880
+ provide(TYPES.strategyCoreService, () => new StrategyCoreService());
11881
+ provide(TYPES.frameCoreService, () => new FrameCoreService());
11882
+ }
11883
+ {
11588
11884
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
11589
11885
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
11590
11886
  provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
@@ -11623,6 +11919,7 @@ class OutlineMarkdownService {
11623
11919
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
11624
11920
  provide(TYPES.riskValidationService, () => new RiskValidationService());
11625
11921
  provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
11922
+ provide(TYPES.configValidationService, () => new ConfigValidationService());
11626
11923
  }
11627
11924
  {
11628
11925
  provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
@@ -11653,10 +11950,12 @@ const schemaServices = {
11653
11950
  riskSchemaService: inject(TYPES.riskSchemaService),
11654
11951
  optimizerSchemaService: inject(TYPES.optimizerSchemaService),
11655
11952
  };
11953
+ const coreServices = {
11954
+ exchangeCoreService: inject(TYPES.exchangeCoreService),
11955
+ strategyCoreService: inject(TYPES.strategyCoreService),
11956
+ frameCoreService: inject(TYPES.frameCoreService),
11957
+ };
11656
11958
  const globalServices = {
11657
- exchangeGlobalService: inject(TYPES.exchangeGlobalService),
11658
- strategyGlobalService: inject(TYPES.strategyGlobalService),
11659
- frameGlobalService: inject(TYPES.frameGlobalService),
11660
11959
  sizingGlobalService: inject(TYPES.sizingGlobalService),
11661
11960
  riskGlobalService: inject(TYPES.riskGlobalService),
11662
11961
  optimizerGlobalService: inject(TYPES.optimizerGlobalService),
@@ -11695,6 +11994,7 @@ const validationServices = {
11695
11994
  sizingValidationService: inject(TYPES.sizingValidationService),
11696
11995
  riskValidationService: inject(TYPES.riskValidationService),
11697
11996
  optimizerValidationService: inject(TYPES.optimizerValidationService),
11997
+ configValidationService: inject(TYPES.configValidationService),
11698
11998
  };
11699
11999
  const templateServices = {
11700
12000
  optimizerTemplateService: inject(TYPES.optimizerTemplateService),
@@ -11704,6 +12004,7 @@ const backtest = {
11704
12004
  ...contextServices,
11705
12005
  ...connectionServices,
11706
12006
  ...schemaServices,
12007
+ ...coreServices,
11707
12008
  ...globalServices,
11708
12009
  ...commandServices,
11709
12010
  ...logicPrivateServices,
@@ -11732,12 +12033,13 @@ var backtest$1 = backtest;
11732
12033
  * });
11733
12034
  * ```
11734
12035
  */
11735
- async function setLogger(logger) {
12036
+ function setLogger(logger) {
11736
12037
  backtest$1.loggerService.setLogger(logger);
11737
12038
  }
11738
12039
  /**
11739
12040
  * Sets global configuration parameters for the framework.
11740
12041
  * @param config - Partial configuration object to override default settings
12042
+ * @param _unsafe - Skip config validations - required for testbed
11741
12043
  *
11742
12044
  * @example
11743
12045
  * ```typescript
@@ -11746,8 +12048,51 @@ async function setLogger(logger) {
11746
12048
  * });
11747
12049
  * ```
11748
12050
  */
11749
- async function setConfig(config) {
11750
- Object.assign(GLOBAL_CONFIG, config);
12051
+ function setConfig(config, _unsafe) {
12052
+ const prevConfig = Object.assign({}, GLOBAL_CONFIG);
12053
+ try {
12054
+ Object.assign(GLOBAL_CONFIG, config);
12055
+ !_unsafe && backtest$1.configValidationService.validate();
12056
+ }
12057
+ catch (error) {
12058
+ console.warn(`backtest-kit setConfig failed: ${getErrorMessage(error)}`, config);
12059
+ Object.assign(GLOBAL_CONFIG, prevConfig);
12060
+ throw error;
12061
+ }
12062
+ }
12063
+ /**
12064
+ * Retrieves a copy of the current global configuration.
12065
+ *
12066
+ * Returns a shallow copy of the current GLOBAL_CONFIG to prevent accidental mutations.
12067
+ * Use this to inspect the current configuration state without modifying it.
12068
+ *
12069
+ * @returns {GlobalConfig} A copy of the current global configuration object
12070
+ *
12071
+ * @example
12072
+ * ```typescript
12073
+ * const currentConfig = getConfig();
12074
+ * console.log(currentConfig.CC_SCHEDULE_AWAIT_MINUTES);
12075
+ * ```
12076
+ */
12077
+ function getConfig() {
12078
+ return Object.assign({}, GLOBAL_CONFIG);
12079
+ }
12080
+ /**
12081
+ * Retrieves the default configuration object for the framework.
12082
+ *
12083
+ * Returns a reference to the default configuration with all preset values.
12084
+ * Use this to see what configuration options are available and their default values.
12085
+ *
12086
+ * @returns {GlobalConfig} The default configuration object
12087
+ *
12088
+ * @example
12089
+ * ```typescript
12090
+ * const defaultConfig = getDefaultConfig();
12091
+ * console.log(defaultConfig.CC_SCHEDULE_AWAIT_MINUTES);
12092
+ * ```
12093
+ */
12094
+ function getDefaultConfig() {
12095
+ return DEFAULT_CONFIG;
11751
12096
  }
11752
12097
 
11753
12098
  const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
@@ -13557,7 +13902,7 @@ class BacktestInstance {
13557
13902
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName });
13558
13903
  }
13559
13904
  {
13560
- backtest$1.strategyGlobalService.clear({ symbol, strategyName: context.strategyName });
13905
+ backtest$1.strategyCoreService.clear({ symbol, strategyName: context.strategyName });
13561
13906
  }
13562
13907
  {
13563
13908
  const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -13592,8 +13937,8 @@ class BacktestInstance {
13592
13937
  });
13593
13938
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
13594
13939
  return () => {
13595
- backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, true);
13596
- backtest$1.strategyGlobalService
13940
+ backtest$1.strategyCoreService.stop({ symbol, strategyName: context.strategyName }, true);
13941
+ backtest$1.strategyCoreService
13597
13942
  .getPendingSignal(symbol, context.strategyName)
13598
13943
  .then(async (pendingSignal) => {
13599
13944
  if (pendingSignal) {
@@ -13634,7 +13979,7 @@ class BacktestInstance {
13634
13979
  symbol,
13635
13980
  strategyName,
13636
13981
  });
13637
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
13982
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
13638
13983
  };
13639
13984
  /**
13640
13985
  * Gets statistical data from all closed signals for a symbol-strategy pair.
@@ -14049,7 +14394,7 @@ class LiveInstance {
14049
14394
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName });
14050
14395
  }
14051
14396
  {
14052
- backtest$1.strategyGlobalService.clear({ symbol, strategyName: context.strategyName });
14397
+ backtest$1.strategyCoreService.clear({ symbol, strategyName: context.strategyName });
14053
14398
  }
14054
14399
  {
14055
14400
  const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -14084,8 +14429,8 @@ class LiveInstance {
14084
14429
  });
14085
14430
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
14086
14431
  return () => {
14087
- backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, false);
14088
- backtest$1.strategyGlobalService
14432
+ backtest$1.strategyCoreService.stop({ symbol, strategyName: context.strategyName }, false);
14433
+ backtest$1.strategyCoreService
14089
14434
  .getPendingSignal(symbol, context.strategyName)
14090
14435
  .then(async (pendingSignal) => {
14091
14436
  if (pendingSignal) {
@@ -14126,7 +14471,7 @@ class LiveInstance {
14126
14471
  symbol,
14127
14472
  strategyName,
14128
14473
  });
14129
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, false);
14474
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, false);
14130
14475
  };
14131
14476
  /**
14132
14477
  * Gets statistical data from all live trading events for a symbol-strategy pair.
@@ -14807,7 +15152,7 @@ class WalkerInstance {
14807
15152
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName });
14808
15153
  }
14809
15154
  {
14810
- backtest$1.strategyGlobalService.clear({ symbol, strategyName });
15155
+ backtest$1.strategyCoreService.clear({ symbol, strategyName });
14811
15156
  }
14812
15157
  {
14813
15158
  const { riskName } = backtest$1.strategySchemaService.get(strategyName);
@@ -14847,7 +15192,7 @@ class WalkerInstance {
14847
15192
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
14848
15193
  return () => {
14849
15194
  for (const strategyName of walkerSchema.strategies) {
14850
- backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
15195
+ backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
14851
15196
  walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
14852
15197
  }
14853
15198
  if (!this._isDone) {
@@ -14893,7 +15238,7 @@ class WalkerInstance {
14893
15238
  const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
14894
15239
  for (const strategyName of walkerSchema.strategies) {
14895
15240
  await walkerStopSubject.next({ symbol, strategyName, walkerName });
14896
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
15241
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
14897
15242
  }
14898
15243
  };
14899
15244
  /**
@@ -15836,4 +16181,4 @@ class ConstantUtils {
15836
16181
  */
15837
16182
  const Constant = new ConstantUtils();
15838
16183
 
15839
- export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
16184
+ export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getConfig, getDate, getDefaultConfig, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };