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.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ var functoolsKit = require('functools-kit');
3
4
  var diKit = require('di-kit');
4
5
  var diScoped = require('di-scoped');
5
- var functoolsKit = require('functools-kit');
6
6
  var fs = require('fs/promises');
7
7
  var path = require('path');
8
8
  var crypto = require('crypto');
@@ -20,12 +20,32 @@ const GLOBAL_CONFIG = {
20
20
  * Default: 5 candles (last 5 minutes when using 1m interval)
21
21
  */
22
22
  CC_AVG_PRICE_CANDLES_COUNT: 5,
23
+ /**
24
+ * Slippage percentage applied to entry and exit prices.
25
+ * Simulates market impact and order book depth.
26
+ * Applied twice (entry and exit) for realistic execution simulation.
27
+ * Default: 0.1% per transaction
28
+ */
29
+ CC_PERCENT_SLIPPAGE: 0.1,
30
+ /**
31
+ * Fee percentage charged per transaction.
32
+ * Applied twice (entry and exit) for total fee calculation.
33
+ * Default: 0.1% per transaction (total 0.2%)
34
+ */
35
+ CC_PERCENT_FEE: 0.1,
23
36
  /**
24
37
  * Minimum TakeProfit distance from priceOpen (percentage)
25
- * Must be greater than trading fees to ensure profitable trades
26
- * Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
38
+ * Must be greater than (slippage + fees) to ensure profitable trades
39
+ *
40
+ * Calculation:
41
+ * - Slippage effect: ~0.2% (0.1% × 2 transactions)
42
+ * - Fees: 0.2% (0.1% × 2 transactions)
43
+ * - Minimum profit buffer: 0.1%
44
+ * - Total: 0.5%
45
+ *
46
+ * Default: 0.5% (covers all costs + minimum profit margin)
27
47
  */
28
- CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
48
+ CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.5,
29
49
  /**
30
50
  * Minimum StopLoss distance from priceOpen (percentage)
31
51
  * Prevents signals from being immediately stopped out due to price volatility
@@ -92,6 +112,7 @@ const GLOBAL_CONFIG = {
92
112
  */
93
113
  CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
94
114
  };
115
+ const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
95
116
 
96
117
  const { init, inject, provide } = diKit.createActivator("backtest");
97
118
 
@@ -146,10 +167,12 @@ const schemaServices$1 = {
146
167
  riskSchemaService: Symbol('riskSchemaService'),
147
168
  optimizerSchemaService: Symbol('optimizerSchemaService'),
148
169
  };
