backtest-kit 11.8.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1996 -1996
  3. package/build/index.cjs +661 -136
  4. package/build/index.mjs +660 -137
  5. package/package.json +86 -86
  6. package/types.d.ts +199 -40
package/build/index.cjs CHANGED
@@ -4681,7 +4681,7 @@ const LOGGER_SERVICE$8 = new LoggerService();
4681
4681
  * @param backtest - `true` for backtest, `false` for live.
4682
4682
  * @returns Colon-joined composite key.
4683
4683
  */
4684
- const CREATE_KEY_FN$y = (symbol, strategyName, exchangeName, frameName, backtest) => {
4684
+ const CREATE_KEY_FN$B = (symbol, strategyName, exchangeName, frameName, backtest) => {
4685
4685
  const parts = [symbol, strategyName, exchangeName];
4686
4686
  if (frameName)
4687
4687
  parts.push(frameName);
@@ -4724,7 +4724,7 @@ class LookupUtils {
4724
4724
  LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
4725
4725
  activity,
4726
4726
  });
4727
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4727
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4728
4728
  this._lookupMap.set(key, activity);
4729
4729
  };
4730
4730
  /**
@@ -4738,7 +4738,7 @@ class LookupUtils {
4738
4738
  LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
4739
4739
  activity,
4740
4740
  });
4741
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4741
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4742
4742
  this._lookupMap.delete(key);
4743
4743
  };
4744
4744
  /**
@@ -12356,7 +12356,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
12356
12356
  * @param backtest - Whether running in backtest mode
12357
12357
  * @returns Unique string key for memoization
12358
12358
  */
12359
- const CREATE_KEY_FN$x = (symbol, strategyName, exchangeName, frameName, backtest) => {
12359
+ const CREATE_KEY_FN$A = (symbol, strategyName, exchangeName, frameName, backtest) => {
12360
12360
  const parts = [symbol, strategyName, exchangeName];
12361
12361
  if (frameName)
12362
12362
  parts.push(frameName);
@@ -12658,7 +12658,7 @@ class StrategyConnectionService {
12658
12658
  * @param backtest - Whether running in backtest mode
12659
12659
  * @returns Configured ClientStrategy instance
12660
12660
  */
12661
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$x(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12661
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12662
12662
  const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
12663
12663
  return new ClientStrategy({
12664
12664
  symbol,
@@ -13620,7 +13620,7 @@ class StrategyConnectionService {
13620
13620
  }
13621
13621
  return;
13622
13622
  }
13623
- const key = CREATE_KEY_FN$x(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13623
+ const key = CREATE_KEY_FN$A(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13624
13624
  if (!this.getStrategy.has(key)) {
13625
13625
  return;
13626
13626
  }
@@ -14166,6 +14166,7 @@ class ClientFrame {
14166
14166
  }
14167
14167
  }
14168
14168
 
14169
+ const DEFAULT_INTERVAL = "1m";
14169
14170
  /**
14170
14171
  * Connection service routing frame operations to correct ClientFrame instance.
14171
14172
  *
@@ -14209,7 +14210,7 @@ class FrameConnectionService {
14209
14210
  logger: this.loggerService,
14210
14211
  startDate,
14211
14212
  endDate,
14212
- interval,
14213
+ interval: interval || DEFAULT_INTERVAL,
14213
14214
  callbacks,
14214
14215
  });
14215
14216
  });
@@ -14868,7 +14869,7 @@ class ClientRisk {
14868
14869
  * @param backtest - Whether running in backtest mode
14869
14870
  * @returns Unique string key for memoization
14870
14871
  */
14871
- const CREATE_KEY_FN$w = (riskName, exchangeName, frameName, backtest) => {
14872
+ const CREATE_KEY_FN$z = (riskName, exchangeName, frameName, backtest) => {
14872
14873
  const parts = [riskName, exchangeName];
14873
14874
  if (frameName)
14874
14875
  parts.push(frameName);
@@ -14968,7 +14969,7 @@ class RiskConnectionService {
14968
14969
  * @param backtest - True if backtest mode, false if live mode
14969
14970
  * @returns Configured ClientRisk instance
14970
14971
  */
14971
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$w(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14972
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$z(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14972
14973
  const schema = this.riskSchemaService.get(riskName);
14973
14974
  return new ClientRisk({
14974
14975
  ...schema,
@@ -15058,7 +15059,7 @@ class RiskConnectionService {
15058
15059
  payload,
15059
15060
  });
15060
15061
  if (payload) {
15061
- const key = CREATE_KEY_FN$w(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
15062
+ const key = CREATE_KEY_FN$z(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
15062
15063
  this.getRisk.clear(key);
15063
15064
  }
15064
15065
  else {
@@ -16177,7 +16178,7 @@ class ClientAction {
16177
16178
  * @param backtest - Whether running in backtest mode
16178
16179
  * @returns Unique string key for memoization
16179
16180
  */
16180
- const CREATE_KEY_FN$v = (actionName, strategyName, exchangeName, frameName, backtest) => {
16181
+ const CREATE_KEY_FN$y = (actionName, strategyName, exchangeName, frameName, backtest) => {
16181
16182
  const parts = [actionName, strategyName, exchangeName];
16182
16183
  if (frameName)
16183
16184
  parts.push(frameName);
@@ -16229,7 +16230,7 @@ class ActionConnectionService {
16229
16230
  * @param backtest - True if backtest mode, false if live mode
16230
16231
  * @returns Configured ClientAction instance
16231
16232
  */
16232
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$v(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16233
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$y(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16233
16234
  const schema = this.actionSchemaService.get(actionName);
16234
16235
  return new ClientAction({
16235
16236
  ...schema,
@@ -16455,7 +16456,7 @@ class ActionConnectionService {
16455
16456
  await Promise.all(actions.map(async (action) => await action.dispose()));
16456
16457
  return;
16457
16458
  }
16458
- const key = CREATE_KEY_FN$v(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16459
+ const key = CREATE_KEY_FN$y(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16459
16460
  if (!this.getAction.has(key)) {
16460
16461
  return;
16461
16462
  }
@@ -16466,14 +16467,14 @@ class ActionConnectionService {
16466
16467
  }
16467
16468
  }
16468
16469
 
16469
- const METHOD_NAME_VALIDATE$3 = "exchangeCoreService validate";
16470
+ const METHOD_NAME_VALIDATE$6 = "exchangeCoreService validate";
16470
16471
  /**
16471
16472
  * Creates a unique key for memoizing validate calls.
16472
16473
  * Key format: "exchangeName"
16473
16474
  * @param exchangeName - Exchange name
16474
16475
  * @returns Unique string key for memoization
16475
16476
  */
16476
- const CREATE_KEY_FN$u = (exchangeName) => {
16477
+ const CREATE_KEY_FN$x = (exchangeName) => {
16477
16478
  return exchangeName;
16478
16479
  };
16479
16480
  /**
@@ -16497,11 +16498,11 @@ class ExchangeCoreService {
16497
16498
  * @param exchangeName - Name of the exchange to validate
16498
16499
  * @returns Promise that resolves when validation is complete
16499
16500
  */
16500
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$u(exchangeName), async (exchangeName) => {
16501
- this.loggerService.log(METHOD_NAME_VALIDATE$3, {
16501
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$x(exchangeName), async (exchangeName) => {
16502
+ this.loggerService.log(METHOD_NAME_VALIDATE$6, {
16502
16503
  exchangeName,
16503
16504
  });
16504
- this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$3);
16505
+ this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$6);
16505
16506
  });
16506
16507
  /**
16507
16508
  * Fetches historical candles with execution context.
@@ -16770,14 +16771,14 @@ class ExchangeCoreService {
16770
16771
  }
16771
16772
  }
16772
16773
 
16773
- const METHOD_NAME_VALIDATE$2 = "strategyCoreService validate";
16774
+ const METHOD_NAME_VALIDATE$5 = "strategyCoreService validate";
16774
16775
  /**
16775
16776
  * Creates a unique key for memoizing validate calls.
16776
16777
  * Key format: "strategyName:exchangeName:frameName"
16777
16778
  * @param context - Execution context with strategyName, exchangeName, frameName
16778
16779
  * @returns Unique string key for memoization
16779
16780
  */
16780
- const CREATE_KEY_FN$t = (context) => {
16781
+ const CREATE_KEY_FN$w = (context) => {
16781
16782
  const parts = [context.strategyName, context.exchangeName];
16782
16783
  if (context.frameName)
16783
16784
  parts.push(context.frameName);
@@ -16800,6 +16801,7 @@ class StrategyCoreService {
16800
16801
  this.strategyValidationService = inject(TYPES.strategyValidationService);
16801
16802
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
16802
16803
  this.frameValidationService = inject(TYPES.frameValidationService);
16804
+ this.actionValidationService = inject(TYPES.actionValidationService);
16803
16805
  /**
16804
16806
  * Validates strategy and associated risk configuration.
16805
16807
  *
@@ -16809,16 +16811,17 @@ class StrategyCoreService {
16809
16811
  * @param context - Execution context with strategyName, exchangeName, frameName
16810
16812
  * @returns Promise that resolves when validation is complete
16811
16813
  */
16812
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$t(context), async (context) => {
16813
- this.loggerService.log(METHOD_NAME_VALIDATE$2, {
16814
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$w(context), async (context) => {
16815
+ this.loggerService.log(METHOD_NAME_VALIDATE$5, {
16814
16816
  context,
16815
16817
  });
16816
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
16817
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$2);
16818
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$2);
16819
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$2);
16820
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2);
16821
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2));
16818
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
16819
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$5);
16820
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$5);
16821
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$5);
16822
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5);
16823
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5));
16824
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$5));
16822
16825
  });
16823
16826
  /**
16824
16827
  * Retrieves the currently active pending signal for the symbol.
@@ -18179,7 +18182,7 @@ class SizingGlobalService {
18179
18182
  * @param context - Context with riskName, exchangeName, frameName
18180
18183
  * @returns Unique string key for memoization
18181
18184
  */
18182
- const CREATE_KEY_FN$s = (context) => {
18185
+ const CREATE_KEY_FN$v = (context) => {
18183
18186
  const parts = [context.riskName, context.exchangeName];
18184
18187
  if (context.frameName)
18185
18188
  parts.push(context.frameName);
@@ -18205,7 +18208,7 @@ class RiskGlobalService {
18205
18208
  * @param payload - Payload with riskName, exchangeName and frameName
18206
18209
  * @returns Promise that resolves when validation is complete
18207
18210
  */
18208
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$s(context), async (context) => {
18211
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$v(context), async (context) => {
18209
18212
  this.loggerService.log("riskGlobalService validate", {
18210
18213
  context,
18211
18214
  });
@@ -18297,14 +18300,14 @@ class RiskGlobalService {
18297
18300
  }
18298
18301
  }
18299
18302
 
18300
- const METHOD_NAME_VALIDATE$1 = "actionCoreService validate";
18303
+ const METHOD_NAME_VALIDATE$4 = "actionCoreService validate";
18301
18304
  /**
18302
18305
  * Creates a unique key for memoizing validate calls.
18303
18306
  * Key format: "strategyName:exchangeName:frameName"
18304
18307
  * @param context - Execution context with strategyName, exchangeName, frameName
18305
18308
  * @returns Unique string key for memoization
18306
18309
  */
18307
- const CREATE_KEY_FN$r = (context) => {
18310
+ const CREATE_KEY_FN$u = (context) => {
18308
18311
  const parts = [context.strategyName, context.exchangeName];
18309
18312
  if (context.frameName)
18310
18313
  parts.push(context.frameName);
@@ -18348,17 +18351,17 @@ class ActionCoreService {
18348
18351
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
18349
18352
  * @returns Promise that resolves when all validations complete
18350
18353
  */
18351
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$r(context), async (context) => {
18352
- this.loggerService.log(METHOD_NAME_VALIDATE$1, {
18354
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$u(context), async (context) => {
18355
+ this.loggerService.log(METHOD_NAME_VALIDATE$4, {
18353
18356
  context,
18354
18357
  });
18355
18358
  const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
18356
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$1);
18357
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$1);
18358
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$1);
18359
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1);
18360
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1));
18361
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$1));
18359
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$4);
18360
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$4);
18361
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$4);
18362
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4);
18363
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4));
18364
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$4));
18362
18365
  });
18363
18366
  /**
18364
18367
  * Initializes all ClientAction instances for the strategy.
@@ -18854,8 +18857,8 @@ class FrameSchemaService {
18854
18857
  if (typeof frameSchema.frameName !== "string") {
18855
18858
  throw new Error(`frame schema validation failed: missing frameName`);
18856
18859
  }
18857
- if (typeof frameSchema.interval !== "string") {
18858
- throw new Error(`frame schema validation failed: missing interval for frameName=${frameSchema.frameName}`);
18860
+ if (frameSchema.interval && typeof frameSchema.interval !== "string") {
18861
+ throw new Error(`frame schema validation failed: invalid interval for frameName=${frameSchema.frameName}`);
18859
18862
  }
18860
18863
  if (!(frameSchema.startDate instanceof Date)) {
18861
18864
  throw new Error(`frame schema validation failed: missing startDate for frameName=${frameSchema.frameName}`);
@@ -20848,6 +20851,17 @@ class WalkerLogicPublicService {
20848
20851
  }
20849
20852
 
20850
20853
  const METHOD_NAME_RUN$2 = "liveCommandService run";
20854
+ const METHOD_NAME_VALIDATE$3 = "liveCommandService validate";
20855
+ /**
20856
+ * Creates a unique key for memoizing validate calls.
20857
+ * Key format: "strategyName:exchangeName:frameName"
20858
+ * @param context - Context with strategyName, exchangeName, frameName
20859
+ * @returns Unique string key for memoization
20860
+ */
20861
+ const CREATE_KEY_FN$t = (context) => {
20862
+ const parts = [context.strategyName, context.exchangeName];
20863
+ return parts.join(":");
20864
+ };
20851
20865
  /**
20852
20866
  * Global service providing access to live trading functionality.
20853
20867
  *
@@ -20863,6 +20877,25 @@ class LiveCommandService {
20863
20877
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20864
20878
  this.riskValidationService = inject(TYPES.riskValidationService);
20865
20879
  this.actionValidationService = inject(TYPES.actionValidationService);
20880
+ /**
20881
+ * Validates strategy and associated risk configuration.
20882
+ * Memoized to avoid redundant validations for the same strategy-exchange combination.
20883
+ *
20884
+ * @param context - Context with strategyName, exchangeName
20885
+ * @param methodName - Name of the calling method for error tracking
20886
+ */
20887
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$t(context), (context, methodName) => {
20888
+ this.loggerService.log(METHOD_NAME_VALIDATE$3, {
20889
+ context,
20890
+ methodName,
20891
+ });
20892
+ this.strategyValidationService.validate(context.strategyName, methodName);
20893
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20894
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20895
+ riskName && this.riskValidationService.validate(riskName, methodName);
20896
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20897
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20898
+ });
20866
20899
  /**
20867
20900
  * Runs live trading for a symbol with context propagation.
20868
20901
  *
@@ -20877,22 +20910,26 @@ class LiveCommandService {
20877
20910
  symbol,
20878
20911
  context,
20879
20912
  });
20880
- {
20881
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
20882
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
20883
- }
20884
- {
20885
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20886
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
20887
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2));
20888
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$2));
20889
- }
20913
+ this.validate(context, METHOD_NAME_RUN$2);
20890
20914
  return this.liveLogicPublicService.run(symbol, context);
20891
20915
  };
20892
20916
  }
20893
20917
  }
20894
20918
 
20895
20919
  const METHOD_NAME_RUN$1 = "backtestCommandService run";
20920
+ const METHOD_NAME_VALIDATE$2 = "backtestCommandService validate";
20921
+ /**
20922
+ * Creates a unique key for memoizing validate calls.
20923
+ * Key format: "strategyName:exchangeName:frameName"
20924
+ * @param context - Context with strategyName, exchangeName, frameName
20925
+ * @returns Unique string key for memoization
20926
+ */
20927
+ const CREATE_KEY_FN$s = (context) => {
20928
+ const parts = [context.strategyName, context.exchangeName];
20929
+ if (context.frameName)
20930
+ parts.push(context.frameName);
20931
+ return parts.join(":");
20932
+ };
20896
20933
  /**
20897
20934
  * Global service providing access to backtest functionality.
20898
20935
  *
@@ -20909,6 +20946,26 @@ class BacktestCommandService {
20909
20946
  this.strategyValidationService = inject(TYPES.strategyValidationService);
20910
20947
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
20911
20948
  this.frameValidationService = inject(TYPES.frameValidationService);
20949
+ /**
20950
+ * Validates strategy and associated risk configuration.
20951
+ * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
20952
+ *
20953
+ * @param context - Context with strategyName, exchangeName and frameName
20954
+ * @param methodName - Name of the calling method for error tracking
20955
+ */
20956
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$s(context), (context, methodName) => {
20957
+ this.loggerService.log(METHOD_NAME_VALIDATE$2, {
20958
+ context,
20959
+ methodName,
20960
+ });
20961
+ this.strategyValidationService.validate(context.strategyName, methodName);
20962
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20963
+ this.frameValidationService.validate(context.frameName, methodName);
20964
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20965
+ riskName && this.riskValidationService.validate(riskName, methodName);
20966
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20967
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20968
+ });
20912
20969
  /**
20913
20970
  * Runs backtest for a symbol with context propagation.
20914
20971
  *
@@ -20921,23 +20978,26 @@ class BacktestCommandService {
20921
20978
  symbol,
20922
20979
  context,
20923
20980
  });
20924
- {
20925
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
20926
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
20927
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
20928
- }
20929
- {
20930
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20931
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
20932
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1));
20933
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$1));
20934
- }
20981
+ this.validate(context, METHOD_NAME_RUN$1);
20935
20982
  return this.backtestLogicPublicService.run(symbol, context);
20936
20983
  };
20937
20984
  }
20938
20985
  }
20939
20986
 
20940
20987
  const METHOD_NAME_RUN = "walkerCommandService run";
20988
+ const METHOD_NAME_VALIDATE$1 = "walkerCommandService validate";
20989
+ /**
20990
+ * Creates a unique key for memoizing validate calls.
20991
+ * Key format: "walkerName:exchangeName:frameName"
20992
+ * @param context - Context with walkerName, exchangeName, frameName
20993
+ * @returns Unique string key for memoization
20994
+ */
20995
+ const CREATE_KEY_FN$r = (context) => {
20996
+ const parts = [context.walkerName, context.exchangeName];
20997
+ if (context.frameName)
20998
+ parts.push(context.frameName);
20999
+ return parts.join(":");
21000
+ };
20941
21001
  /**
20942
21002
  * Global service providing access to walker functionality.
20943
21003
  *
@@ -20956,6 +21016,34 @@ class WalkerCommandService {
20956
21016
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20957
21017
  this.riskValidationService = inject(TYPES.riskValidationService);
20958
21018
  this.actionValidationService = inject(TYPES.actionValidationService);
21019
+ /**
21020
+ * Validates walker and associated strategy configurations.
21021
+ * Memoized to avoid redundant validations for the same walker-exchange-frame combination.
21022
+ *
21023
+ * Strategy/risk/action validation is performed explicitly here in addition to the
21024
+ * cascade inside WalkerValidationService — this is critical-path code and the
21025
+ * redundant check is intentional defense-in-depth.
21026
+ *
21027
+ * @param context - Context with walkerName, exchangeName and frameName
21028
+ * @param methodName - Name of the calling method for error tracking
21029
+ */
21030
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$r(context), (context, methodName) => {
21031
+ this.loggerService.log(METHOD_NAME_VALIDATE$1, {
21032
+ context,
21033
+ methodName,
21034
+ });
21035
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
21036
+ this.frameValidationService.validate(context.frameName, methodName);
21037
+ this.walkerValidationService.validate(context.walkerName, methodName);
21038
+ const walkerSchema = this.walkerSchemaService.get(context.walkerName);
21039
+ for (const strategyName of walkerSchema.strategies) {
21040
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
21041
+ this.strategyValidationService.validate(strategyName, methodName);
21042
+ riskName && this.riskValidationService.validate(riskName, methodName);
21043
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
21044
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
21045
+ }
21046
+ });
20959
21047
  /**
20960
21048
  * Runs walker comparison for a symbol with context propagation.
20961
21049
  *
@@ -20967,26 +21055,30 @@ class WalkerCommandService {
20967
21055
  symbol,
20968
21056
  context,
20969
21057
  });
20970
- {
20971
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
20972
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
20973
- this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
20974
- }
20975
- {
20976
- const walkerSchema = this.walkerSchemaService.get(context.walkerName);
20977
- for (const strategyName of walkerSchema.strategies) {
20978
- const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
20979
- this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
20980
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
20981
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN));
20982
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN));
20983
- }
20984
- }
21058
+ this.validate(context, METHOD_NAME_RUN);
20985
21059
  return this.walkerLogicPublicService.run(symbol, context);
20986
21060
  };
20987
21061
  }
20988
21062
  }
20989
21063
 
21064
+ /**
21065
+ * Derives the number of decimal places to show for a price based on the
21066
+ * magnitude of its integer part. Larger prices need fewer decimals; sub-dollar
21067
+ * prices keep full precision.
21068
+ * @param value - The price to derive the decimal scale for
21069
+ * @returns The number of digits after the decimal point
21070
+ */
21071
+ const getPriceScale = (value) => {
21072
+ const abs = Math.abs(value);
21073
+ if (abs >= 1) {
21074
+ // 1..9 -> 4, 10..99 -> 3, 100..999 -> 2, 1000+ -> 2 (floor), capped at 2
21075
+ const digits = Math.floor(Math.log10(abs)) + 1;
21076
+ return Math.max(2, 6 - digits);
21077
+ }
21078
+ // Sub-dollar prices need more precision.
21079
+ return 8;
21080
+ };
21081
+
20990
21082
  /**
20991
21083
  * Converts markdown content to plain text with minimal formatting
20992
21084
  * @param content - Markdown string to convert
@@ -21095,43 +21187,43 @@ const backtest_columns = [
21095
21187
  {
21096
21188
  key: "openPrice",
21097
21189
  label: "Open Price",
21098
- format: (data) => `${data.signal.priceOpen.toFixed(8)} USD`,
21190
+ format: (data) => `${data.signal.priceOpen.toFixed(getPriceScale(data.signal.priceOpen))} USD`,
21099
21191
  isVisible: () => true,
21100
21192
  },
21101
21193
  {
21102
21194
  key: "closePrice",
21103
21195
  label: "Close Price",
21104
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21196
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21105
21197
  isVisible: () => true,
21106
21198
  },
21107
21199
  {
21108
21200
  key: "takeProfit",
21109
21201
  label: "Take Profit",
21110
- format: (data) => `${data.signal.priceTakeProfit.toFixed(8)} USD`,
21202
+ format: (data) => `${data.signal.priceTakeProfit.toFixed(getPriceScale(data.signal.priceTakeProfit))} USD`,
21111
21203
  isVisible: () => true,
21112
21204
  },
21113
21205
  {
21114
21206
  key: "stopLoss",
21115
21207
  label: "Stop Loss",
21116
- format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
21208
+ format: (data) => `${data.signal.priceStopLoss.toFixed(getPriceScale(data.signal.priceStopLoss))} USD`,
21117
21209
  isVisible: () => true,
21118
21210
  },
21119
21211
  {
21120
21212
  key: "originalPriceTakeProfit",
21121
21213
  label: "Original TP",
21122
- format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(8)} USD`,
21214
+ format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(getPriceScale(data.signal.originalPriceTakeProfit))} USD`,
21123
21215
  isVisible: () => true,
21124
21216
  },
21125
21217
  {
21126
21218
  key: "originalPriceStopLoss",
21127
21219
  label: "Original SL",
21128
- format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
21220
+ format: (data) => `${data.signal.originalPriceStopLoss.toFixed(getPriceScale(data.signal.originalPriceStopLoss))} USD`,
21129
21221
  isVisible: () => true,
21130
21222
  },
21131
21223
  {
21132
21224
  key: "originalPriceOpen",
21133
21225
  label: "Original Entry",
21134
- format: (data) => `${data.signal.originalPriceOpen.toFixed(8)} USD`,
21226
+ format: (data) => `${data.signal.originalPriceOpen.toFixed(getPriceScale(data.signal.originalPriceOpen))} USD`,
21135
21227
  isVisible: () => true,
21136
21228
  },
21137
21229
  {
@@ -21434,6 +21526,71 @@ const heat_columns = [
21434
21526
  : "N/A",
21435
21527
  isVisible: () => true,
21436
21528
  },
21529
+ {
21530
+ key: "trend",
21531
+ label: "Trend",
21532
+ format: (data) => data.trend ?? "N/A",
21533
+ isVisible: () => true,
21534
+ },
21535
+ {
21536
+ key: "trendStrength",
21537
+ label: "Trend %/d",
21538
+ format: (data) => data.trendStrength !== null ? functoolsKit.str(data.trendStrength, "%") : "N/A",
21539
+ isVisible: () => true,
21540
+ },
21541
+ {
21542
+ key: "trendConfidence",
21543
+ label: "Trend R²",
21544
+ format: (data) => data.trendConfidence !== null ? data.trendConfidence.toFixed(3) : "N/A",
21545
+ isVisible: () => true,
21546
+ },
21547
+ {
21548
+ key: "buyerPressure",
21549
+ label: "Buyer Pres",
21550
+ format: (data) => data.buyerPressure !== null
21551
+ ? (data.buyerPressure * 100).toFixed(1) + "%"
21552
+ : "N/A",
21553
+ isVisible: () => true,
21554
+ },
21555
+ {
21556
+ key: "sellerPressure",
21557
+ label: "Seller Pres",
21558
+ format: (data) => data.sellerPressure !== null
21559
+ ? (data.sellerPressure * 100).toFixed(1) + "%"
21560
+ : "N/A",
21561
+ isVisible: () => true,
21562
+ },
21563
+ {
21564
+ key: "buyerStrength",
21565
+ label: "Buyer Str",
21566
+ format: (data) => data.buyerStrength !== null
21567
+ ? (data.buyerStrength * 100).toFixed(1) + "%"
21568
+ : "N/A",
21569
+ isVisible: () => true,
21570
+ },
21571
+ {
21572
+ key: "sellerStrength",
21573
+ label: "Seller Str",
21574
+ format: (data) => data.sellerStrength !== null
21575
+ ? (data.sellerStrength * 100).toFixed(1) + "%"
21576
+ : "N/A",
21577
+ isVisible: () => true,
21578
+ },
21579
+ {
21580
+ key: "pressureImbalance",
21581
+ label: "Pres Imb",
21582
+ format: (data) => data.pressureImbalance !== null
21583
+ ? (data.pressureImbalance > 0 ? "+" : "") +
21584
+ data.pressureImbalance.toFixed(3)
21585
+ : "N/A",
21586
+ isVisible: () => true,
21587
+ },
21588
+ {
21589
+ key: "medianStepSize",
21590
+ label: "Median Step",
21591
+ format: (data) => data.medianStepSize !== null ? functoolsKit.str(data.medianStepSize, "%") : "N/A",
21592
+ isVisible: () => true,
21593
+ },
21437
21594
  {
21438
21595
  key: "sortinoRatio",
21439
21596
  label: "Sortino",
@@ -21530,34 +21687,34 @@ const live_columns = [
21530
21687
  {
21531
21688
  key: "currentPrice",
21532
21689
  label: "Current Price",
21533
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21690
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21534
21691
  isVisible: () => true,
21535
21692
  },
21536
21693
  {
21537
21694
  key: "openPrice",
21538
21695
  label: "Open Price",
21539
- format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(8)} USD` : "N/A",
21696
+ format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD` : "N/A",
21540
21697
  isVisible: () => true,
21541
21698
  },
21542
21699
  {
21543
21700
  key: "takeProfit",
21544
21701
  label: "Take Profit",
21545
21702
  format: (data) => data.priceTakeProfit !== undefined
21546
- ? `${data.priceTakeProfit.toFixed(8)} USD`
21703
+ ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`
21547
21704
  : "N/A",
21548
21705
  isVisible: () => true,
21549
21706
  },
21550
21707
  {
21551
21708
  key: "stopLoss",
21552
21709
  label: "Stop Loss",
21553
- format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A",
21710
+ format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A",
21554
21711
  isVisible: () => true,
21555
21712
  },
21556
21713
  {
21557
21714
  key: "originalPriceTakeProfit",
21558
21715
  label: "Original TP",
21559
21716
  format: (data) => data.originalPriceTakeProfit !== undefined
21560
- ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
21717
+ ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD`
21561
21718
  : "N/A",
21562
21719
  isVisible: () => true,
21563
21720
  },
@@ -21565,7 +21722,7 @@ const live_columns = [
21565
21722
  key: "originalPriceStopLoss",
21566
21723
  label: "Original SL",
21567
21724
  format: (data) => data.originalPriceStopLoss !== undefined
21568
- ? `${data.originalPriceStopLoss.toFixed(8)} USD`
21725
+ ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD`
21569
21726
  : "N/A",
21570
21727
  isVisible: () => true,
21571
21728
  },
@@ -21573,7 +21730,7 @@ const live_columns = [
21573
21730
  key: "originalPriceOpen",
21574
21731
  label: "Original Entry",
21575
21732
  format: (data) => data.originalPriceOpen !== undefined
21576
- ? `${data.originalPriceOpen.toFixed(8)} USD`
21733
+ ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD`
21577
21734
  : "N/A",
21578
21735
  isVisible: () => true,
21579
21736
  },
@@ -21744,43 +21901,43 @@ const partial_columns = [
21744
21901
  {
21745
21902
  key: "currentPrice",
21746
21903
  label: "Current Price",
21747
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21904
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21748
21905
  isVisible: () => true,
21749
21906
  },
21750
21907
  {
21751
21908
  key: "priceOpen",
21752
21909
  label: "Entry Price",
21753
- format: (data) => (data.priceOpen ? `${data.priceOpen.toFixed(8)} USD` : "N/A"),
21910
+ format: (data) => (data.priceOpen ? `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD` : "N/A"),
21754
21911
  isVisible: () => true,
21755
21912
  },
21756
21913
  {
21757
21914
  key: "priceTakeProfit",
21758
21915
  label: "Take Profit",
21759
- format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(8)} USD` : "N/A"),
21916
+ format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD` : "N/A"),
21760
21917
  isVisible: () => true,
21761
21918
  },
21762
21919
  {
21763
21920
  key: "priceStopLoss",
21764
21921
  label: "Stop Loss",
21765
- format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A"),
21922
+ format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A"),
21766
21923
  isVisible: () => true,
21767
21924
  },
21768
21925
  {
21769
21926
  key: "originalPriceTakeProfit",
21770
21927
  label: "Original TP",
21771
- format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(8)} USD` : "N/A"),
21928
+ format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD` : "N/A"),
21772
21929
  isVisible: () => true,
21773
21930
  },
21774
21931
  {
21775
21932
  key: "originalPriceStopLoss",
21776
21933
  label: "Original SL",
21777
- format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
21934
+ format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD` : "N/A"),
21778
21935
  isVisible: () => true,
21779
21936
  },
21780
21937
  {
21781
21938
  key: "originalPriceOpen",
21782
21939
  label: "Original Entry",
21783
- format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
21940
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD` : "N/A"),
21784
21941
  isVisible: () => true,
21785
21942
  },
21786
21943
  {
@@ -21904,43 +22061,43 @@ const breakeven_columns = [
21904
22061
  {
21905
22062
  key: "priceOpen",
21906
22063
  label: "Entry Price",
21907
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22064
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
21908
22065
  isVisible: () => true,
21909
22066
  },
21910
22067
  {
21911
22068
  key: "currentPrice",
21912
22069
  label: "Breakeven Price",
21913
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22070
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21914
22071
  isVisible: () => true,
21915
22072
  },
21916
22073
  {
21917
22074
  key: "priceTakeProfit",
21918
22075
  label: "Take Profit",
21919
- format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(8)} USD` : "N/A"),
22076
+ format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD` : "N/A"),
21920
22077
  isVisible: () => true,
21921
22078
  },
21922
22079
  {
21923
22080
  key: "priceStopLoss",
21924
22081
  label: "Stop Loss",
21925
- format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A"),
22082
+ format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A"),
21926
22083
  isVisible: () => true,
21927
22084
  },
21928
22085
  {
21929
22086
  key: "originalPriceTakeProfit",
21930
22087
  label: "Original TP",
21931
- format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(8)} USD` : "N/A"),
22088
+ format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD` : "N/A"),
21932
22089
  isVisible: () => true,
21933
22090
  },
21934
22091
  {
21935
22092
  key: "originalPriceStopLoss",
21936
22093
  label: "Original SL",
21937
- format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
22094
+ format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD` : "N/A"),
21938
22095
  isVisible: () => true,
21939
22096
  },
21940
22097
  {
21941
22098
  key: "originalPriceOpen",
21942
22099
  label: "Original Entry",
21943
- format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
22100
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD` : "N/A"),
21944
22101
  isVisible: () => true,
21945
22102
  },
21946
22103
  {
@@ -22197,7 +22354,7 @@ const risk_columns = [
22197
22354
  key: "openPrice",
22198
22355
  label: "Open Price",
22199
22356
  format: (data) => data.currentSignal.priceOpen !== undefined
22200
- ? `${data.currentSignal.priceOpen.toFixed(8)} USD`
22357
+ ? `${data.currentSignal.priceOpen.toFixed(getPriceScale(data.currentSignal.priceOpen))} USD`
22201
22358
  : "N/A",
22202
22359
  isVisible: () => true,
22203
22360
  },
@@ -22205,7 +22362,7 @@ const risk_columns = [
22205
22362
  key: "takeProfit",
22206
22363
  label: "Take Profit",
22207
22364
  format: (data) => data.currentSignal.priceTakeProfit !== undefined
22208
- ? `${data.currentSignal.priceTakeProfit.toFixed(8)} USD`
22365
+ ? `${data.currentSignal.priceTakeProfit.toFixed(getPriceScale(data.currentSignal.priceTakeProfit))} USD`
22209
22366
  : "N/A",
22210
22367
  isVisible: () => true,
22211
22368
  },
@@ -22213,7 +22370,7 @@ const risk_columns = [
22213
22370
  key: "stopLoss",
22214
22371
  label: "Stop Loss",
22215
22372
  format: (data) => data.currentSignal.priceStopLoss !== undefined
22216
- ? `${data.currentSignal.priceStopLoss.toFixed(8)} USD`
22373
+ ? `${data.currentSignal.priceStopLoss.toFixed(getPriceScale(data.currentSignal.priceStopLoss))} USD`
22217
22374
  : "N/A",
22218
22375
  isVisible: () => true,
22219
22376
  },
@@ -22221,7 +22378,7 @@ const risk_columns = [
22221
22378
  key: "originalPriceTakeProfit",
22222
22379
  label: "Original TP",
22223
22380
  format: (data) => data.currentSignal.originalPriceTakeProfit !== undefined
22224
- ? `${data.currentSignal.originalPriceTakeProfit.toFixed(8)} USD`
22381
+ ? `${data.currentSignal.originalPriceTakeProfit.toFixed(getPriceScale(data.currentSignal.originalPriceTakeProfit))} USD`
22225
22382
  : "N/A",
22226
22383
  isVisible: () => true,
22227
22384
  },
@@ -22229,7 +22386,7 @@ const risk_columns = [
22229
22386
  key: "originalPriceStopLoss",
22230
22387
  label: "Original SL",
22231
22388
  format: (data) => data.currentSignal.originalPriceStopLoss !== undefined
22232
- ? `${data.currentSignal.originalPriceStopLoss.toFixed(8)} USD`
22389
+ ? `${data.currentSignal.originalPriceStopLoss.toFixed(getPriceScale(data.currentSignal.originalPriceStopLoss))} USD`
22233
22390
  : "N/A",
22234
22391
  isVisible: () => true,
22235
22392
  },
@@ -22237,7 +22394,7 @@ const risk_columns = [
22237
22394
  key: "originalPriceOpen",
22238
22395
  label: "Original Entry",
22239
22396
  format: (data) => data.currentSignal.originalPriceOpen !== undefined
22240
- ? `${data.currentSignal.originalPriceOpen.toFixed(8)} USD`
22397
+ ? `${data.currentSignal.originalPriceOpen.toFixed(getPriceScale(data.currentSignal.originalPriceOpen))} USD`
22241
22398
  : "N/A",
22242
22399
  isVisible: () => true,
22243
22400
  },
@@ -22268,7 +22425,7 @@ const risk_columns = [
22268
22425
  {
22269
22426
  key: "currentPrice",
22270
22427
  label: "Current Price",
22271
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22428
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22272
22429
  isVisible: () => true,
22273
22430
  },
22274
22431
  {
@@ -22394,32 +22551,32 @@ const schedule_columns = [
22394
22551
  {
22395
22552
  key: "currentPrice",
22396
22553
  label: "Current Price",
22397
- format: (data) => data.currentPrice ? `${data.currentPrice.toFixed(8)} USD` : "N/A",
22554
+ format: (data) => data.currentPrice ? `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD` : "N/A",
22398
22555
  isVisible: () => true,
22399
22556
  },
22400
22557
  {
22401
22558
  key: "priceOpen",
22402
22559
  label: "Entry Price",
22403
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22560
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22404
22561
  isVisible: () => true,
22405
22562
  },
22406
22563
  {
22407
22564
  key: "takeProfit",
22408
22565
  label: "Take Profit",
22409
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22566
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22410
22567
  isVisible: () => true,
22411
22568
  },
22412
22569
  {
22413
22570
  key: "stopLoss",
22414
22571
  label: "Stop Loss",
22415
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22572
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22416
22573
  isVisible: () => true,
22417
22574
  },
22418
22575
  {
22419
22576
  key: "originalPriceTakeProfit",
22420
22577
  label: "Original TP",
22421
22578
  format: (data) => data.originalPriceTakeProfit !== undefined
22422
- ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
22579
+ ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD`
22423
22580
  : "N/A",
22424
22581
  isVisible: () => true,
22425
22582
  },
@@ -22427,7 +22584,7 @@ const schedule_columns = [
22427
22584
  key: "originalPriceStopLoss",
22428
22585
  label: "Original SL",
22429
22586
  format: (data) => data.originalPriceStopLoss !== undefined
22430
- ? `${data.originalPriceStopLoss.toFixed(8)} USD`
22587
+ ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD`
22431
22588
  : "N/A",
22432
22589
  isVisible: () => true,
22433
22590
  },
@@ -22435,7 +22592,7 @@ const schedule_columns = [
22435
22592
  key: "originalPriceOpen",
22436
22593
  label: "Original Entry",
22437
22594
  format: (data) => data.originalPriceOpen !== undefined
22438
- ? `${data.originalPriceOpen.toFixed(8)} USD`
22595
+ ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD`
22439
22596
  : "N/A",
22440
22597
  isVisible: () => true,
22441
22598
  },
@@ -22560,7 +22717,7 @@ const strategy_columns = [
22560
22717
  {
22561
22718
  key: "currentPrice",
22562
22719
  label: "Price",
22563
- format: (data) => (data.currentPrice !== undefined ? `${data.currentPrice.toFixed(8)} USD` : "N/A"),
22720
+ format: (data) => (data.currentPrice !== undefined ? `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD` : "N/A"),
22564
22721
  isVisible: () => true,
22565
22722
  },
22566
22723
  {
@@ -22680,25 +22837,25 @@ const sync_columns = [
22680
22837
  {
22681
22838
  key: "currentPrice",
22682
22839
  label: "Current Price",
22683
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22840
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22684
22841
  isVisible: () => true,
22685
22842
  },
22686
22843
  {
22687
22844
  key: "priceOpen",
22688
22845
  label: "Entry Price",
22689
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22846
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22690
22847
  isVisible: () => true,
22691
22848
  },
22692
22849
  {
22693
22850
  key: "priceTakeProfit",
22694
22851
  label: "Take Profit",
22695
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22852
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22696
22853
  isVisible: () => true,
22697
22854
  },
22698
22855
  {
22699
22856
  key: "priceStopLoss",
22700
22857
  label: "Stop Loss",
22701
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22858
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22702
22859
  isVisible: () => true,
22703
22860
  },
22704
22861
  {
@@ -22791,25 +22948,25 @@ const highest_profit_columns = [
22791
22948
  {
22792
22949
  key: "currentPrice",
22793
22950
  label: "Peak Price",
22794
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22951
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22795
22952
  isVisible: () => true,
22796
22953
  },
22797
22954
  {
22798
22955
  key: "priceOpen",
22799
22956
  label: "Entry Price",
22800
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22957
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22801
22958
  isVisible: () => true,
22802
22959
  },
22803
22960
  {
22804
22961
  key: "priceTakeProfit",
22805
22962
  label: "Take Profit",
22806
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22963
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22807
22964
  isVisible: () => true,
22808
22965
  },
22809
22966
  {
22810
22967
  key: "priceStopLoss",
22811
22968
  label: "Stop Loss",
22812
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22969
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22813
22970
  isVisible: () => true,
22814
22971
  },
22815
22972
  {
@@ -22878,25 +23035,25 @@ const max_drawdown_columns = [
22878
23035
  {
22879
23036
  key: "currentPrice",
22880
23037
  label: "DD Price",
22881
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
23038
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22882
23039
  isVisible: () => true,
22883
23040
  },
22884
23041
  {
22885
23042
  key: "priceOpen",
22886
23043
  label: "Entry Price",
22887
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
23044
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22888
23045
  isVisible: () => true,
22889
23046
  },
22890
23047
  {
22891
23048
  key: "priceTakeProfit",
22892
23049
  label: "Take Profit",
22893
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
23050
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22894
23051
  isVisible: () => true,
22895
23052
  },
22896
23053
  {
22897
23054
  key: "priceStopLoss",
22898
23055
  label: "Stop Loss",
22899
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
23056
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22900
23057
  isVisible: () => true,
22901
23058
  },
22902
23059
  {
@@ -23778,6 +23935,184 @@ MarkdownFileBase = functoolsKit.makeExtendable(MarkdownFileBase);
23778
23935
  ReportBase = functoolsKit.makeExtendable(ReportBase);
23779
23936
  const ReportWriter = new ReportWriterAdapter();
23780
23937
 
23938
+ /**
23939
+ * Price-profile metrics derived purely from a series of trade closes — no
23940
+ * candles, no exchange queries. Designed for `BacktestMarkdownService`,
23941
+ * `LiveMarkdownService` and per-symbol Heat: all three already have a
23942
+ * chronological series of `(closeAt, close)` points and nothing else.
23943
+ *
23944
+ * Conventions
23945
+ * -----------
23946
+ * - Pressure: fraction of up-moves vs down-moves (frequency).
23947
+ * - Strength: fraction of upward magnitude vs total movement.
23948
+ * - A divergence between pressure and strength surfaces asymmetry — e.g.
23949
+ * frequent shallow up-moves vs rare deep down-moves is "rising on weak
23950
+ * buys, falling on strong sells".
23951
+ * - Trend: linear regression of log(close) vs days. Slope in %/day,
23952
+ * confidence in R². Classification is bivariate (slope × R²): neither
23953
+ * axis alone fires, both must agree. Slope threshold is normalised by
23954
+ * medianStepSize so the metric self-tunes to the instrument's typical
23955
+ * move size.
23956
+ */
23957
+ /** Minimum samples to surface any price-profile metric. Below this the
23958
+ * per-trade step-distribution and the regression are statistically noisy. */
23959
+ const MIN_SIGNALS = 10;
23960
+ /** R² gate for declaring any trend at all. Below this the regression is
23961
+ * too weak to claim a direction — treat the series as sideways even if
23962
+ * the slope is large. 0.30 is the conventional weak-to-moderate-fit
23963
+ * boundary in econometrics (Cohen's f² ≈ 0.43). */
23964
+ const R2_TREND_GATE = 0.30;
23965
+ /** Slope-magnitude threshold relative to medianStepSize for declaring the
23966
+ * trend strong enough to call bullish/bearish. Below this the regression
23967
+ * fits but the actual drift is weaker than the typical daily step, so we
23968
+ * downgrade to "neutral" (a real but uninteresting tilt). */
23969
+ const SLOPE_VS_STEP_GATE = 0.25;
23970
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
23971
+ const median = (values) => {
23972
+ const sorted = values.slice().sort((a, b) => a - b);
23973
+ const mid = sorted.length >> 1;
23974
+ return sorted.length % 2 === 0
23975
+ ? (sorted[mid - 1] + sorted[mid]) / 2
23976
+ : sorted[mid];
23977
+ };
23978
+ const emptyProfile = () => ({
23979
+ medianStepSize: null,
23980
+ buyerPressure: null,
23981
+ sellerPressure: null,
23982
+ buyerStrength: null,
23983
+ sellerStrength: null,
23984
+ pressureImbalance: null,
23985
+ trend: null,
23986
+ trendStrength: null,
23987
+ trendConfidence: null,
23988
+ });
23989
+ /**
23990
+ * Computes the price-profile bundle for a chronological series of trade
23991
+ * closes. The input is expected to be sorted by `closeAt` ascending; the
23992
+ * function does not sort defensively (the markdown services already sort).
23993
+ *
23994
+ * @param series - One point per closed trade: timestamp (ms) and close price.
23995
+ * @returns A bundle of nine metrics, each `null` when the input is too small
23996
+ * or numerically unsafe.
23997
+ */
23998
+ const getPriceProfile = (series) => {
23999
+ const valid = series.filter((p) => isFiniteNumber(p.closeAt) &&
24000
+ p.closeAt > 0 &&
24001
+ isFiniteNumber(p.close) &&
24002
+ p.close > 0);
24003
+ if (valid.length < MIN_SIGNALS)
24004
+ return emptyProfile();
24005
+ const n = valid.length;
24006
+ const closes = valid.map((p) => p.close);
24007
+ const times = valid.map((p) => p.closeAt);
24008
+ const absSteps = [];
24009
+ let upMoves = 0;
24010
+ let downMoves = 0;
24011
+ let upMagnitude = 0;
24012
+ let downMagnitude = 0;
24013
+ for (let i = 1; i < n; i++) {
24014
+ const prev = closes[i - 1];
24015
+ const cur = closes[i];
24016
+ const ret = (cur - prev) / prev;
24017
+ const abs = Math.abs(ret);
24018
+ absSteps.push(abs);
24019
+ if (ret > 0) {
24020
+ upMoves++;
24021
+ upMagnitude += abs;
24022
+ }
24023
+ else if (ret < 0) {
24024
+ downMoves++;
24025
+ downMagnitude += abs;
24026
+ }
24027
+ }
24028
+ if (absSteps.length === 0)
24029
+ return emptyProfile();
24030
+ const medianStepSize = median(absSteps) * 100; // percent
24031
+ // --- Pressure / strength ---
24032
+ const decisiveMoves = upMoves + downMoves;
24033
+ const totalMagnitude = upMagnitude + downMagnitude;
24034
+ const buyerPressure = decisiveMoves > 0 ? upMoves / decisiveMoves : null;
24035
+ const sellerPressure = decisiveMoves > 0 ? downMoves / decisiveMoves : null;
24036
+ const buyerStrength = totalMagnitude > 0 ? upMagnitude / totalMagnitude : null;
24037
+ const sellerStrength = totalMagnitude > 0 ? downMagnitude / totalMagnitude : null;
24038
+ const pressureImbalance = buyerStrength !== null && sellerStrength !== null
24039
+ ? buyerStrength - sellerStrength
24040
+ : null;
24041
+ // --- Trend: linear regression of log(close) vs days ---
24042
+ // log-price slope is scale-invariant: 1%/day means the same whether the
24043
+ // asset trades at $0.01 or $10000. Use `closeAt[0]` as time origin so x_i
24044
+ // starts at zero — keeps numerical conditioning sane on long horizons.
24045
+ const t0 = times[0];
24046
+ const xs = new Array(n);
24047
+ const ys = new Array(n);
24048
+ for (let i = 0; i < n; i++) {
24049
+ xs[i] = (times[i] - t0) / (1000 * 60 * 60 * 24); // days
24050
+ ys[i] = Math.log(closes[i]);
24051
+ }
24052
+ // Calendar span must be non-degenerate for the slope to mean anything.
24053
+ const xRange = xs[n - 1] - xs[0];
24054
+ let trend = null;
24055
+ let trendStrength = null;
24056
+ let trendConfidence = null;
24057
+ if (xRange > 0) {
24058
+ let sumX = 0;
24059
+ let sumY = 0;
24060
+ for (let i = 0; i < n; i++) {
24061
+ sumX += xs[i];
24062
+ sumY += ys[i];
24063
+ }
24064
+ const meanX = sumX / n;
24065
+ const meanY = sumY / n;
24066
+ let ssXX = 0;
24067
+ let ssXY = 0;
24068
+ let ssYY = 0;
24069
+ for (let i = 0; i < n; i++) {
24070
+ const dx = xs[i] - meanX;
24071
+ const dy = ys[i] - meanY;
24072
+ ssXX += dx * dx;
24073
+ ssXY += dx * dy;
24074
+ ssYY += dy * dy;
24075
+ }
24076
+ if (ssXX > 0) {
24077
+ const slopeLog = ssXY / ssXX; // log-return per day
24078
+ const slopePct = slopeLog * 100; // %/day (small-slope approx)
24079
+ // R² = 1 - SS_res / SS_tot. With a single explanatory variable this
24080
+ // equals (ssXY)² / (ssXX * ssYY) when ssYY > 0.
24081
+ const r2 = ssYY > 0 ? (ssXY * ssXY) / (ssXX * ssYY) : 0;
24082
+ trendStrength = slopePct;
24083
+ trendConfidence = Math.max(0, Math.min(1, r2));
24084
+ if (trendConfidence < R2_TREND_GATE) {
24085
+ trend = "sideways";
24086
+ }
24087
+ else {
24088
+ const slopeMagnitude = Math.abs(slopePct);
24089
+ const stepScale = medianStepSize * SLOPE_VS_STEP_GATE;
24090
+ if (slopeMagnitude < stepScale) {
24091
+ trend = "neutral";
24092
+ }
24093
+ else if (slopePct > 0) {
24094
+ trend = "bullish";
24095
+ }
24096
+ else {
24097
+ trend = "bearish";
24098
+ }
24099
+ }
24100
+ }
24101
+ }
24102
+ const safe = (v) => v === null || !Number.isFinite(v) ? null : v;
24103
+ return {
24104
+ medianStepSize: safe(medianStepSize),
24105
+ buyerPressure: safe(buyerPressure),
24106
+ sellerPressure: safe(sellerPressure),
24107
+ buyerStrength: safe(buyerStrength),
24108
+ sellerStrength: safe(sellerStrength),
24109
+ pressureImbalance: safe(pressureImbalance),
24110
+ trend,
24111
+ trendStrength: safe(trendStrength),
24112
+ trendConfidence: safe(trendConfidence),
24113
+ };
24114
+ };
24115
+
23781
24116
  /**
23782
24117
  * Creates a unique key for memoizing ReportStorage instances.
23783
24118
  * Key format: "symbol:strategyName:exchangeName:frameName:backtest" or "symbol:strategyName:exchangeName:live"
@@ -23910,6 +24245,15 @@ let ReportStorage$a = class ReportStorage {
23910
24245
  avgConsecutiveLossPnl: null,
23911
24246
  avgWinDuration: null,
23912
24247
  avgLossDuration: null,
24248
+ medianStepSize: null,
24249
+ buyerPressure: null,
24250
+ sellerPressure: null,
24251
+ buyerStrength: null,
24252
+ sellerStrength: null,
24253
+ pressureImbalance: null,
24254
+ trend: null,
24255
+ trendStrength: null,
24256
+ trendConfidence: null,
23913
24257
  };
23914
24258
  }
23915
24259
  // Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
@@ -24206,6 +24550,13 @@ let ReportStorage$a = class ReportStorage {
24206
24550
  const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
24207
24551
  ? null
24208
24552
  : Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
24553
+ // Price profile — buyer/seller pressure, trend classification. Walks the
24554
+ // chronological close series (`orderedSignals` is already sorted by
24555
+ // closeTimestamp). N-gated internally by the helper (MIN_SIGNALS = 10).
24556
+ const priceProfile = getPriceProfile(orderedSignals.map((s) => ({
24557
+ closeAt: s.closeTimestamp,
24558
+ close: s.currentPrice,
24559
+ })));
24209
24560
  return {
24210
24561
  signalList: this._signalList,
24211
24562
  totalSignals,
@@ -24231,6 +24582,15 @@ let ReportStorage$a = class ReportStorage {
24231
24582
  avgConsecutiveLossPnl: isUnsafe$4(avgConsecutiveLossPnl) ? null : avgConsecutiveLossPnl,
24232
24583
  avgWinDuration: isUnsafe$4(avgWinDuration) ? null : avgWinDuration,
24233
24584
  avgLossDuration: isUnsafe$4(avgLossDuration) ? null : avgLossDuration,
24585
+ medianStepSize: priceProfile.medianStepSize,
24586
+ buyerPressure: priceProfile.buyerPressure,
24587
+ sellerPressure: priceProfile.sellerPressure,
24588
+ buyerStrength: priceProfile.buyerStrength,
24589
+ sellerStrength: priceProfile.sellerStrength,
24590
+ pressureImbalance: priceProfile.pressureImbalance,
24591
+ trend: priceProfile.trend,
24592
+ trendStrength: priceProfile.trendStrength,
24593
+ trendConfidence: priceProfile.trendConfidence,
24234
24594
  };
24235
24595
  }
24236
24596
  /**
@@ -24287,6 +24647,15 @@ let ReportStorage$a = class ReportStorage {
24287
24647
  `**Avg Loss Duration:** ${stats.avgLossDuration === null ? "N/A" : `${stats.avgLossDuration.toFixed(1)} min`}`,
24288
24648
  `**Avg Consecutive Win PNL:** ${stats.avgConsecutiveWinPnl === null ? "N/A" : `${stats.avgConsecutiveWinPnl > 0 ? "+" : ""}${stats.avgConsecutiveWinPnl.toFixed(3)}% (higher is better)`}`,
24289
24649
  `**Avg Consecutive Loss PNL:** ${stats.avgConsecutiveLossPnl === null ? "N/A" : `${stats.avgConsecutiveLossPnl.toFixed(3)}% (closer to 0 is better)`}`,
24650
+ `**Trend:** ${stats.trend === null ? "N/A" : stats.trend}`,
24651
+ `**Trend Strength:** ${stats.trendStrength === null ? "N/A" : `${stats.trendStrength > 0 ? "+" : ""}${stats.trendStrength.toFixed(3)}%/day`}`,
24652
+ `**Trend Confidence (R²):** ${stats.trendConfidence === null ? "N/A" : stats.trendConfidence.toFixed(3)}`,
24653
+ `**Buyer Pressure:** ${stats.buyerPressure === null ? "N/A" : `${(stats.buyerPressure * 100).toFixed(1)}%`}`,
24654
+ `**Seller Pressure:** ${stats.sellerPressure === null ? "N/A" : `${(stats.sellerPressure * 100).toFixed(1)}%`}`,
24655
+ `**Buyer Strength:** ${stats.buyerStrength === null ? "N/A" : `${(stats.buyerStrength * 100).toFixed(1)}%`}`,
24656
+ `**Seller Strength:** ${stats.sellerStrength === null ? "N/A" : `${(stats.sellerStrength * 100).toFixed(1)}%`}`,
24657
+ `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
24658
+ `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
24290
24659
  "",
24291
24660
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24292
24661
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
@@ -24300,6 +24669,11 @@ let ReportStorage$a = class ReportStorage {
24300
24669
  `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
24301
24670
  `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24302
24671
  `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures — these metrics represent a theoretical upper bound under full allocation.*`,
24672
+ `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on R² ≥ 0.30 (else "sideways") AND |slope| ≥ 0.25 × medianStepSize (else "neutral" — a real but weak tilt). Self-tunes to the instrument's typical move size — no magic constants on price magnitude.*`,
24673
+ `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
24674
+ `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
24675
+ `*Pressure Imbalance: buyerStrength − sellerStrength ∈ [−1, +1]. Single signed summary of magnitude bias.*`,
24676
+ `*Median Step Size: median |close[i] − close[i−1]| / close[i−1] across closed trades, in %. Robust to outliers — describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility — there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
24303
24677
  `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
24304
24678
  ].join("\n");
24305
24679
  }
@@ -24930,6 +25304,15 @@ let ReportStorage$9 = class ReportStorage {
24930
25304
  avgConsecutiveLossPnl: null,
24931
25305
  avgWinDuration: null,
24932
25306
  avgLossDuration: null,
25307
+ medianStepSize: null,
25308
+ buyerPressure: null,
25309
+ sellerPressure: null,
25310
+ buyerStrength: null,
25311
+ sellerStrength: null,
25312
+ pressureImbalance: null,
25313
+ trend: null,
25314
+ trendStrength: null,
25315
+ trendConfidence: null,
24933
25316
  };
24934
25317
  }
24935
25318
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
@@ -25228,6 +25611,14 @@ let ReportStorage$9 = class ReportStorage {
25228
25611
  const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
25229
25612
  ? null
25230
25613
  : Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
25614
+ // Price profile — buyer/seller pressure, trend classification. Built only
25615
+ // from closed events' (timestamp, currentPrice) pairs in chronological
25616
+ // close order — the same data the equity curve uses. N-gated internally
25617
+ // by the helper (MIN_SIGNALS = 10).
25618
+ const priceProfile = getPriceProfile(validClosed
25619
+ .slice()
25620
+ .sort((a, b) => a.timestamp - b.timestamp)
25621
+ .map((e) => ({ closeAt: e.timestamp, close: e.currentPrice })));
25231
25622
  return {
25232
25623
  eventList: this._eventList,
25233
25624
  totalEvents: this._eventList.length,
@@ -25254,6 +25645,15 @@ let ReportStorage$9 = class ReportStorage {
25254
25645
  avgConsecutiveLossPnl: isUnsafe$3(avgConsecutiveLossPnl) ? null : avgConsecutiveLossPnl,
25255
25646
  avgWinDuration: isUnsafe$3(avgWinDuration) ? null : avgWinDuration,
25256
25647
  avgLossDuration: isUnsafe$3(avgLossDuration) ? null : avgLossDuration,
25648
+ medianStepSize: priceProfile.medianStepSize,
25649
+ buyerPressure: priceProfile.buyerPressure,
25650
+ sellerPressure: priceProfile.sellerPressure,
25651
+ buyerStrength: priceProfile.buyerStrength,
25652
+ sellerStrength: priceProfile.sellerStrength,
25653
+ pressureImbalance: priceProfile.pressureImbalance,
25654
+ trend: priceProfile.trend,
25655
+ trendStrength: priceProfile.trendStrength,
25656
+ trendConfidence: priceProfile.trendConfidence,
25257
25657
  };
25258
25658
  }
25259
25659
  /**
@@ -25310,6 +25710,15 @@ let ReportStorage$9 = class ReportStorage {
25310
25710
  `**Avg Loss Duration:** ${stats.avgLossDuration === null ? "N/A" : `${stats.avgLossDuration.toFixed(1)} min`}`,
25311
25711
  `**Avg Consecutive Win PNL:** ${stats.avgConsecutiveWinPnl === null ? "N/A" : `${stats.avgConsecutiveWinPnl > 0 ? "+" : ""}${stats.avgConsecutiveWinPnl.toFixed(3)}% (higher is better)`}`,
25312
25712
  `**Avg Consecutive Loss PNL:** ${stats.avgConsecutiveLossPnl === null ? "N/A" : `${stats.avgConsecutiveLossPnl.toFixed(3)}% (closer to 0 is better)`}`,
25713
+ `**Trend:** ${stats.trend === null ? "N/A" : stats.trend}`,
25714
+ `**Trend Strength:** ${stats.trendStrength === null ? "N/A" : `${stats.trendStrength > 0 ? "+" : ""}${stats.trendStrength.toFixed(3)}%/day`}`,
25715
+ `**Trend Confidence (R²):** ${stats.trendConfidence === null ? "N/A" : stats.trendConfidence.toFixed(3)}`,
25716
+ `**Buyer Pressure:** ${stats.buyerPressure === null ? "N/A" : `${(stats.buyerPressure * 100).toFixed(1)}%`}`,
25717
+ `**Seller Pressure:** ${stats.sellerPressure === null ? "N/A" : `${(stats.sellerPressure * 100).toFixed(1)}%`}`,
25718
+ `**Buyer Strength:** ${stats.buyerStrength === null ? "N/A" : `${(stats.buyerStrength * 100).toFixed(1)}%`}`,
25719
+ `**Seller Strength:** ${stats.sellerStrength === null ? "N/A" : `${(stats.sellerStrength * 100).toFixed(1)}%`}`,
25720
+ `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
25721
+ `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
25313
25722
  "",
25314
25723
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
25315
25724
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
@@ -25323,6 +25732,11 @@ let ReportStorage$9 = class ReportStorage {
25323
25732
  `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
25324
25733
  `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
25325
25734
  `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures — these metrics represent a theoretical upper bound under full allocation.*`,
25735
+ `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on R² ≥ 0.30 (else "sideways") AND |slope| ≥ 0.25 × medianStepSize (else "neutral" — a real but weak tilt). Self-tunes to the instrument's typical move size — no magic constants on price magnitude.*`,
25736
+ `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
25737
+ `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
25738
+ `*Pressure Imbalance: buyerStrength − sellerStrength ∈ [−1, +1]. Single signed summary of magnitude bias.*`,
25739
+ `*Median Step Size: median |close[i] − close[i−1]| / close[i−1] across closed trades, in %. Robust to outliers — describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility — there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
25326
25740
  `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
25327
25741
  ].join("\n");
25328
25742
  }
@@ -27605,6 +28019,12 @@ class HeatmapStorage {
27605
28019
  calmarRatio = null;
27606
28020
  if (isUnsafe(recoveryFactor))
27607
28021
  recoveryFactor = null;
28022
+ // Price profile — buyer/seller pressure, trend classification. Built from
28023
+ // the chronological close-price series of this symbol's closed signals.
28024
+ // N-gated internally by the helper (MIN_SIGNALS = 10).
28025
+ const priceProfile = getPriceProfile([...signals]
28026
+ .sort((a, b) => a.closeTimestamp - b.closeTimestamp)
28027
+ .map((s) => ({ closeAt: s.closeTimestamp, close: s.currentPrice })));
27608
28028
  return {
27609
28029
  symbol,
27610
28030
  totalPnl,
@@ -27639,6 +28059,15 @@ class HeatmapStorage {
27639
28059
  certaintyRatio,
27640
28060
  expectedYearlyReturns,
27641
28061
  tradesPerYear,
28062
+ medianStepSize: priceProfile.medianStepSize,
28063
+ buyerPressure: priceProfile.buyerPressure,
28064
+ sellerPressure: priceProfile.sellerPressure,
28065
+ buyerStrength: priceProfile.buyerStrength,
28066
+ sellerStrength: priceProfile.sellerStrength,
28067
+ pressureImbalance: priceProfile.pressureImbalance,
28068
+ trend: priceProfile.trend,
28069
+ trendStrength: priceProfile.trendStrength,
28070
+ trendConfidence: priceProfile.trendConfidence,
27642
28071
  };
27643
28072
  }
27644
28073
  /**
@@ -28102,6 +28531,9 @@ class HeatmapStorage {
28102
28531
  `*Max Drawdown: mark-to-market — both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. The pooled curve walks trades chronologically by closeTimestamp; simultaneous cross-symbol drawdowns within the same minute are still serialised (one trade applied at a time), so genuine same-instant tail correlation is not modelled.*`,
28103
28532
  `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
28104
28533
  `*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures — these metrics represent a theoretical upper bound under full allocation.*`,
28534
+ `*Trend / Trend %/d / Trend R² (per-symbol only): linear regression of log(close) vs days across that symbol's closed-trade prices. Classification gates on R² ≥ 0.30 AND |slope| ≥ 0.25 × medianStepSize — self-tunes to the instrument. Not aggregated portfolio-wide (price series across symbols are not comparable).*`,
28535
+ `*Buyer / Seller Pres (per-symbol only): fraction of up-moves (resp. down-moves) among decisive close-to-close changes for that symbol. Buyer / Seller Str: magnitude share. Pres Imb: buyerStrength − sellerStrength ∈ [−1, +1].*`,
28536
+ `*Median Step (per-symbol only): typical |close[i] − close[i−1]| / close[i−1] across closed trades, in %. Robust to outliers. NOT classical volatility — there are no candles between closes in the report data.*`,
28105
28537
  `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
28106
28538
  ].join("\n");
28107
28539
  }
@@ -28704,6 +29136,36 @@ class WalkerValidationService {
28704
29136
  * Injected logger service instance
28705
29137
  */
28706
29138
  this.loggerService = inject(TYPES.loggerService);
29139
+ /**
29140
+ * @private
29141
+ * @readonly
29142
+ * Injected walker schema service instance
29143
+ */
29144
+ this.walkerSchemaService = inject(TYPES.walkerSchemaService);
29145
+ /**
29146
+ * @private
29147
+ * @readonly
29148
+ * Injected strategy validation service instance
29149
+ */
29150
+ this.strategyValidationService = inject(TYPES.strategyValidationService);
29151
+ /**
29152
+ * @private
29153
+ * @readonly
29154
+ * Injected strategy schema service instance
29155
+ */
29156
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
29157
+ /**
29158
+ * @private
29159
+ * @readonly
29160
+ * Injected risk validation service instance
29161
+ */
29162
+ this.riskValidationService = inject(TYPES.riskValidationService);
29163
+ /**
29164
+ * @private
29165
+ * @readonly
29166
+ * Injected action validation service instance
29167
+ */
29168
+ this.actionValidationService = inject(TYPES.actionValidationService);
28707
29169
  /**
28708
29170
  * @private
28709
29171
  * Map storing walker schemas by walker name
@@ -28725,9 +29187,12 @@ class WalkerValidationService {
28725
29187
  this._walkerMap.set(walkerName, walkerSchema);
28726
29188
  };
28727
29189
  /**
28728
- * Validates the existence of a walker
29190
+ * Validates the existence of a walker and its associated strategy configurations.
29191
+ * Each strategy referenced by the walker is validated via StrategyValidationService,
29192
+ * which in turn validates the strategy's risk profiles and actions.
28729
29193
  * @public
28730
29194
  * @throws {Error} If walkerName is not found
29195
+ * @throws {Error} If any referenced strategy (or its risk/actions) is invalid
28731
29196
  * Memoized function to cache validation results
28732
29197
  */
28733
29198
  this.validate = functoolsKit.memoize(([walkerName]) => walkerName, (walkerName, source) => {
@@ -28739,6 +29204,14 @@ class WalkerValidationService {
28739
29204
  if (!walker) {
28740
29205
  throw new Error(`walker ${walkerName} not found source=${source}`);
28741
29206
  }
29207
+ const walkerSchema = this.walkerSchemaService.get(walkerName);
29208
+ for (const strategyName of walkerSchema.strategies) {
29209
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
29210
+ this.strategyValidationService.validate(strategyName, source);
29211
+ riskName && this.riskValidationService.validate(riskName, source);
29212
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, source));
29213
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, source));
29214
+ }
28742
29215
  return true;
28743
29216
  });
28744
29217
  /**
@@ -30175,6 +30648,10 @@ class PartialGlobalService {
30175
30648
  * Frame validation service for validating frame existence.
30176
30649
  */
30177
30650
  this.frameValidationService = inject(TYPES.frameValidationService);
30651
+ /**
30652
+ * Action validation service for validating action existence.
30653
+ */
30654
+ this.actionValidationService = inject(TYPES.actionValidationService);
30178
30655
  /**
30179
30656
  * Validates strategy and associated risk configuration.
30180
30657
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -30190,9 +30667,10 @@ class PartialGlobalService {
30190
30667
  this.strategyValidationService.validate(context.strategyName, methodName);
30191
30668
  this.exchangeValidationService.validate(context.exchangeName, methodName);
30192
30669
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
30193
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
30670
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
30194
30671
  riskName && this.riskValidationService.validate(riskName, methodName);
30195
30672
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
30673
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
30196
30674
  });
30197
30675
  /**
30198
30676
  * Processes profit state and emits events for newly reached profit levels.
@@ -31259,6 +31737,10 @@ class BreakevenGlobalService {
31259
31737
  * Frame validation service for validating frame existence.
31260
31738
  */
31261
31739
  this.frameValidationService = inject(TYPES.frameValidationService);
31740
+ /**
31741
+ * Action validation service for validating frame existence.
31742
+ */
31743
+ this.actionValidationService = inject(TYPES.actionValidationService);
31262
31744
  /**
31263
31745
  * Validates strategy and associated risk configuration.
31264
31746
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -31274,9 +31756,10 @@ class BreakevenGlobalService {
31274
31756
  this.strategyValidationService.validate(context.strategyName, methodName);
31275
31757
  this.exchangeValidationService.validate(context.exchangeName, methodName);
31276
31758
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
31277
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
31759
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
31278
31760
  riskName && this.riskValidationService.validate(riskName, methodName);
31279
31761
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
31762
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
31280
31763
  });
31281
31764
  /**
31282
31765
  * Checks if breakeven should be triggered and emits event if conditions met.
@@ -50618,6 +51101,7 @@ const GET_TIMESTAMP_METHOD_NAME = "meta.getTimestamp";
50618
51101
  const GET_MODE_METHOD_NAME = "meta.getMode";
50619
51102
  const GET_SYMBOL_METHOD_NAME = "meta.getSymbol";
50620
51103
  const GET_CONTEXT_METHOD_NAME = "meta.getContext";
51104
+ const GET_RUNTIME_INFO_METHOD_NAME = "meta.getRuntimeInfo";
50621
51105
  /**
50622
51106
  * Gets the current date from execution context.
50623
51107
  *
@@ -50734,6 +51218,45 @@ async function getContext() {
50734
51218
  }
50735
51219
  return backtest.methodContextService.context;
50736
51220
  }
51221
+ /**
51222
+ * Gets runtime information about the current execution environment.
51223
+ *
51224
+ * This includes details such as the current symbol, exchange, timeframe, strategy, and whether it's a backtest or live run.
51225
+ *
51226
+ * @returns Promise resolving to an object containing runtime information
51227
+ * @throws Error if method context or execution context is not active
51228
+ *
51229
+ * @example
51230
+ * ```typescript
51231
+ * const runtimeInfo = await getRuntimeInfo();
51232
+ * console.log(runtimeInfo);
51233
+ * // {
51234
+ * // symbol: "BTCUSDT",
51235
+ * // context: {,
51236
+ * // exchangeName: "Binance",
51237
+ * // frameName: "1m",
51238
+ * // strategyName: "MyStrategy",
51239
+ * // },
51240
+ * // backtest: false
51241
+ * // }
51242
+ * ```
51243
+ */
51244
+ async function getRuntimeInfo() {
51245
+ backtest.loggerService.info(GET_RUNTIME_INFO_METHOD_NAME);
51246
+ if (!MethodContextService.hasContext()) {
51247
+ throw new Error("getRuntimeInfo requires a method context");
51248
+ }
51249
+ if (!ExecutionContextService.hasContext()) {
51250
+ throw new Error("getRuntimeInfo requires an execution context");
51251
+ }
51252
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51253
+ const { symbol, backtest: isBacktest } = backtest.executionContextService.context;
51254
+ return await backtest.runtimeMetaService.getRuntimeInfo(symbol, {
51255
+ exchangeName,
51256
+ frameName,
51257
+ strategyName,
51258
+ }, isBacktest);
51259
+ }
50737
51260
 
50738
51261
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ACTIVE_PING = "RecentPersistBacktestUtils.handleActivePing";
50739
51262
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL = "RecentPersistBacktestUtils.getLatestSignal";
@@ -66811,8 +67334,10 @@ exports.getPositionPartials = getPositionPartials;
66811
67334
  exports.getPositionPnlCost = getPositionPnlCost;
66812
67335
  exports.getPositionPnlPercent = getPositionPnlPercent;
66813
67336
  exports.getPositionWaitingMinutes = getPositionWaitingMinutes;
67337
+ exports.getPriceScale = getPriceScale;
66814
67338
  exports.getRawCandles = getRawCandles;
66815
67339
  exports.getRiskSchema = getRiskSchema;
67340
+ exports.getRuntimeInfo = getRuntimeInfo;
66816
67341
  exports.getScheduledSignal = getScheduledSignal;
66817
67342
  exports.getSessionData = getSessionData;
66818
67343
  exports.getSignalState = getSignalState;