170
+ const coreServices$1 = {
171
+ exchangeCoreService: Symbol('exchangeCoreService'),
172
+ strategyCoreService: Symbol('strategyCoreService'),
173
+ frameCoreService: Symbol('frameCoreService'),
174
+ };
149
175
  const globalServices$1 = {
150
- exchangeGlobalService: Symbol('exchangeGlobalService'),
151
- strategyGlobalService: Symbol('strategyGlobalService'),
152
- frameGlobalService: Symbol('frameGlobalService'),
153
176
  sizingGlobalService: Symbol('sizingGlobalService'),
154
177
  riskGlobalService: Symbol('riskGlobalService'),
155
178
  optimizerGlobalService: Symbol('optimizerGlobalService'),
@@ -188,6 +211,7 @@ const validationServices$1 = {
188
211
  sizingValidationService: Symbol('sizingValidationService'),
189
212
  riskValidationService: Symbol('riskValidationService'),
190
213
  optimizerValidationService: Symbol('optimizerValidationService'),
214
+ configValidationService: Symbol('configValidationService'),
191
215
  };
192
216
  const templateServices$1 = {
193
217
  optimizerTemplateService: Symbol('optimizerTemplateService'),
@@ -197,6 +221,7 @@ const TYPES = {
197
221
  ...contextServices$1,
198
222
  ...connectionServices$1,
199
223
  ...schemaServices$1,
224
+ ...coreServices$1,
200
225
  ...globalServices$1,
201
226
  ...commandServices$1,
202
227
  ...logicPrivateServices$1,
@@ -717,16 +742,6 @@ class ExchangeConnectionService {
717
742
  }
718
743
  }
719
744
 
720
- /**
721
- * Slippage percentage applied to entry and exit prices.
722
- * Simulates market impact and order book depth.
723
- */
724
- const PERCENT_SLIPPAGE = 0.1;
725
- /**
726
- * Fee percentage charged per transaction.
727
- * Applied twice (entry and exit) for total fee calculation.
728
- */
729
- const PERCENT_FEE = 0.1;
730
745
  /**
731
746
  * Calculates profit/loss for a closed signal with slippage and fees.
732
747
  *
@@ -762,16 +777,16 @@ const toProfitLossDto = (signal, priceClose) => {
762
777
  let priceCloseWithSlippage;
763
778
  if (signal.position === "long") {
764
779
  // LONG: покупаем дороже, продаем дешевле
765
- priceOpenWithSlippage = priceOpen * (1 + PERCENT_SLIPPAGE / 100);
766
- priceCloseWithSlippage = priceClose * (1 - PERCENT_SLIPPAGE / 100);
780
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
781
+ priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
767
782
  }
768
783
  else {
769
784
  // SHORT: продаем дешевле, покупаем дороже
770
- priceOpenWithSlippage = priceOpen * (1 - PERCENT_SLIPPAGE / 100);
771
- priceCloseWithSlippage = priceClose * (1 + PERCENT_SLIPPAGE / 100);
785
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
786
+ priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
772
787
  }
773
788
  // Применяем комиссию дважды (при открытии и закрытии)
774
- const totalFee = PERCENT_FEE * 2;
789
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
775
790
  let pnlPercentage;
776
791
  if (signal.position === "long") {
777
792
  // LONG: прибыль при росте цены
@@ -1989,15 +2004,6 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1989
2004
  self._lastSignalTimestamp = currentTime;
1990
2005
  }
1991
2006
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1992
- if (await functoolsKit.not(self.params.risk.checkSignal({
1993
- symbol: self.params.execution.context.symbol,
1994
- strategyName: self.params.method.context.strategyName,
1995
- exchangeName: self.params.method.context.exchangeName,
1996
- currentPrice,
1997
- timestamp: currentTime,
1998
- }))) {
1999
- return null;
2000
- }
2001
2007
  const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
2002
2008
  const signal = await Promise.race([
2003
2009
  self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
@@ -2012,6 +2018,16 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
2012
2018
  if (self._isStopped) {
2013
2019
  return null;
2014
2020
  }
2021
+ if (await functoolsKit.not(self.params.risk.checkSignal({
2022
+ pendingSignal: signal,
2023
+ symbol: self.params.execution.context.symbol,
2024
+ strategyName: self.params.method.context.strategyName,
2025
+ exchangeName: self.params.method.context.exchangeName,
2026
+ currentPrice,
2027
+ timestamp: currentTime,
2028
+ }))) {
2029
+ return null;
2030
+ }
2015
2031
  // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
2016
2032
  if (signal.priceOpen !== undefined) {
2017
2033
  // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
@@ -2243,6 +2259,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2243
2259
  });
2244
2260
  if (await functoolsKit.not(self.params.risk.checkSignal({
2245
2261
  symbol: self.params.execution.context.symbol,
2262
+ pendingSignal: scheduled,
2246
2263
  strategyName: self.params.method.context.strategyName,
2247
2264
  exchangeName: self.params.method.context.exchangeName,
2248
2265
  currentPrice: scheduled.priceOpen,
@@ -2326,6 +2343,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2326
2343
  };
2327
2344
  const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2328
2345
  if (await functoolsKit.not(self.params.risk.checkSignal({
2346
+ pendingSignal: signal,
2329
2347
  symbol: self.params.execution.context.symbol,
2330
2348
  strategyName: self.params.method.context.strategyName,
2331
2349
  exchangeName: self.params.method.context.exchangeName,
@@ -2552,6 +2570,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2552
2570
  pendingAt: activationTime,
2553
2571
  });
2554
2572
  if (await functoolsKit.not(self.params.risk.checkSignal({
2573
+ pendingSignal: scheduled,
2555
2574
  symbol: self.params.execution.context.symbol,
2556
2575
  strategyName: self.params.method.context.strategyName,
2557
2576
  exchangeName: self.params.method.context.exchangeName,
@@ -4013,7 +4032,7 @@ class RiskConnectionService {
4013
4032
  }
4014
4033
  }
4015
4034
 
4016
- const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
4035
+ const METHOD_NAME_VALIDATE$1 = "exchangeCoreService validate";
4017
4036
  /**
4018
4037
  * Global service for exchange operations with execution context injection.
4019
4038
  *
@@ -4022,7 +4041,7 @@ const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
4022
4041
  *
4023
4042
  * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
4024
4043
  */
4025
- class ExchangeGlobalService {
4044
+ class ExchangeCoreService {
4026
4045
  constructor() {
4027
4046
  this.loggerService = inject(TYPES.loggerService);
4028
4047
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
@@ -4052,13 +4071,16 @@ class ExchangeGlobalService {
4052
4071
  * @returns Promise resolving to array of candles
4053
4072
  */
4054
4073
  this.getCandles = async (symbol, interval, limit, when, backtest) => {
4055
- this.loggerService.log("exchangeGlobalService getCandles", {
4074
+ this.loggerService.log("exchangeCoreService getCandles", {
4056
4075
  symbol,
4057
4076
  interval,
4058
4077
  limit,
4059
4078
  when,
4060
4079
  backtest,
4061
4080
  });
4081
+ if (!MethodContextService.hasContext()) {
4082
+ throw new Error("exchangeCoreService getCandles requires a method context");
4083
+ }
4062
4084
  await this.validate(this.methodContextService.context.exchangeName);
4063
4085
  return await ExecutionContextService.runInContext(async () => {
4064
4086
  return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
@@ -4079,13 +4101,16 @@ class ExchangeGlobalService {
4079
4101
  * @returns Promise resolving to array of future candles
4080
4102
  */
4081
4103
  this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
4082
- this.loggerService.log("exchangeGlobalService getNextCandles", {
4104
+ this.loggerService.log("exchangeCoreService getNextCandles", {
4083
4105
  symbol,
4084
4106
  interval,
4085
4107
  limit,
4086
4108
  when,
4087
4109
  backtest,
4088
4110
  });
4111
+ if (!MethodContextService.hasContext()) {
4112
+ throw new Error("exchangeCoreService getNextCandles requires a method context");
4113
+ }
4089
4114
  await this.validate(this.methodContextService.context.exchangeName);
4090
4115
  return await ExecutionContextService.runInContext(async () => {
4091
4116
  return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
@@ -4104,11 +4129,14 @@ class ExchangeGlobalService {
4104
4129
  * @returns Promise resolving to VWAP price
4105
4130
  */
4106
4131
  this.getAveragePrice = async (symbol, when, backtest) => {
4107
- this.loggerService.log("exchangeGlobalService getAveragePrice", {
4132
+ this.loggerService.log("exchangeCoreService getAveragePrice", {
4108
4133
  symbol,
4109
4134
  when,
4110
4135
  backtest,
4111
4136
  });
4137
+ if (!MethodContextService.hasContext()) {
4138
+ throw new Error("exchangeCoreService getAveragePrice requires a method context");
4139
+ }
4112
4140
  await this.validate(this.methodContextService.context.exchangeName);
4113
4141
  return await ExecutionContextService.runInContext(async () => {
4114
4142
  return await this.exchangeConnectionService.getAveragePrice(symbol);
@@ -4128,12 +4156,15 @@ class ExchangeGlobalService {
4128
4156
  * @returns Promise resolving to formatted price string
4129
4157
  */
4130
4158
  this.formatPrice = async (symbol, price, when, backtest) => {
4131
- this.loggerService.log("exchangeGlobalService formatPrice", {
4159
+ this.loggerService.log("exchangeCoreService formatPrice", {
4132
4160
  symbol,
4133
4161
  price,
4134
4162
  when,
4135
4163
  backtest,
4136
4164
  });
4165
+ if (!MethodContextService.hasContext()) {
4166
+ throw new Error("exchangeCoreService formatPrice requires a method context");
4167
+ }
4137
4168
  await this.validate(this.methodContextService.context.exchangeName);
4138
4169
  return await ExecutionContextService.runInContext(async () => {
4139
4170
  return await this.exchangeConnectionService.formatPrice(symbol, price);
@@ -4153,12 +4184,15 @@ class ExchangeGlobalService {
4153
4184
  * @returns Promise resolving to formatted quantity string
4154
4185
  */
4155
4186
  this.formatQuantity = async (symbol, quantity, when, backtest) => {
4156
- this.loggerService.log("exchangeGlobalService formatQuantity", {
4187
+ this.loggerService.log("exchangeCoreService formatQuantity", {
4157
4188
  symbol,
4158
4189
  quantity,
4159
4190
  when,
4160
4191
  backtest,
4161
4192
  });
4193
+ if (!MethodContextService.hasContext()) {
4194
+ throw new Error("exchangeCoreService formatQuantity requires a method context");
4195
+ }
4162
4196
  await this.validate(this.methodContextService.context.exchangeName);
4163
4197
  return await ExecutionContextService.runInContext(async () => {
4164
4198
  return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
@@ -4171,7 +4205,7 @@ class ExchangeGlobalService {
4171
4205
  }
4172
4206
  }
4173
4207
 
4174
- const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
4208
+ const METHOD_NAME_VALIDATE = "strategyCoreService validate";
4175
4209
  /**
4176
4210
  * Global service for strategy operations with execution context injection.
4177
4211
  *
@@ -4180,7 +4214,7 @@ const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
4180
4214
  *
4181
4215
  * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
4182
4216
  */
4183
- class StrategyGlobalService {
4217
+ class StrategyCoreService {
4184
4218
  constructor() {
4185
4219
  this.loggerService = inject(TYPES.loggerService);
4186
4220
  this.strategyConnectionService = inject(TYPES.strategyConnectionService);
@@ -4218,10 +4252,13 @@ class StrategyGlobalService {
4218
4252
  * @returns Promise resolving to pending signal or null
4219
4253
  */
4220
4254
  this.getPendingSignal = async (symbol, strategyName) => {
4221
- this.loggerService.log("strategyGlobalService getPendingSignal", {
4255
+ this.loggerService.log("strategyCoreService getPendingSignal", {
4222
4256
  symbol,
4223
4257
  strategyName,
4224
4258
  });
4259
+ if (!MethodContextService.hasContext()) {
4260
+ throw new Error("strategyCoreService getPendingSignal requires a method context");
4261
+ }
4225
4262
  await this.validate(symbol, strategyName);
4226
4263
  return await this.strategyConnectionService.getPendingSignal(symbol, strategyName);
4227
4264
  };
@@ -4236,10 +4273,13 @@ class StrategyGlobalService {
4236
4273
  * @returns Promise resolving to true if strategy is stopped, false otherwise
4237
4274
  */
4238
4275
  this.getStopped = async (symbol, strategyName) => {
4239
- this.loggerService.log("strategyGlobalService getStopped", {
4276
+ this.loggerService.log("strategyCoreService getStopped", {
4240
4277
  symbol,
4241
4278
  strategyName,
4242
4279
  });
4280
+ if (!MethodContextService.hasContext()) {
4281
+ throw new Error("strategyCoreService getStopped requires a method context");
4282
+ }
4243
4283
  await this.validate(symbol, strategyName);
4244
4284
  return await this.strategyConnectionService.getStopped(symbol, strategyName);
4245
4285
  };
@@ -4255,11 +4295,14 @@ class StrategyGlobalService {
4255
4295
  * @returns Discriminated union of tick result (idle, opened, active, closed)
4256
4296
  */
4257
4297
  this.tick = async (symbol, when, backtest) => {
4258
- this.loggerService.log("strategyGlobalService tick", {
4298
+ this.loggerService.log("strategyCoreService tick", {
4259
4299
  symbol,
4260
4300
  when,
4261
4301
  backtest,
4262
4302
  });
4303
+ if (!MethodContextService.hasContext()) {
4304
+ throw new Error("strategyCoreService tick requires a method context");
4305
+ }
4263
4306
  const strategyName = this.methodContextService.context.strategyName;
4264
4307
  await this.validate(symbol, strategyName);
4265
4308
  return await ExecutionContextService.runInContext(async () => {
@@ -4283,12 +4326,15 @@ class StrategyGlobalService {
4283
4326
  * @returns Closed signal result with PNL
4284
4327
  */
4285
4328
  this.backtest = async (symbol, candles, when, backtest) => {
4286
- this.loggerService.log("strategyGlobalService backtest", {
4329
+ this.loggerService.log("strategyCoreService backtest", {
4287
4330
  symbol,
4288
4331
  candleCount: candles.length,
4289
4332
  when,
4290
4333
  backtest,
4291
4334
  });
4335
+ if (!MethodContextService.hasContext()) {
4336
+ throw new Error("strategyCoreService backtest requires a method context");
4337
+ }
4292
4338
  const strategyName = this.methodContextService.context.strategyName;
4293
4339
  await this.validate(symbol, strategyName);
4294
4340
  return await ExecutionContextService.runInContext(async () => {
@@ -4310,7 +4356,7 @@ class StrategyGlobalService {
4310
4356
  * @returns Promise that resolves when stop flag is set
4311
4357
  */
4312
4358
  this.stop = async (ctx, backtest) => {
4313
- this.loggerService.log("strategyGlobalService stop", {
4359
+ this.loggerService.log("strategyCoreService stop", {
4314
4360
  ctx,
4315
4361
  backtest,
4316
4362
  });
@@ -4326,7 +4372,7 @@ class StrategyGlobalService {
4326
4372
  * @param ctx - Optional context with symbol and strategyName (clears all if not provided)
4327
4373
  */
4328
4374
  this.clear = async (ctx) => {
4329
- this.loggerService.log("strategyGlobalService clear", {
4375
+ this.loggerService.log("strategyCoreService clear", {
4330
4376
  ctx,
4331
4377
  });
4332
4378
  if (ctx) {
@@ -4337,14 +4383,14 @@ class StrategyGlobalService {
4337
4383
  }
4338
4384
  }
4339
4385
 
4340
- const METHOD_NAME_GET_TIMEFRAME = "frameGlobalService getTimeframe";
4386
+ const METHOD_NAME_GET_TIMEFRAME = "frameCoreService getTimeframe";
4341
4387
  /**
4342
4388
  * Global service for frame operations.
4343
4389
  *
4344
4390
  * Wraps FrameConnectionService for timeframe generation.
4345
4391
  * Used internally by BacktestLogicPrivateService.
4346
4392
  */
4347
- class FrameGlobalService {
4393
+ class FrameCoreService {
4348
4394
  constructor() {
4349
4395
  this.loggerService = inject(TYPES.loggerService);
4350
4396
  this.frameConnectionService = inject(TYPES.frameConnectionService);
@@ -4360,6 +4406,9 @@ class FrameGlobalService {
4360
4406
  frameName,
4361
4407
  symbol,
4362
4408
  });
4409
+ if (!MethodContextService.hasContext()) {
4410
+ throw new Error("frameCoreService getTimeframe requires a method context");
4411
+ }
4363
4412
  this.frameValidationService.validate(frameName, METHOD_NAME_GET_TIMEFRAME);
4364
4413
  return await this.frameConnectionService.getTimeframe(symbol, frameName);
4365
4414
  };
@@ -4965,9 +5014,9 @@ class WalkerSchemaService {
4965
5014
  class BacktestLogicPrivateService {
4966
5015
  constructor() {
4967
5016
  this.loggerService = inject(TYPES.loggerService);
4968
- this.strategyGlobalService = inject(TYPES.strategyGlobalService);
4969
- this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
4970
- this.frameGlobalService = inject(TYPES.frameGlobalService);
5017
+ this.strategyCoreService = inject(TYPES.strategyCoreService);
5018
+ this.exchangeCoreService = inject(TYPES.exchangeCoreService);
5019
+ this.frameCoreService = inject(TYPES.frameCoreService);
4971
5020
  this.methodContextService = inject(TYPES.methodContextService);
4972
5021
  }
4973
5022
  /**
@@ -4989,7 +5038,7 @@ class BacktestLogicPrivateService {
4989
5038
  symbol,
4990
5039
  });
4991
5040
  const backtestStartTime = performance.now();
4992
- const timeframes = await this.frameGlobalService.getTimeframe(symbol, this.methodContextService.context.frameName);
5041
+ const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
4993
5042
  const totalFrames = timeframes.length;
4994
5043
  let i = 0;
4995
5044
  let previousEventTimestamp = null;
@@ -5008,7 +5057,7 @@ class BacktestLogicPrivateService {
5008
5057
  });
5009
5058
  }
5010
5059
  // Check if strategy should stop before processing next frame
5011
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5060
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5012
5061
  this.loggerService.info("backtestLogicPrivateService stopped by user request (before tick)", {
5013
5062
  symbol,
5014
5063
  when: when.toISOString(),
@@ -5019,7 +5068,7 @@ class BacktestLogicPrivateService {
5019
5068
  }
5020
5069
  let result;
5021
5070
  try {
5022
- result = await this.strategyGlobalService.tick(symbol, when, true);
5071
+ result = await this.strategyCoreService.tick(symbol, when, true);
5023
5072
  }
5024
5073
  catch (error) {
5025
5074
  console.warn(`backtestLogicPrivateService tick failed, skipping timeframe when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5033,7 +5082,7 @@ class BacktestLogicPrivateService {
5033
5082
  continue;
5034
5083
  }
5035
5084
  // Check if strategy should stop when idle (no active signal)
5036
- if (await functoolsKit.and(Promise.resolve(result.action === "idle"), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5085
+ if (await functoolsKit.and(Promise.resolve(result.action === "idle"), this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5037
5086
  this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
5038
5087
  symbol,
5039
5088
  when: when.toISOString(),
@@ -5063,7 +5112,7 @@ class BacktestLogicPrivateService {
5063
5112
  const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
5064
5113
  let candles;
5065
5114
  try {
5066
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
5115
+ candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
5067
5116
  }
5068
5117
  catch (error) {
5069
5118
  console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5092,7 +5141,7 @@ class BacktestLogicPrivateService {
5092
5141
  // и если активируется - продолжит с TP/SL мониторингом
5093
5142
  let backtestResult;
5094
5143
  try {
5095
- backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
5144
+ backtestResult = await this.strategyCoreService.backtest(symbol, candles, when, true);
5096
5145
  }
5097
5146
  catch (error) {
5098
5147
  console.warn(`backtestLogicPrivateService backtest failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5135,7 +5184,7 @@ class BacktestLogicPrivateService {
5135
5184
  }
5136
5185
  yield backtestResult;
5137
5186
  // Check if strategy should stop after signal is closed
5138
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5187
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5139
5188
  this.loggerService.info("backtestLogicPrivateService stopped by user request (after scheduled signal closed)", {
5140
5189
  symbol,
5141
5190
  signalId: backtestResult.signal.id,
@@ -5162,7 +5211,7 @@ class BacktestLogicPrivateService {
5162
5211
  const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
5163
5212
  let candles;
5164
5213
  try {
5165
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
5214
+ candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
5166
5215
  }
5167
5216
  catch (error) {
5168
5217
  console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5189,7 +5238,7 @@ class BacktestLogicPrivateService {
5189
5238
  // Вызываем backtest - всегда возвращает closed
5190
5239
  let backtestResult;
5191
5240
  try {
5192
- backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
5241
+ backtestResult = await this.strategyCoreService.backtest(symbol, candles, when, true);
5193
5242
  }
5194
5243
  catch (error) {
5195
5244
  console.warn(`backtestLogicPrivateService backtest failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5228,7 +5277,7 @@ class BacktestLogicPrivateService {
5228
5277
  }
5229
5278
  yield backtestResult;
5230
5279
  // Check if strategy should stop after signal is closed
5231
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5280
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5232
5281
  this.loggerService.info("backtestLogicPrivateService stopped by user request (after signal closed)", {
5233
5282
  symbol,
5234
5283
  signalId: backtestResult.signal.id,
@@ -5302,7 +5351,7 @@ const TICK_TTL = 1 * 60 * 1000 + 1;
5302
5351
  class LiveLogicPrivateService {
5303
5352
  constructor() {
5304
5353
  this.loggerService = inject(TYPES.loggerService);
5305
- this.strategyGlobalService = inject(TYPES.strategyGlobalService);
5354
+ this.strategyCoreService = inject(TYPES.strategyCoreService);
5306
5355
  this.methodContextService = inject(TYPES.methodContextService);
5307
5356
  }
5308
5357
  /**
@@ -5337,7 +5386,7 @@ class LiveLogicPrivateService {
5337
5386
  const when = new Date();
5338
5387
  let result;
5339
5388
  try {
5340
- result = await this.strategyGlobalService.tick(symbol, when, false);
5389
+ result = await this.strategyCoreService.tick(symbol, when, false);
5341
5390
  }
5342
5391
  catch (error) {
5343
5392
  console.warn(`backtestLogicPrivateService tick failed when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -5371,7 +5420,7 @@ class LiveLogicPrivateService {
5371
5420
  previousEventTimestamp = currentTimestamp;
5372
5421
  // Check if strategy should stop when idle (no active signal)
5373
5422
  if (result.action === "idle") {
5374
- if (await functoolsKit.and(Promise.resolve(true), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5423
+ if (await functoolsKit.and(Promise.resolve(true), this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName))) {
5375
5424
  this.loggerService.info("liveLogicPrivateService stopped by user request (idle state)", {
5376
5425
  symbol,
5377
5426
  when: when.toISOString(),
@@ -5393,7 +5442,7 @@ class LiveLogicPrivateService {
5393
5442
  yield result;
5394
5443
  // Check if strategy should stop after signal is closed
5395
5444
  if (result.action === "closed") {
5396
- if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5445
+ if (await this.strategyCoreService.getStopped(symbol, this.methodContextService.context.strategyName)) {
5397
5446
  this.loggerService.info("liveLogicPrivateService stopped by user request (after signal closed)", {
5398
5447
  symbol,
5399
5448
  signalId: result.signal.id,
@@ -8684,8 +8733,27 @@ class HeatMarkdownService {
8684
8733
  }
8685
8734
 
8686
8735
  /**
8687
- * @class ExchangeValidationService
8688
- * Service for managing and validating exchange configurations
8736
+ * Service for managing and validating exchange configurations.
8737
+ *
8738
+ * Maintains a registry of all configured exchanges and validates
8739
+ * their existence before operations. Uses memoization for performance.
8740
+ *
8741
+ * Key features:
8742
+ * - Registry management: addExchange() to register new exchanges
8743
+ * - Validation: validate() ensures exchange exists before use
8744
+ * - Memoization: validation results are cached for performance
8745
+ * - Listing: list() returns all registered exchanges
8746
+ *
8747
+ * @throws {Error} If duplicate exchange name is added
8748
+ * @throws {Error} If unknown exchange is referenced
8749
+ *
8750
+ * @example
8751
+ * ```typescript
8752
+ * const exchangeValidation = new ExchangeValidationService();
8753
+ * exchangeValidation.addExchange("binance", binanceSchema);
8754
+ * exchangeValidation.validate("binance", "backtest"); // OK
8755
+ * exchangeValidation.validate("unknown", "live"); // Throws error
8756
+ * ```
8689
8757
  */
8690
8758
  class ExchangeValidationService {
8691
8759
  constructor() {
@@ -8745,8 +8813,29 @@ class ExchangeValidationService {
8745
8813
  }
8746
8814
 
8747
8815
  /**
8748
- * @class StrategyValidationService
8749
- * Service for managing and validating strategy configurations
8816
+ * Service for managing and validating trading strategy configurations.
8817
+ *
8818
+ * Maintains a registry of all configured strategies, validates their existence
8819
+ * before operations, and ensures associated risk profiles are valid.
8820
+ * Uses memoization for performance.
8821
+ *
8822
+ * Key features:
8823
+ * - Registry management: addStrategy() to register new strategies
8824
+ * - Dual validation: validates both strategy existence and risk profile (if configured)
8825
+ * - Memoization: validation results are cached for performance
8826
+ * - Listing: list() returns all registered strategies
8827
+ *
8828
+ * @throws {Error} If duplicate strategy name is added
8829
+ * @throws {Error} If unknown strategy is referenced
8830
+ * @throws {Error} If strategy's risk profile doesn't exist
8831
+ *
8832
+ * @example
8833
+ * ```typescript
8834
+ * const strategyValidation = new StrategyValidationService();
8835
+ * strategyValidation.addStrategy("momentum-btc", { ...schema, riskName: "conservative" });
8836
+ * strategyValidation.validate("momentum-btc", "backtest"); // Validates strategy + risk
8837
+ * strategyValidation.validate("unknown", "live"); // Throws error
8838
+ * ```
8750
8839
  */
8751
8840
  class StrategyValidationService {
8752
8841
  constructor() {
@@ -8817,8 +8906,27 @@ class StrategyValidationService {
8817
8906
  }
8818
8907
 
8819
8908
  /**
8820
- * @class FrameValidationService
8821
- * Service for managing and validating frame configurations
8909
+ * Service for managing and validating frame (timeframe) configurations.
8910
+ *
8911
+ * Maintains a registry of all configured frames and validates
8912
+ * their existence before operations. Uses memoization for performance.
8913
+ *
8914
+ * Key features:
8915
+ * - Registry management: addFrame() to register new timeframes
8916
+ * - Validation: validate() ensures frame exists before use
8917
+ * - Memoization: validation results are cached for performance
8918
+ * - Listing: list() returns all registered frames
8919
+ *
8920
+ * @throws {Error} If duplicate frame name is added
8921
+ * @throws {Error} If unknown frame is referenced
8922
+ *
8923
+ * @example
8924
+ * ```typescript
8925
+ * const frameValidation = new FrameValidationService();
8926
+ * frameValidation.addFrame("2024-Q1", frameSchema);
8927
+ * frameValidation.validate("2024-Q1", "backtest"); // OK
8928
+ * frameValidation.validate("unknown", "live"); // Throws error
8929
+ * ```
8822
8930
  */
8823
8931
  class FrameValidationService {
8824
8932
  constructor() {
@@ -8878,8 +8986,29 @@ class FrameValidationService {
8878
8986
  }
8879
8987
 
8880
8988
  /**
8881
- * @class WalkerValidationService
8882
- * Service for managing and validating walker configurations
8989
+ * Service for managing and validating walker (parameter sweep) configurations.
8990
+ *
8991
+ * Maintains a registry of all configured walkers and validates
8992
+ * their existence before operations. Uses memoization for performance.
8993
+ *
8994
+ * Walkers define parameter ranges for optimization and hyperparameter tuning.
8995
+ *
8996
+ * Key features:
8997
+ * - Registry management: addWalker() to register new walker configurations
8998
+ * - Validation: validate() ensures walker exists before use
8999
+ * - Memoization: validation results are cached for performance
9000
+ * - Listing: list() returns all registered walkers
9001
+ *
9002
+ * @throws {Error} If duplicate walker name is added
9003
+ * @throws {Error} If unknown walker is referenced
9004
+ *
9005
+ * @example
9006
+ * ```typescript
9007
+ * const walkerValidation = new WalkerValidationService();
9008
+ * walkerValidation.addWalker("rsi-sweep", walkerSchema);
9009
+ * walkerValidation.validate("rsi-sweep", "optimizer"); // OK
9010
+ * walkerValidation.validate("unknown", "optimizer"); // Throws error
9011
+ * ```
8883
9012
  */
8884
9013
  class WalkerValidationService {
8885
9014
  constructor() {
@@ -8939,8 +9068,27 @@ class WalkerValidationService {
8939
9068
  }
8940
9069
 
8941
9070
  /**
8942
- * @class SizingValidationService
8943
- * Service for managing and validating sizing configurations
9071
+ * Service for managing and validating position sizing configurations.
9072
+ *
9073
+ * Maintains a registry of all configured sizing strategies and validates
9074
+ * their existence before operations. Uses memoization for performance.
9075
+ *
9076
+ * Key features:
9077
+ * - Registry management: addSizing() to register new sizing strategies
9078
+ * - Validation: validate() ensures sizing strategy exists before use
9079
+ * - Memoization: validation results are cached for performance
9080
+ * - Listing: list() returns all registered sizing strategies
9081
+ *
9082
+ * @throws {Error} If duplicate sizing name is added
9083
+ * @throws {Error} If unknown sizing strategy is referenced
9084
+ *
9085
+ * @example
9086
+ * ```typescript
9087
+ * const sizingValidation = new SizingValidationService();
9088
+ * sizingValidation.addSizing("fixed-1000", fixedSizingSchema);
9089
+ * sizingValidation.validate("fixed-1000", "strategy-1"); // OK
9090
+ * sizingValidation.validate("unknown", "strategy-2"); // Throws error
9091
+ * ```
8944
9092
  */
8945
9093
  class SizingValidationService {
8946
9094
  constructor() {
@@ -9005,8 +9153,27 @@ class SizingValidationService {
9005
9153
  }
9006
9154
 
9007
9155
  /**
9008
- * @class RiskValidationService
9009
- * Service for managing and validating risk configurations
9156
+ * Service for managing and validating risk management configurations.
9157
+ *
9158
+ * Maintains a registry of all configured risk profiles and validates
9159
+ * their existence before operations. Uses memoization for performance.
9160
+ *
9161
+ * Key features:
9162
+ * - Registry management: addRisk() to register new risk profiles
9163
+ * - Validation: validate() ensures risk profile exists before use
9164
+ * - Memoization: validation results are cached by riskName:source for performance
9165
+ * - Listing: list() returns all registered risk profiles
9166
+ *
9167
+ * @throws {Error} If duplicate risk name is added
9168
+ * @throws {Error} If unknown risk profile is referenced
9169
+ *
9170
+ * @example
9171
+ * ```typescript
9172
+ * const riskValidation = new RiskValidationService();
9173
+ * riskValidation.addRisk("conservative", conservativeSchema);
9174
+ * riskValidation.validate("conservative", "strategy-1"); // OK
9175
+ * riskValidation.validate("unknown", "strategy-2"); // Throws error
9176
+ * ```
9010
9177
  */
9011
9178
  class RiskValidationService {
9012
9179
  constructor() {
@@ -11558,6 +11725,133 @@ class OutlineMarkdownService {
11558
11725
  }
11559
11726
  }
11560
11727
 
11728
+ /**
11729
+ * Service for validating GLOBAL_CONFIG parameters to ensure mathematical correctness
11730
+ * and prevent unprofitable trading configurations.
11731
+ *
11732
+ * Performs comprehensive validation on:
11733
+ * - **Percentage parameters**: Slippage, fees, and profit margins must be non-negative
11734
+ * - **Economic viability**: Ensures CC_MIN_TAKEPROFIT_DISTANCE_PERCENT covers all trading costs
11735
+ * (slippage + fees) to guarantee profitable trades when TakeProfit is hit
11736
+ * - **Range constraints**: Validates MIN < MAX relationships (e.g., StopLoss distances)
11737
+ * - **Time-based parameters**: Ensures positive integer values for timeouts and lifetimes
11738
+ * - **Candle parameters**: Validates retry counts, delays, and anomaly detection thresholds
11739
+ *
11740
+ * @throws {Error} If any validation fails, throws with detailed breakdown of all errors
11741
+ *
11742
+ * @example
11743
+ * ```typescript
11744
+ * const validator = new ConfigValidationService();
11745
+ * validator.validate(); // Throws if config is invalid
11746
+ * ```
11747
+ *
11748
+ * @example Validation failure output:
11749
+ * ```
11750
+ * GLOBAL_CONFIG validation failed:
11751
+ * 1. CC_MIN_TAKEPROFIT_DISTANCE_PERCENT (0.3%) is too low to cover trading costs.
11752
+ * Required minimum: 0.40%
11753
+ * Breakdown:
11754
+ * - Slippage effect: 0.20% (0.1% × 2 transactions)
11755
+ * - Fees: 0.20% (0.1% × 2 transactions)
11756
+ * All TakeProfit signals will be unprofitable with current settings!
11757
+ * ```
11758
+ */
11759
+ class ConfigValidationService {
11760
+ constructor() {
11761
+ /**
11762
+ * @private
11763
+ * @readonly
11764
+ * Injected logger service instance
11765
+ */
11766
+ this.loggerService = inject(TYPES.loggerService);
11767
+ /**
11768
+ * Validates GLOBAL_CONFIG parameters for mathematical correctness.
11769
+ *
11770
+ * Checks:
11771
+ * 1. CC_MIN_TAKEPROFIT_DISTANCE_PERCENT must cover slippage + fees
11772
+ * 2. All percentage values must be positive
11773
+ * 3. Time/count values must be positive integers
11774
+ *
11775
+ * @throws Error if configuration is invalid
11776
+ */
11777
+ this.validate = () => {
11778
+ this.loggerService.log("configValidationService validate");
11779
+ const errors = [];
11780
+ // Validate slippage and fee percentages
11781
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE) || GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE < 0) {
11782
+ errors.push(`CC_PERCENT_SLIPPAGE must be a non-negative number, got ${GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE}`);
11783
+ }
11784
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_PERCENT_FEE) || GLOBAL_CONFIG.CC_PERCENT_FEE < 0) {
11785
+ errors.push(`CC_PERCENT_FEE must be a non-negative number, got ${GLOBAL_CONFIG.CC_PERCENT_FEE}`);
11786
+ }
11787
+ // Calculate minimum required TP distance to cover costs
11788
+ const slippageEffect = GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE * 2; // Applied twice (entry + exit)
11789
+ const feesTotal = GLOBAL_CONFIG.CC_PERCENT_FEE * 2; // Applied twice (entry + exit)
11790
+ const minRequiredTpDistance = slippageEffect + feesTotal;
11791
+ // Validate CC_MIN_TAKEPROFIT_DISTANCE_PERCENT
11792
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT <= 0) {
11793
+ errors.push(`CC_MIN_TAKEPROFIT_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}`);
11794
+ }
11795
+ else if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT < minRequiredTpDistance) {
11796
+ errors.push(`CC_MIN_TAKEPROFIT_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}%) is too low to cover trading costs.\n` +
11797
+ ` Required minimum: ${minRequiredTpDistance.toFixed(2)}%\n` +
11798
+ ` Breakdown:\n` +
11799
+ ` - Slippage effect: ${slippageEffect.toFixed(2)}% (${GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE}% × 2 transactions)\n` +
11800
+ ` - Fees: ${feesTotal.toFixed(2)}% (${GLOBAL_CONFIG.CC_PERCENT_FEE}% × 2 transactions)\n` +
11801
+ ` All TakeProfit signals will be unprofitable with current settings!`);
11802
+ }
11803
+ // Validate CC_MIN_STOPLOSS_DISTANCE_PERCENT
11804
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT <= 0) {
11805
+ errors.push(`CC_MIN_STOPLOSS_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}`);
11806
+ }
11807
+ // Validate CC_MAX_STOPLOSS_DISTANCE_PERCENT
11808
+ if (!Number.isFinite(GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) || GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT <= 0) {
11809
+ errors.push(`CC_MAX_STOPLOSS_DISTANCE_PERCENT must be a positive number, got ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}`);
11810
+ }
11811
+ // Validate that MIN < MAX for StopLoss
11812
+ if (Number.isFinite(GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) &&
11813
+ Number.isFinite(GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) &&
11814
+ GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT >= GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
11815
+ errors.push(`CC_MIN_STOPLOSS_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}%) must be less than ` +
11816
+ `CC_MAX_STOPLOSS_DISTANCE_PERCENT (${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}%)`);
11817
+ }
11818
+ // Validate time-based parameters
11819
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES) || GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES <= 0) {
11820
+ errors.push(`CC_SCHEDULE_AWAIT_MINUTES must be a positive integer, got ${GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES}`);
11821
+ }
11822
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) || GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES <= 0) {
11823
+ errors.push(`CC_MAX_SIGNAL_LIFETIME_MINUTES must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES}`);
11824
+ }
11825
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS) || GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS <= 0) {
11826
+ errors.push(`CC_MAX_SIGNAL_GENERATION_SECONDS must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS}`);
11827
+ }
11828
+ // Validate candle-based parameters
11829
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT) || GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT <= 0) {
11830
+ errors.push(`CC_AVG_PRICE_CANDLES_COUNT must be a positive integer, got ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT}`);
11831
+ }
11832
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT) || GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT < 0) {
11833
+ errors.push(`CC_GET_CANDLES_RETRY_COUNT must be a non-negative integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT}`);
11834
+ }
11835
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS) || GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS < 0) {
11836
+ errors.push(`CC_GET_CANDLES_RETRY_DELAY_MS must be a non-negative integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS}`);
11837
+ }
11838
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR) || GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR <= 0) {
11839
+ errors.push(`CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR}`);
11840
+ }
11841
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) || GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN <= 0) {
11842
+ errors.push(`CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN}`);
11843
+ }
11844
+ // Throw aggregated errors if any
11845
+ if (errors.length > 0) {
11846
+ const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
11847
+ this.loggerService.warn(errorMessage);
11848
+ throw new Error(errorMessage);
11849
+ }
11850
+ this.loggerService.log("configValidationService validation passed");
11851
+ };
11852
+ }
11853
+ }
11854
+
11561
11855
  {
11562
11856
  provide(TYPES.loggerService, () => new LoggerService());
11563
11857
  }
@@ -11584,9 +11878,11 @@ class OutlineMarkdownService {
11584
11878
  provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
11585
11879
  }
11586
11880
  {
11587
- provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
11588
- provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
11589
- provide(TYPES.frameGlobalService, () => new FrameGlobalService());
11881
+ provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
11882
+ provide(TYPES.strategyCoreService, () => new StrategyCoreService());
11883
+ provide(TYPES.frameCoreService, () => new FrameCoreService());
11884
+ }
11885
+ {
11590
11886
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
11591
11887
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
11592
11888
  provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
@@ -11625,6 +11921,7 @@ class OutlineMarkdownService {
11625
11921
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
11626
11922
  provide(TYPES.riskValidationService, () => new RiskValidationService());
11627
11923
  provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
11924
+ provide(TYPES.configValidationService, () => new ConfigValidationService());
11628
11925
  }
11629
11926
  {
11630
11927
  provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
@@ -11655,10 +11952,12 @@ const schemaServices = {
11655
11952
  riskSchemaService: inject(TYPES.riskSchemaService),
11656
11953
  optimizerSchemaService: inject(TYPES.optimizerSchemaService),
11657
11954
  };
11955
+ const coreServices = {
11956
+ exchangeCoreService: inject(TYPES.exchangeCoreService),
11957
+ strategyCoreService: inject(TYPES.strategyCoreService),
11958
+ frameCoreService: inject(TYPES.frameCoreService),
11959
+ };
11658
11960
  const globalServices = {
11659
- exchangeGlobalService: inject(TYPES.exchangeGlobalService),
11660
- strategyGlobalService: inject(TYPES.strategyGlobalService),
11661
- frameGlobalService: inject(TYPES.frameGlobalService),
11662
11961
  sizingGlobalService: inject(TYPES.sizingGlobalService),
11663
11962
  riskGlobalService: inject(TYPES.riskGlobalService),
11664
11963
  optimizerGlobalService: inject(TYPES.optimizerGlobalService),
@@ -11697,6 +11996,7 @@ const validationServices = {
11697
11996
  sizingValidationService: inject(TYPES.sizingValidationService),
11698
11997
  riskValidationService: inject(TYPES.riskValidationService),
11699
11998
  optimizerValidationService: inject(TYPES.optimizerValidationService),
11999
+ configValidationService: inject(TYPES.configValidationService),
11700
12000
  };
11701
12001
  const templateServices = {
11702
12002
  optimizerTemplateService: inject(TYPES.optimizerTemplateService),
@@ -11706,6 +12006,7 @@ const backtest = {
11706
12006
  ...contextServices,
11707
12007
  ...connectionServices,
11708
12008
  ...schemaServices,
12009
+ ...coreServices,
11709
12010
  ...globalServices,
11710
12011
  ...commandServices,
11711
12012
  ...logicPrivateServices,
@@ -11734,12 +12035,13 @@ var backtest$1 = backtest;
11734
12035
  * });
11735
12036
  * ```
11736
12037
  */
11737
- async function setLogger(logger) {
12038
+ function setLogger(logger) {
11738
12039
  backtest$1.loggerService.setLogger(logger);
11739
12040
  }
11740
12041
  /**
11741
12042
  * Sets global configuration parameters for the framework.
11742
12043
  * @param config - Partial configuration object to override default settings
12044
+ * @param _unsafe - Skip config validations - required for testbed
11743
12045
  *
11744
12046
  * @example
11745
12047
  * ```typescript
@@ -11748,8 +12050,51 @@ async function setLogger(logger) {
11748
12050
  * });
11749
12051
  * ```
11750
12052
  */
11751
- async function setConfig(config) {
11752
- Object.assign(GLOBAL_CONFIG, config);
12053
+ function setConfig(config, _unsafe) {
12054
+ const prevConfig = Object.assign({}, GLOBAL_CONFIG);
12055
+ try {
12056
+ Object.assign(GLOBAL_CONFIG, config);
12057
+ !_unsafe && backtest$1.configValidationService.validate();
12058
+ }
12059
+ catch (error) {
12060
+ console.warn(`backtest-kit setConfig failed: ${functoolsKit.getErrorMessage(error)}`, config);
12061
+ Object.assign(GLOBAL_CONFIG, prevConfig);
12062
+ throw error;
12063
+ }
12064
+ }
12065
+ /**
12066
+ * Retrieves a copy of the current global configuration.
12067
+ *
12068
+ * Returns a shallow copy of the current GLOBAL_CONFIG to prevent accidental mutations.
12069
+ * Use this to inspect the current configuration state without modifying it.
12070
+ *
12071
+ * @returns {GlobalConfig} A copy of the current global configuration object
12072
+ *
12073
+ * @example
12074
+ * ```typescript
12075
+ * const currentConfig = getConfig();
12076
+ * console.log(currentConfig.CC_SCHEDULE_AWAIT_MINUTES);
12077
+ * ```
12078
+ */
12079
+ function getConfig() {
12080
+ return Object.assign({}, GLOBAL_CONFIG);
12081
+ }
12082
+ /**
12083
+ * Retrieves the default configuration object for the framework.
12084
+ *
12085
+ * Returns a reference to the default configuration with all preset values.
12086
+ * Use this to see what configuration options are available and their default values.
12087
+ *
12088
+ * @returns {GlobalConfig} The default configuration object
12089
+ *
12090
+ * @example
12091
+ * ```typescript
12092
+ * const defaultConfig = getDefaultConfig();
12093
+ * console.log(defaultConfig.CC_SCHEDULE_AWAIT_MINUTES);
12094
+ * ```
12095
+ */
12096
+ function getDefaultConfig() {
12097
+ return DEFAULT_CONFIG;
11753
12098
  }
11754
12099
 
11755
12100
  const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
@@ -13559,7 +13904,7 @@ class BacktestInstance {
13559
13904
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName });
13560
13905
  }
13561
13906
  {
13562
- backtest$1.strategyGlobalService.clear({ symbol, strategyName: context.strategyName });
13907
+ backtest$1.strategyCoreService.clear({ symbol, strategyName: context.strategyName });
13563
13908
  }
13564
13909
  {
13565
13910
  const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -13594,8 +13939,8 @@ class BacktestInstance {
13594
13939
  });
13595
13940
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
13596
13941
  return () => {
13597
- backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, true);
13598
- backtest$1.strategyGlobalService
13942
+ backtest$1.strategyCoreService.stop({ symbol, strategyName: context.strategyName }, true);
13943
+ backtest$1.strategyCoreService
13599
13944
  .getPendingSignal(symbol, context.strategyName)
13600
13945
  .then(async (pendingSignal) => {
13601
13946
  if (pendingSignal) {
@@ -13636,7 +13981,7 @@ class BacktestInstance {
13636
13981
  symbol,
13637
13982
  strategyName,
13638
13983
  });
13639
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
13984
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
13640
13985
  };
13641
13986
  /**
13642
13987
  * Gets statistical data from all closed signals for a symbol-strategy pair.
@@ -14051,7 +14396,7 @@ class LiveInstance {
14051
14396
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName });
14052
14397
  }
14053
14398
  {
14054
- backtest$1.strategyGlobalService.clear({ symbol, strategyName: context.strategyName });
14399
+ backtest$1.strategyCoreService.clear({ symbol, strategyName: context.strategyName });
14055
14400
  }
14056
14401
  {
14057
14402
  const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -14086,8 +14431,8 @@ class LiveInstance {
14086
14431
  });
14087
14432
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
14088
14433
  return () => {
14089
- backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, false);
14090
- backtest$1.strategyGlobalService
14434
+ backtest$1.strategyCoreService.stop({ symbol, strategyName: context.strategyName }, false);
14435
+ backtest$1.strategyCoreService
14091
14436
  .getPendingSignal(symbol, context.strategyName)
14092
14437
  .then(async (pendingSignal) => {
14093
14438
  if (pendingSignal) {
@@ -14128,7 +14473,7 @@ class LiveInstance {
14128
14473
  symbol,
14129
14474
  strategyName,
14130
14475
  });
14131
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, false);
14476
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, false);
14132
14477
  };
14133
14478
  /**
14134
14479
  * Gets statistical data from all live trading events for a symbol-strategy pair.
@@ -14809,7 +15154,7 @@ class WalkerInstance {
14809
15154
  backtest$1.scheduleMarkdownService.clear({ symbol, strategyName });
14810
15155
  }
14811
15156
  {
14812
- backtest$1.strategyGlobalService.clear({ symbol, strategyName });
15157
+ backtest$1.strategyCoreService.clear({ symbol, strategyName });
14813
15158
  }
14814
15159
  {
14815
15160
  const { riskName } = backtest$1.strategySchemaService.get(strategyName);
@@ -14849,7 +15194,7 @@ class WalkerInstance {
14849
15194
  this.task(symbol, context).catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
14850
15195
  return () => {
14851
15196
  for (const strategyName of walkerSchema.strategies) {
14852
- backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
15197
+ backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
14853
15198
  walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
14854
15199
  }
14855
15200
  if (!this._isDone) {
@@ -14895,7 +15240,7 @@ class WalkerInstance {
14895
15240
  const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
14896
15241
  for (const strategyName of walkerSchema.strategies) {
14897
15242
  await walkerStopSubject.next({ symbol, strategyName, walkerName });
14898
- await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
15243
+ await backtest$1.strategyCoreService.stop({ symbol, strategyName }, true);
14899
15244
  }
14900
15245
  };
14901
15246
  /**
@@ -15868,7 +16213,9 @@ exports.formatPrice = formatPrice;
15868
16213
  exports.formatQuantity = formatQuantity;
15869
16214
  exports.getAveragePrice = getAveragePrice;
15870
16215
  exports.getCandles = getCandles;
16216
+ exports.getConfig = getConfig;
15871
16217
  exports.getDate = getDate;
16218
+ exports.getDefaultConfig = getDefaultConfig;
15872
16219
  exports.getMode = getMode;
15873
16220
  exports.lib = backtest;
15874
16221
  exports.listExchanges = listExchanges;