backtest-kit 1.7.2 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/build/index.cjs +1537 -489
  2. package/build/index.mjs +1536 -490
  3. package/package.json +2 -1
  4. package/types.d.ts +1033 -388
package/build/index.cjs CHANGED
@@ -772,6 +772,11 @@ class ExchangeConnectionService {
772
772
  /**
773
773
  * Calculates profit/loss for a closed signal with slippage and fees.
774
774
  *
775
+ * For signals with partial closes:
776
+ * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
777
+ * - Each partial close has its own fees and slippage
778
+ * - Total fees = 2 × (number of partial closes + 1 final close) × CC_PERCENT_FEE
779
+ *
775
780
  * Formula breakdown:
776
781
  * 1. Apply slippage to open/close prices (worse execution)
777
782
  * - LONG: buy higher (+slippage), sell lower (-slippage)
@@ -779,27 +784,113 @@ class ExchangeConnectionService {
779
784
  * 2. Calculate raw PNL percentage
780
785
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
781
786
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
782
- * 3. Subtract total fees (0.1% * 2 = 0.2%)
787
+ * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
783
788
  *
784
- * @param signal - Closed signal with position details
785
- * @param priceClose - Actual close price at exit
789
+ * @param signal - Closed signal with position details and optional partial history
790
+ * @param priceClose - Actual close price at final exit
786
791
  * @returns PNL data with percentage and prices
787
792
  *
788
793
  * @example
789
794
  * ```typescript
795
+ * // Signal without partial closes
790
796
  * const pnl = toProfitLossDto(
791
797
  * {
792
798
  * position: "long",
793
- * priceOpen: 50000,
794
- * // ... other signal fields
799
+ * priceOpen: 100,
800
+ * },
801
+ * 110 // close at +10%
802
+ * );
803
+ * console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
804
+ *
805
+ * // Signal with partial closes
806
+ * const pnlPartial = toProfitLossDto(
807
+ * {
808
+ * position: "long",
809
+ * priceOpen: 100,
810
+ * _partial: [
811
+ * { type: "profit", percent: 30, price: 120 }, // +20% on 30%
812
+ * { type: "profit", percent: 40, price: 115 }, // +15% on 40%
813
+ * ],
795
814
  * },
796
- * 51000 // close price
815
+ * 105 // final close at +5% for remaining 30%
797
816
  * );
798
- * console.log(pnl.pnlPercentage); // e.g., 1.8% (after slippage and fees)
817
+ * // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
799
818
  * ```
800
819
  */
801
820
  const toProfitLossDto = (signal, priceClose) => {
802
821
  const priceOpen = signal.priceOpen;
822
+ // Calculate weighted PNL with partial closes
823
+ if (signal._partial && signal._partial.length > 0) {
824
+ let totalWeightedPnl = 0;
825
+ let totalFees = 0;
826
+ // Calculate PNL for each partial close
827
+ for (const partial of signal._partial) {
828
+ const partialPercent = partial.percent;
829
+ const partialPrice = partial.price;
830
+ // Apply slippage to prices
831
+ let priceOpenWithSlippage;
832
+ let priceCloseWithSlippage;
833
+ if (signal.position === "long") {
834
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
835
+ priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
836
+ }
837
+ else {
838
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
839
+ priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
840
+ }
841
+ // Calculate PNL for this partial
842
+ let partialPnl;
843
+ if (signal.position === "long") {
844
+ partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
845
+ }
846
+ else {
847
+ partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
848
+ }
849
+ // Weight by percentage of position closed
850
+ const weightedPnl = (partialPercent / 100) * partialPnl;
851
+ totalWeightedPnl += weightedPnl;
852
+ // Each partial has fees for open + close (2 transactions)
853
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
854
+ }
855
+ // Calculate PNL for remaining position (if any)
856
+ // Compute totalClosed from _partial array
857
+ const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
858
+ const remainingPercent = 100 - totalClosed;
859
+ if (remainingPercent > 0) {
860
+ // Apply slippage
861
+ let priceOpenWithSlippage;
862
+ let priceCloseWithSlippage;
863
+ if (signal.position === "long") {
864
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
865
+ priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
866
+ }
867
+ else {
868
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
869
+ priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
870
+ }
871
+ // Calculate PNL for remaining
872
+ let remainingPnl;
873
+ if (signal.position === "long") {
874
+ remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
875
+ }
876
+ else {
877
+ remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
878
+ }
879
+ // Weight by remaining percentage
880
+ const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
881
+ totalWeightedPnl += weightedRemainingPnl;
882
+ // Final close also has fees
883
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
884
+ }
885
+ // Subtract total fees from weighted PNL
886
+ const pnlPercentage = totalWeightedPnl - totalFees;
887
+ return {
888
+ pnlPercentage,
889
+ priceOpen,
890
+ priceClose,
891
+ };
892
+ }
893
+ // Original logic for signals without partial closes
803
894
  let priceOpenWithSlippage;
804
895
  let priceCloseWithSlippage;
805
896
  if (signal.position === "long") {
@@ -2090,15 +2181,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
2090
2181
  if (self._isStopped) {
2091
2182
  return null;
2092
2183
  }
2093
- if (await functoolsKit.not(self.params.risk.checkSignal({
2094
- pendingSignal: signal,
2095
- symbol: self.params.execution.context.symbol,
2096
- strategyName: self.params.method.context.strategyName,
2097
- exchangeName: self.params.method.context.exchangeName,
2098
- frameName: self.params.method.context.frameName,
2099
- currentPrice,
2100
- timestamp: currentTime,
2101
- }))) {
2184
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest))) {
2102
2185
  return null;
2103
2186
  }
2104
2187
  // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
@@ -2207,10 +2290,9 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
2207
2290
  }
2208
2291
  self._pendingSignal = pendingSignal;
2209
2292
  // Call onActive callback for restored signal
2210
- if (self.params.callbacks?.onActive) {
2211
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2212
- self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
2213
- }
2293
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2294
+ const currentTime = self.params.execution.context.when.getTime();
2295
+ await CALL_ACTIVE_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, self.params.execution.context.backtest);
2214
2296
  }
2215
2297
  // Restore scheduled signal
2216
2298
  const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.execution.context.symbol, self.params.strategyName);
@@ -2223,11 +2305,84 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
2223
2305
  }
2224
2306
  self._scheduledSignal = scheduledSignal;
2225
2307
  // Call onSchedule callback for restored scheduled signal
2226
- if (self.params.callbacks?.onSchedule) {
2227
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2228
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
2229
- }
2308
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2309
+ const currentTime = self.params.execution.context.when.getTime();
2310
+ await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduledSignal, currentPrice, currentTime, self.params.execution.context.backtest);
2311
+ }
2312
+ };
2313
+ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
2314
+ // Initialize partial array if not present
2315
+ if (!signal._partial)
2316
+ signal._partial = [];
2317
+ // Calculate current totals (computed values)
2318
+ const tpClosed = signal._partial
2319
+ .filter((p) => p.type === "profit")
2320
+ .reduce((sum, p) => sum + p.percent, 0);
2321
+ const slClosed = signal._partial
2322
+ .filter((p) => p.type === "loss")
2323
+ .reduce((sum, p) => sum + p.percent, 0);
2324
+ const totalClosed = tpClosed + slClosed;
2325
+ // Check if would exceed 100% total closed
2326
+ const newTotalClosed = totalClosed + percentToClose;
2327
+ if (newTotalClosed > 100) {
2328
+ self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
2329
+ signalId: signal.id,
2330
+ currentTotalClosed: totalClosed,
2331
+ percentToClose,
2332
+ newTotalClosed,
2333
+ });
2334
+ return;
2230
2335
  }
2336
+ // Add new partial close entry
2337
+ signal._partial.push({
2338
+ type: "profit",
2339
+ percent: percentToClose,
2340
+ price: currentPrice,
2341
+ });
2342
+ self.params.logger.info("PARTIAL_PROFIT_FN executed", {
2343
+ signalId: signal.id,
2344
+ percentClosed: percentToClose,
2345
+ totalClosed: newTotalClosed,
2346
+ currentPrice,
2347
+ tpClosed: tpClosed + percentToClose,
2348
+ });
2349
+ };
2350
+ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2351
+ // Initialize partial array if not present
2352
+ if (!signal._partial)
2353
+ signal._partial = [];
2354
+ // Calculate current totals (computed values)
2355
+ const tpClosed = signal._partial
2356
+ .filter((p) => p.type === "profit")
2357
+ .reduce((sum, p) => sum + p.percent, 0);
2358
+ const slClosed = signal._partial
2359
+ .filter((p) => p.type === "loss")
2360
+ .reduce((sum, p) => sum + p.percent, 0);
2361
+ const totalClosed = tpClosed + slClosed;
2362
+ // Check if would exceed 100% total closed
2363
+ const newTotalClosed = totalClosed + percentToClose;
2364
+ if (newTotalClosed > 100) {
2365
+ self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
2366
+ signalId: signal.id,
2367
+ currentTotalClosed: totalClosed,
2368
+ percentToClose,
2369
+ newTotalClosed,
2370
+ });
2371
+ return;
2372
+ }
2373
+ // Add new partial close entry
2374
+ signal._partial.push({
2375
+ type: "loss",
2376
+ percent: percentToClose,
2377
+ price: currentPrice,
2378
+ });
2379
+ self.params.logger.warn("PARTIAL_LOSS_FN executed", {
2380
+ signalId: signal.id,
2381
+ percentClosed: percentToClose,
2382
+ totalClosed: newTotalClosed,
2383
+ currentPrice,
2384
+ slClosed: slClosed + percentToClose,
2385
+ });
2231
2386
  };
2232
2387
  const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
2233
2388
  const currentTime = self.params.execution.context.when.getTime();
@@ -2244,9 +2399,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
2244
2399
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
2245
2400
  });
2246
2401
  await self.setScheduledSignal(null);
2247
- if (self.params.callbacks?.onCancel) {
2248
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
2249
- }
2402
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2250
2403
  const result = {
2251
2404
  action: "cancelled",
2252
2405
  signal: scheduled,
@@ -2259,9 +2412,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
2259
2412
  backtest: self.params.execution.context.backtest,
2260
2413
  reason: "timeout",
2261
2414
  };
2262
- if (self.params.callbacks?.onTick) {
2263
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2264
- }
2415
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2265
2416
  return result;
2266
2417
  };
2267
2418
  const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
@@ -2302,14 +2453,13 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
2302
2453
  priceStopLoss: scheduled.priceStopLoss,
2303
2454
  });
2304
2455
  await self.setScheduledSignal(null);
2305
- if (self.params.callbacks?.onCancel) {
2306
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
2307
- }
2456
+ const currentTime = self.params.execution.context.when.getTime();
2457
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2308
2458
  const result = {
2309
2459
  action: "cancelled",
2310
2460
  signal: scheduled,
2311
2461
  currentPrice: currentPrice,
2312
- closeTimestamp: self.params.execution.context.when.getTime(),
2462
+ closeTimestamp: currentTime,
2313
2463
  strategyName: self.params.method.context.strategyName,
2314
2464
  exchangeName: self.params.method.context.exchangeName,
2315
2465
  frameName: self.params.method.context.frameName,
@@ -2317,9 +2467,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
2317
2467
  backtest: self.params.execution.context.backtest,
2318
2468
  reason: "price_reject",
2319
2469
  };
2320
- if (self.params.callbacks?.onTick) {
2321
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2322
- }
2470
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2323
2471
  return result;
2324
2472
  };
2325
2473
  const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
@@ -2346,15 +2494,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2346
2494
  scheduledAt: scheduled.scheduledAt,
2347
2495
  pendingAt: activationTime,
2348
2496
  });
2349
- if (await functoolsKit.not(self.params.risk.checkSignal({
2350
- symbol: self.params.execution.context.symbol,
2351
- pendingSignal: scheduled,
2352
- strategyName: self.params.method.context.strategyName,
2353
- exchangeName: self.params.method.context.exchangeName,
2354
- frameName: self.params.method.context.frameName,
2355
- currentPrice: scheduled.priceOpen,
2356
- timestamp: activationTime,
2357
- }))) {
2497
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
2358
2498
  self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
2359
2499
  symbol: self.params.execution.context.symbol,
2360
2500
  signalId: scheduled.id,
@@ -2370,15 +2510,8 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2370
2510
  _isScheduled: false,
2371
2511
  };
2372
2512
  await self.setPendingSignal(activatedSignal);
2373
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2374
- strategyName: self.params.method.context.strategyName,
2375
- riskName: self.params.riskName,
2376
- exchangeName: self.params.method.context.exchangeName,
2377
- frameName: self.params.method.context.frameName,
2378
- });
2379
- if (self.params.callbacks?.onOpen) {
2380
- self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
2381
- }
2513
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
2514
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
2382
2515
  const result = {
2383
2516
  action: "opened",
2384
2517
  signal: self._pendingSignal,
@@ -2389,18 +2522,22 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2389
2522
  currentPrice: self._pendingSignal.priceOpen,
2390
2523
  backtest: self.params.execution.context.backtest,
2391
2524
  };
2392
- if (self.params.callbacks?.onTick) {
2393
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2394
- }
2525
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, activationTime, self.params.execution.context.backtest);
2395
2526
  return result;
2396
2527
  };
2397
- const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, timestamp) => {
2398
- // Call system onPing callback first (emits to pingSubject)
2399
- await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, scheduled, self.params.execution.context.backtest, timestamp);
2400
- // Call user onPing callback only if signal is still active (not cancelled, not activated)
2401
- if (self.params.callbacks?.onPing) {
2402
- await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
2403
- }
2528
+ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, scheduled, timestamp, backtest) => {
2529
+ await ExecutionContextService.runInContext(async () => {
2530
+ // Call system onPing callback first (emits to pingSubject)
2531
+ await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, scheduled, self.params.execution.context.backtest, timestamp);
2532
+ // Call user onPing callback only if signal is still active (not cancelled, not activated)
2533
+ if (self.params.callbacks?.onPing) {
2534
+ await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
2535
+ }
2536
+ }, {
2537
+ when: new Date(timestamp),
2538
+ symbol: symbol,
2539
+ backtest: backtest,
2540
+ });
2404
2541
  }, {
2405
2542
  fallback: (error) => {
2406
2543
  const message = "ClientStrategy CALL_PING_CALLBACKS_FN thrown";
@@ -2413,8 +2550,308 @@ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, tim
2413
2550
  errorEmitter.next(error);
2414
2551
  },
2415
2552
  });
2553
+ const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2554
+ await ExecutionContextService.runInContext(async () => {
2555
+ if (self.params.callbacks?.onActive) {
2556
+ self.params.callbacks.onActive(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2557
+ }
2558
+ }, {
2559
+ when: new Date(timestamp),
2560
+ symbol: symbol,
2561
+ backtest: backtest,
2562
+ });
2563
+ }, {
2564
+ fallback: (error) => {
2565
+ const message = "ClientStrategy CALL_ACTIVE_CALLBACKS_FN thrown";
2566
+ const payload = {
2567
+ error: functoolsKit.errorData(error),
2568
+ message: functoolsKit.getErrorMessage(error),
2569
+ };
2570
+ backtest$1.loggerService.warn(message, payload);
2571
+ console.warn(message, payload);
2572
+ errorEmitter.next(error);
2573
+ },
2574
+ });
2575
+ const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2576
+ await ExecutionContextService.runInContext(async () => {
2577
+ if (self.params.callbacks?.onSchedule) {
2578
+ self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2579
+ }
2580
+ }, {
2581
+ when: new Date(timestamp),
2582
+ symbol: symbol,
2583
+ backtest: backtest,
2584
+ });
2585
+ }, {
2586
+ fallback: (error) => {
2587
+ const message = "ClientStrategy CALL_SCHEDULE_CALLBACKS_FN thrown";
2588
+ const payload = {
2589
+ error: functoolsKit.errorData(error),
2590
+ message: functoolsKit.getErrorMessage(error),
2591
+ };
2592
+ backtest$1.loggerService.warn(message, payload);
2593
+ console.warn(message, payload);
2594
+ errorEmitter.next(error);
2595
+ },
2596
+ });
2597
+ const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2598
+ await ExecutionContextService.runInContext(async () => {
2599
+ if (self.params.callbacks?.onCancel) {
2600
+ self.params.callbacks.onCancel(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2601
+ }
2602
+ }, {
2603
+ when: new Date(timestamp),
2604
+ symbol: symbol,
2605
+ backtest: backtest,
2606
+ });
2607
+ }, {
2608
+ fallback: (error) => {
2609
+ const message = "ClientStrategy CALL_CANCEL_CALLBACKS_FN thrown";
2610
+ const payload = {
2611
+ error: functoolsKit.errorData(error),
2612
+ message: functoolsKit.getErrorMessage(error),
2613
+ };
2614
+ backtest$1.loggerService.warn(message, payload);
2615
+ console.warn(message, payload);
2616
+ errorEmitter.next(error);
2617
+ },
2618
+ });
2619
+ const CALL_OPEN_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, priceOpen, timestamp, backtest) => {
2620
+ await ExecutionContextService.runInContext(async () => {
2621
+ if (self.params.callbacks?.onOpen) {
2622
+ self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, priceOpen, self.params.execution.context.backtest);
2623
+ }
2624
+ }, {
2625
+ when: new Date(timestamp),
2626
+ symbol: symbol,
2627
+ backtest: backtest,
2628
+ });
2629
+ }, {
2630
+ fallback: (error) => {
2631
+ const message = "ClientStrategy CALL_OPEN_CALLBACKS_FN thrown";
2632
+ const payload = {
2633
+ error: functoolsKit.errorData(error),
2634
+ message: functoolsKit.getErrorMessage(error),
2635
+ };
2636
+ backtest$1.loggerService.warn(message, payload);
2637
+ console.warn(message, payload);
2638
+ errorEmitter.next(error);
2639
+ },
2640
+ });
2641
+ const CALL_CLOSE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2642
+ await ExecutionContextService.runInContext(async () => {
2643
+ if (self.params.callbacks?.onClose) {
2644
+ self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2645
+ }
2646
+ }, {
2647
+ when: new Date(timestamp),
2648
+ symbol: symbol,
2649
+ backtest: backtest,
2650
+ });
2651
+ }, {
2652
+ fallback: (error) => {
2653
+ const message = "ClientStrategy CALL_CLOSE_CALLBACKS_FN thrown";
2654
+ const payload = {
2655
+ error: functoolsKit.errorData(error),
2656
+ message: functoolsKit.getErrorMessage(error),
2657
+ };
2658
+ backtest$1.loggerService.warn(message, payload);
2659
+ console.warn(message, payload);
2660
+ errorEmitter.next(error);
2661
+ },
2662
+ });
2663
+ const CALL_TICK_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, result, timestamp, backtest) => {
2664
+ await ExecutionContextService.runInContext(async () => {
2665
+ if (self.params.callbacks?.onTick) {
2666
+ self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2667
+ }
2668
+ }, {
2669
+ when: new Date(timestamp),
2670
+ symbol: symbol,
2671
+ backtest: backtest,
2672
+ });
2673
+ }, {
2674
+ fallback: (error) => {
2675
+ const message = "ClientStrategy CALL_TICK_CALLBACKS_FN thrown";
2676
+ const payload = {
2677
+ error: functoolsKit.errorData(error),
2678
+ message: functoolsKit.getErrorMessage(error),
2679
+ };
2680
+ backtest$1.loggerService.warn(message, payload);
2681
+ console.warn(message, payload);
2682
+ errorEmitter.next(error);
2683
+ },
2684
+ });
2685
+ const CALL_IDLE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, currentPrice, timestamp, backtest) => {
2686
+ await ExecutionContextService.runInContext(async () => {
2687
+ if (self.params.callbacks?.onIdle) {
2688
+ self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2689
+ }
2690
+ }, {
2691
+ when: new Date(timestamp),
2692
+ symbol: symbol,
2693
+ backtest: backtest,
2694
+ });
2695
+ }, {
2696
+ fallback: (error) => {
2697
+ const message = "ClientStrategy CALL_IDLE_CALLBACKS_FN thrown";
2698
+ const payload = {
2699
+ error: functoolsKit.errorData(error),
2700
+ message: functoolsKit.getErrorMessage(error),
2701
+ };
2702
+ backtest$1.loggerService.warn(message, payload);
2703
+ console.warn(message, payload);
2704
+ errorEmitter.next(error);
2705
+ },
2706
+ });
2707
+ const CALL_RISK_ADD_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
2708
+ await ExecutionContextService.runInContext(async () => {
2709
+ await self.params.risk.addSignal(symbol, {
2710
+ strategyName: self.params.method.context.strategyName,
2711
+ riskName: self.params.riskName,
2712
+ exchangeName: self.params.method.context.exchangeName,
2713
+ frameName: self.params.method.context.frameName,
2714
+ });
2715
+ }, {
2716
+ when: new Date(timestamp),
2717
+ symbol: symbol,
2718
+ backtest: backtest,
2719
+ });
2720
+ }, {
2721
+ fallback: (error) => {
2722
+ const message = "ClientStrategy CALL_RISK_ADD_SIGNAL_FN thrown";
2723
+ const payload = {
2724
+ error: functoolsKit.errorData(error),
2725
+ message: functoolsKit.getErrorMessage(error),
2726
+ };
2727
+ backtest$1.loggerService.warn(message, payload);
2728
+ console.warn(message, payload);
2729
+ errorEmitter.next(error);
2730
+ },
2731
+ });
2732
+ const CALL_RISK_REMOVE_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
2733
+ await ExecutionContextService.runInContext(async () => {
2734
+ await self.params.risk.removeSignal(symbol, {
2735
+ strategyName: self.params.method.context.strategyName,
2736
+ riskName: self.params.riskName,
2737
+ exchangeName: self.params.method.context.exchangeName,
2738
+ frameName: self.params.method.context.frameName,
2739
+ });
2740
+ }, {
2741
+ when: new Date(timestamp),
2742
+ symbol: symbol,
2743
+ backtest: backtest,
2744
+ });
2745
+ }, {
2746
+ fallback: (error) => {
2747
+ const message = "ClientStrategy CALL_RISK_REMOVE_SIGNAL_FN thrown";
2748
+ const payload = {
2749
+ error: functoolsKit.errorData(error),
2750
+ message: functoolsKit.getErrorMessage(error),
2751
+ };
2752
+ backtest$1.loggerService.warn(message, payload);
2753
+ console.warn(message, payload);
2754
+ errorEmitter.next(error);
2755
+ },
2756
+ });
2757
+ const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2758
+ await ExecutionContextService.runInContext(async () => {
2759
+ await self.params.partial.clear(symbol, signal, currentPrice, backtest);
2760
+ }, {
2761
+ when: new Date(timestamp),
2762
+ symbol: symbol,
2763
+ backtest: backtest,
2764
+ });
2765
+ }, {
2766
+ fallback: (error) => {
2767
+ const message = "ClientStrategy CALL_PARTIAL_CLEAR_FN thrown";
2768
+ const payload = {
2769
+ error: functoolsKit.errorData(error),
2770
+ message: functoolsKit.getErrorMessage(error),
2771
+ };
2772
+ backtest$1.loggerService.warn(message, payload);
2773
+ console.warn(message, payload);
2774
+ errorEmitter.next(error);
2775
+ },
2776
+ });
2777
+ const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
2778
+ return await ExecutionContextService.runInContext(async () => {
2779
+ return await self.params.risk.checkSignal({
2780
+ pendingSignal,
2781
+ symbol: symbol,
2782
+ strategyName: self.params.method.context.strategyName,
2783
+ exchangeName: self.params.method.context.exchangeName,
2784
+ frameName: self.params.method.context.frameName,
2785
+ currentPrice,
2786
+ timestamp,
2787
+ });
2788
+ }, {
2789
+ when: new Date(timestamp),
2790
+ symbol: symbol,
2791
+ backtest: backtest,
2792
+ });
2793
+ }, {
2794
+ defaultValue: false,
2795
+ fallback: (error) => {
2796
+ const message = "ClientStrategy CALL_RISK_CHECK_SIGNAL_FN thrown";
2797
+ const payload = {
2798
+ error: functoolsKit.errorData(error),
2799
+ message: functoolsKit.getErrorMessage(error),
2800
+ };
2801
+ backtest$1.loggerService.warn(message, payload);
2802
+ console.warn(message, payload);
2803
+ errorEmitter.next(error);
2804
+ },
2805
+ });
2806
+ const CALL_PARTIAL_PROFIT_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentTp, timestamp, backtest) => {
2807
+ await ExecutionContextService.runInContext(async () => {
2808
+ await self.params.partial.profit(symbol, signal, currentPrice, percentTp, backtest, new Date(timestamp));
2809
+ if (self.params.callbacks?.onPartialProfit) {
2810
+ self.params.callbacks.onPartialProfit(symbol, signal, currentPrice, percentTp, backtest);
2811
+ }
2812
+ }, {
2813
+ when: new Date(timestamp),
2814
+ symbol: symbol,
2815
+ backtest: backtest,
2816
+ });
2817
+ }, {
2818
+ fallback: (error) => {
2819
+ const message = "ClientStrategy CALL_PARTIAL_PROFIT_CALLBACKS_FN thrown";
2820
+ const payload = {
2821
+ error: functoolsKit.errorData(error),
2822
+ message: functoolsKit.getErrorMessage(error),
2823
+ };
2824
+ backtest$1.loggerService.warn(message, payload);
2825
+ console.warn(message, payload);
2826
+ errorEmitter.next(error);
2827
+ },
2828
+ });
2829
+ const CALL_PARTIAL_LOSS_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentSl, timestamp, backtest) => {
2830
+ await ExecutionContextService.runInContext(async () => {
2831
+ await self.params.partial.loss(symbol, signal, currentPrice, percentSl, backtest, new Date(timestamp));
2832
+ if (self.params.callbacks?.onPartialLoss) {
2833
+ self.params.callbacks.onPartialLoss(symbol, signal, currentPrice, percentSl, backtest);
2834
+ }
2835
+ }, {
2836
+ when: new Date(timestamp),
2837
+ symbol: symbol,
2838
+ backtest: backtest,
2839
+ });
2840
+ }, {
2841
+ fallback: (error) => {
2842
+ const message = "ClientStrategy CALL_PARTIAL_LOSS_CALLBACKS_FN thrown";
2843
+ const payload = {
2844
+ error: functoolsKit.errorData(error),
2845
+ message: functoolsKit.getErrorMessage(error),
2846
+ };
2847
+ backtest$1.loggerService.warn(message, payload);
2848
+ console.warn(message, payload);
2849
+ errorEmitter.next(error);
2850
+ },
2851
+ });
2416
2852
  const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
2417
- await CALL_PING_CALLBACKS_FN(self, scheduled, self.params.execution.context.when.getTime());
2853
+ const currentTime = self.params.execution.context.when.getTime();
2854
+ await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
2418
2855
  const result = {
2419
2856
  action: "active",
2420
2857
  signal: scheduled,
@@ -2427,13 +2864,12 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2427
2864
  percentSl: 0,
2428
2865
  backtest: self.params.execution.context.backtest,
2429
2866
  };
2430
- if (self.params.callbacks?.onTick) {
2431
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2432
- }
2867
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2433
2868
  return result;
2434
2869
  };
2435
2870
  const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2436
2871
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2872
+ const currentTime = self.params.execution.context.when.getTime();
2437
2873
  self.params.logger.info("ClientStrategy scheduled signal created", {
2438
2874
  symbol: self.params.execution.context.symbol,
2439
2875
  signalId: signal.id,
@@ -2441,9 +2877,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2441
2877
  priceOpen: signal.priceOpen,
2442
2878
  currentPrice: currentPrice,
2443
2879
  });
2444
- if (self.params.callbacks?.onSchedule) {
2445
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2446
- }
2880
+ await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2447
2881
  const result = {
2448
2882
  action: "scheduled",
2449
2883
  signal: signal,
@@ -2454,32 +2888,16 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2454
2888
  currentPrice: currentPrice,
2455
2889
  backtest: self.params.execution.context.backtest,
2456
2890
  };
2457
- if (self.params.callbacks?.onTick) {
2458
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2459
- }
2891
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2460
2892
  return result;
2461
2893
  };
2462
2894
  const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2463
- if (await functoolsKit.not(self.params.risk.checkSignal({
2464
- pendingSignal: signal,
2465
- symbol: self.params.execution.context.symbol,
2466
- strategyName: self.params.method.context.strategyName,
2467
- exchangeName: self.params.method.context.exchangeName,
2468
- frameName: self.params.method.context.frameName,
2469
- currentPrice: signal.priceOpen,
2470
- timestamp: self.params.execution.context.when.getTime(),
2471
- }))) {
2895
+ const currentTime = self.params.execution.context.when.getTime();
2896
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest))) {
2472
2897
  return null;
2473
2898
  }
2474
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2475
- strategyName: self.params.method.context.strategyName,
2476
- riskName: self.params.riskName,
2477
- exchangeName: self.params.method.context.exchangeName,
2478
- frameName: self.params.method.context.frameName,
2479
- });
2480
- if (self.params.callbacks?.onOpen) {
2481
- self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
2482
- }
2899
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
2900
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
2483
2901
  const result = {
2484
2902
  action: "opened",
2485
2903
  signal: signal,
@@ -2490,9 +2908,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2490
2908
  currentPrice: signal.priceOpen,
2491
2909
  backtest: self.params.execution.context.backtest,
2492
2910
  };
2493
- if (self.params.callbacks?.onTick) {
2494
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2495
- }
2911
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2496
2912
  return result;
2497
2913
  };
2498
2914
  const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
@@ -2526,6 +2942,7 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
2526
2942
  };
2527
2943
  const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
2528
2944
  const pnl = toProfitLossDto(signal, currentPrice);
2945
+ const currentTime = self.params.execution.context.when.getTime();
2529
2946
  self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
2530
2947
  symbol: self.params.execution.context.symbol,
2531
2948
  signalId: signal.id,
@@ -2533,24 +2950,17 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2533
2950
  priceClose: currentPrice,
2534
2951
  pnlPercentage: pnl.pnlPercentage,
2535
2952
  });
2536
- if (self.params.callbacks?.onClose) {
2537
- self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2538
- }
2953
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2539
2954
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2540
- await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2541
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2542
- strategyName: self.params.method.context.strategyName,
2543
- riskName: self.params.riskName,
2544
- exchangeName: self.params.method.context.exchangeName,
2545
- frameName: self.params.method.context.frameName,
2546
- });
2955
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2956
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
2547
2957
  await self.setPendingSignal(null);
2548
2958
  const result = {
2549
2959
  action: "closed",
2550
2960
  signal: signal,
2551
2961
  currentPrice: currentPrice,
2552
2962
  closeReason: closeReason,
2553
- closeTimestamp: self.params.execution.context.when.getTime(),
2963
+ closeTimestamp: currentTime,
2554
2964
  pnl: pnl,
2555
2965
  strategyName: self.params.method.context.strategyName,
2556
2966
  exchangeName: self.params.method.context.exchangeName,
@@ -2558,14 +2968,13 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2558
2968
  symbol: self.params.execution.context.symbol,
2559
2969
  backtest: self.params.execution.context.backtest,
2560
2970
  };
2561
- if (self.params.callbacks?.onTick) {
2562
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2563
- }
2971
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2564
2972
  return result;
2565
2973
  };
2566
2974
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2567
2975
  let percentTp = 0;
2568
2976
  let percentSl = 0;
2977
+ const currentTime = self.params.execution.context.when.getTime();
2569
2978
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2570
2979
  {
2571
2980
  if (signal.position === "long") {
@@ -2576,20 +2985,14 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2576
2985
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2577
2986
  const progressPercent = (currentDistance / tpDistance) * 100;
2578
2987
  percentTp = Math.min(progressPercent, 100);
2579
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2580
- if (self.params.callbacks?.onPartialProfit) {
2581
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2582
- }
2988
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
2583
2989
  }
2584
2990
  else if (currentDistance < 0) {
2585
2991
  // Moving towards SL
2586
2992
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2587
2993
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2588
2994
  percentSl = Math.min(progressPercent, 100);
2589
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2590
- if (self.params.callbacks?.onPartialLoss) {
2591
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2592
- }
2995
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
2593
2996
  }
2594
2997
  }
2595
2998
  else if (signal.position === "short") {
@@ -2600,20 +3003,14 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2600
3003
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2601
3004
  const progressPercent = (currentDistance / tpDistance) * 100;
2602
3005
  percentTp = Math.min(progressPercent, 100);
2603
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2604
- if (self.params.callbacks?.onPartialProfit) {
2605
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2606
- }
3006
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
2607
3007
  }
2608
3008
  if (currentDistance < 0) {
2609
3009
  // Moving towards SL
2610
3010
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2611
3011
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2612
3012
  percentSl = Math.min(progressPercent, 100);
2613
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2614
- if (self.params.callbacks?.onPartialLoss) {
2615
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2616
- }
3013
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
2617
3014
  }
2618
3015
  }
2619
3016
  }
@@ -2629,15 +3026,12 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2629
3026
  percentSl,
2630
3027
  backtest: self.params.execution.context.backtest,
2631
3028
  };
2632
- if (self.params.callbacks?.onTick) {
2633
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2634
- }
3029
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2635
3030
  return result;
2636
3031
  };
2637
3032
  const RETURN_IDLE_FN = async (self, currentPrice) => {
2638
- if (self.params.callbacks?.onIdle) {
2639
- self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2640
- }
3033
+ const currentTime = self.params.execution.context.when.getTime();
3034
+ await CALL_IDLE_CALLBACKS_FN(self, self.params.execution.context.symbol, currentPrice, currentTime, self.params.execution.context.backtest);
2641
3035
  const result = {
2642
3036
  action: "idle",
2643
3037
  signal: null,
@@ -2648,9 +3042,7 @@ const RETURN_IDLE_FN = async (self, currentPrice) => {
2648
3042
  currentPrice: currentPrice,
2649
3043
  backtest: self.params.execution.context.backtest,
2650
3044
  };
2651
- if (self.params.callbacks?.onTick) {
2652
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2653
- }
3045
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2654
3046
  return result;
2655
3047
  };
2656
3048
  const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp, reason) => {
@@ -2663,9 +3055,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2663
3055
  reason,
2664
3056
  });
2665
3057
  await self.setScheduledSignal(null);
2666
- if (self.params.callbacks?.onCancel) {
2667
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
2668
- }
3058
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
2669
3059
  const result = {
2670
3060
  action: "cancelled",
2671
3061
  signal: scheduled,
@@ -2678,9 +3068,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2678
3068
  backtest: self.params.execution.context.backtest,
2679
3069
  reason,
2680
3070
  };
2681
- if (self.params.callbacks?.onTick) {
2682
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2683
- }
3071
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
2684
3072
  return result;
2685
3073
  };
2686
3074
  const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
@@ -2704,15 +3092,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2704
3092
  scheduledAt: scheduled.scheduledAt,
2705
3093
  pendingAt: activationTime,
2706
3094
  });
2707
- if (await functoolsKit.not(self.params.risk.checkSignal({
2708
- pendingSignal: scheduled,
2709
- symbol: self.params.execution.context.symbol,
2710
- strategyName: self.params.method.context.strategyName,
2711
- exchangeName: self.params.method.context.exchangeName,
2712
- frameName: self.params.method.context.frameName,
2713
- currentPrice: scheduled.priceOpen,
2714
- timestamp: activationTime,
2715
- }))) {
3095
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
2716
3096
  self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
2717
3097
  symbol: self.params.execution.context.symbol,
2718
3098
  signalId: scheduled.id,
@@ -2728,15 +3108,8 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2728
3108
  _isScheduled: false,
2729
3109
  };
2730
3110
  await self.setPendingSignal(activatedSignal);
2731
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2732
- strategyName: self.params.method.context.strategyName,
2733
- riskName: self.params.riskName,
2734
- exchangeName: self.params.method.context.exchangeName,
2735
- frameName: self.params.method.context.frameName,
2736
- });
2737
- if (self.params.callbacks?.onOpen) {
2738
- self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
2739
- }
3111
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
3112
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
2740
3113
  return true;
2741
3114
  };
2742
3115
  const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
@@ -2755,17 +3128,10 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2755
3128
  if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
2756
3129
  self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
2757
3130
  }
2758
- if (self.params.callbacks?.onClose) {
2759
- self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2760
- }
3131
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
2761
3132
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2762
- await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2763
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2764
- strategyName: self.params.method.context.strategyName,
2765
- riskName: self.params.riskName,
2766
- exchangeName: self.params.method.context.exchangeName,
2767
- frameName: self.params.method.context.frameName,
2768
- });
3133
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
3134
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
2769
3135
  await self.setPendingSignal(null);
2770
3136
  const result = {
2771
3137
  action: "closed",
@@ -2780,9 +3146,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2780
3146
  symbol: self.params.execution.context.symbol,
2781
3147
  backtest: self.params.execution.context.backtest,
2782
3148
  };
2783
- if (self.params.callbacks?.onTick) {
2784
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2785
- }
3149
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
2786
3150
  return result;
2787
3151
  };
2788
3152
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
@@ -2857,7 +3221,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
2857
3221
  result: null,
2858
3222
  };
2859
3223
  }
2860
- await CALL_PING_CALLBACKS_FN(self, scheduled, candle.timestamp);
3224
+ await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true);
2861
3225
  }
2862
3226
  return {
2863
3227
  activated: false,
@@ -2938,19 +3302,13 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2938
3302
  // Moving towards TP
2939
3303
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2940
3304
  const progressPercent = (currentDistance / tpDistance) * 100;
2941
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2942
- if (self.params.callbacks?.onPartialProfit) {
2943
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2944
- }
3305
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2945
3306
  }
2946
3307
  else if (currentDistance < 0) {
2947
3308
  // Moving towards SL
2948
3309
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2949
3310
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2950
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2951
- if (self.params.callbacks?.onPartialLoss) {
2952
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2953
- }
3311
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2954
3312
  }
2955
3313
  }
2956
3314
  else if (signal.position === "short") {
@@ -2960,19 +3318,13 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2960
3318
  // Moving towards TP
2961
3319
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2962
3320
  const progressPercent = (currentDistance / tpDistance) * 100;
2963
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2964
- if (self.params.callbacks?.onPartialProfit) {
2965
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2966
- }
3321
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2967
3322
  }
2968
3323
  if (currentDistance < 0) {
2969
3324
  // Moving towards SL
2970
3325
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2971
3326
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2972
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2973
- if (self.params.callbacks?.onPartialLoss) {
2974
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2975
- }
3327
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2976
3328
  }
2977
3329
  }
2978
3330
  }
@@ -3158,9 +3510,7 @@ class ClientStrategy {
3158
3510
  signalId: cancelledSignal.id,
3159
3511
  });
3160
3512
  // Call onCancel callback
3161
- if (this.params.callbacks?.onCancel) {
3162
- this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
3163
- }
3513
+ await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
3164
3514
  const result = {
3165
3515
  action: "cancelled",
3166
3516
  signal: cancelledSignal,
@@ -3272,14 +3622,13 @@ class ClientStrategy {
3272
3622
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
3273
3623
  const cancelledSignal = this._cancelledSignal;
3274
3624
  this._cancelledSignal = null; // Clear after using
3275
- if (this.params.callbacks?.onCancel) {
3276
- this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
3277
- }
3625
+ const closeTimestamp = this.params.execution.context.when.getTime();
3626
+ await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
3278
3627
  const cancelledResult = {
3279
3628
  action: "cancelled",
3280
3629
  signal: cancelledSignal,
3281
3630
  currentPrice,
3282
- closeTimestamp: this.params.execution.context.when.getTime(),
3631
+ closeTimestamp: closeTimestamp,
3283
3632
  strategyName: this.params.method.context.strategyName,
3284
3633
  exchangeName: this.params.method.context.exchangeName,
3285
3634
  frameName: this.params.method.context.frameName,
@@ -3458,6 +3807,190 @@ class ClientStrategy {
3458
3807
  }
3459
3808
  await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName);
3460
3809
  }
3810
+ /**
3811
+ * Executes partial close at profit level (moving toward TP).
3812
+ *
3813
+ * Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
3814
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
3815
+ *
3816
+ * Behavior:
3817
+ * - Adds entry to signal's `_partial` array with type "profit"
3818
+ * - Validates percentToClose is in range (0, 100]
3819
+ * - Silently skips if total closed would exceed 100%
3820
+ * - Persists updated signal state (backtest and live modes)
3821
+ * - Calls onWrite callback for persistence testing
3822
+ *
3823
+ * Validation:
3824
+ * - Throws if no pending signal exists
3825
+ * - Throws if percentToClose is not a finite number
3826
+ * - Throws if percentToClose <= 0 or > 100
3827
+ * - Throws if currentPrice is not a positive finite number
3828
+ * - Throws if currentPrice is not moving toward TP:
3829
+ * - LONG: currentPrice must be > priceOpen
3830
+ * - SHORT: currentPrice must be < priceOpen
3831
+ *
3832
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3833
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
3834
+ * @param currentPrice - Current market price for this partial close (must be in profit direction)
3835
+ * @param backtest - Whether running in backtest mode (controls persistence)
3836
+ * @returns Promise that resolves when state is updated and persisted
3837
+ *
3838
+ * @example
3839
+ * ```typescript
3840
+ * // Close 30% of position at profit (moving toward TP)
3841
+ * await strategy.partialProfit("BTCUSDT", 30, 45000, false);
3842
+ *
3843
+ * // Later close another 20%
3844
+ * await strategy.partialProfit("BTCUSDT", 20, 46000, false);
3845
+ *
3846
+ * // Final close will calculate weighted PNL from all partials
3847
+ * ```
3848
+ */
3849
+ async partialProfit(symbol, percentToClose, currentPrice, backtest) {
3850
+ this.params.logger.debug("ClientStrategy partialProfit", {
3851
+ symbol,
3852
+ percentToClose,
3853
+ currentPrice,
3854
+ hasPendingSignal: this._pendingSignal !== null,
3855
+ });
3856
+ // Validation: must have pending signal
3857
+ if (!this._pendingSignal) {
3858
+ throw new Error(`ClientStrategy partialProfit: No pending signal exists for symbol=${symbol}`);
3859
+ }
3860
+ // Validation: percentToClose must be valid
3861
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
3862
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
3863
+ }
3864
+ if (percentToClose <= 0) {
3865
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be > 0, got ${percentToClose}`);
3866
+ }
3867
+ if (percentToClose > 100) {
3868
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be <= 100, got ${percentToClose}`);
3869
+ }
3870
+ // Validation: currentPrice must be valid
3871
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
3872
+ throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
3873
+ }
3874
+ // Validation: currentPrice must be moving toward TP (profit direction)
3875
+ if (this._pendingSignal.position === "long") {
3876
+ // For LONG: currentPrice must be higher than priceOpen (moving toward TP)
3877
+ if (currentPrice <= this._pendingSignal.priceOpen) {
3878
+ throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
3879
+ }
3880
+ }
3881
+ else {
3882
+ // For SHORT: currentPrice must be lower than priceOpen (moving toward TP)
3883
+ if (currentPrice >= this._pendingSignal.priceOpen) {
3884
+ throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
3885
+ }
3886
+ }
3887
+ // Execute partial close logic
3888
+ PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
3889
+ // Persist updated signal state (inline setPendingSignal content)
3890
+ // Note: this._pendingSignal already mutated by PARTIAL_PROFIT_FN, no reassignment needed
3891
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
3892
+ pendingSignal: this._pendingSignal,
3893
+ });
3894
+ // Call onWrite callback for testing persist storage
3895
+ if (this.params.callbacks?.onWrite) {
3896
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
3897
+ }
3898
+ if (!backtest) {
3899
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
3900
+ }
3901
+ }
3902
+ /**
3903
+ * Executes partial close at loss level (moving toward SL).
3904
+ *
3905
+ * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
3906
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
3907
+ *
3908
+ * Behavior:
3909
+ * - Adds entry to signal's `_partial` array with type "loss"
3910
+ * - Validates percentToClose is in range (0, 100]
3911
+ * - Silently skips if total closed would exceed 100%
3912
+ * - Persists updated signal state (backtest and live modes)
3913
+ * - Calls onWrite callback for persistence testing
3914
+ *
3915
+ * Validation:
3916
+ * - Throws if no pending signal exists
3917
+ * - Throws if percentToClose is not a finite number
3918
+ * - Throws if percentToClose <= 0 or > 100
3919
+ * - Throws if currentPrice is not a positive finite number
3920
+ * - Throws if currentPrice is not moving toward SL:
3921
+ * - LONG: currentPrice must be < priceOpen
3922
+ * - SHORT: currentPrice must be > priceOpen
3923
+ *
3924
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3925
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
3926
+ * @param currentPrice - Current market price for this partial close (must be in loss direction)
3927
+ * @param backtest - Whether running in backtest mode (controls persistence)
3928
+ * @returns Promise that resolves when state is updated and persisted
3929
+ *
3930
+ * @example
3931
+ * ```typescript
3932
+ * // Close 40% of position at loss (moving toward SL)
3933
+ * await strategy.partialLoss("BTCUSDT", 40, 38000, false);
3934
+ *
3935
+ * // Later close another 30%
3936
+ * await strategy.partialLoss("BTCUSDT", 30, 37000, false);
3937
+ *
3938
+ * // Final close will calculate weighted PNL from all partials
3939
+ * ```
3940
+ */
3941
+ async partialLoss(symbol, percentToClose, currentPrice, backtest) {
3942
+ this.params.logger.debug("ClientStrategy partialLoss", {
3943
+ symbol,
3944
+ percentToClose,
3945
+ currentPrice,
3946
+ hasPendingSignal: this._pendingSignal !== null,
3947
+ });
3948
+ // Validation: must have pending signal
3949
+ if (!this._pendingSignal) {
3950
+ throw new Error(`ClientStrategy partialLoss: No pending signal exists for symbol=${symbol}`);
3951
+ }
3952
+ // Validation: percentToClose must be valid
3953
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
3954
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
3955
+ }
3956
+ if (percentToClose <= 0) {
3957
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be > 0, got ${percentToClose}`);
3958
+ }
3959
+ if (percentToClose > 100) {
3960
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be <= 100, got ${percentToClose}`);
3961
+ }
3962
+ // Validation: currentPrice must be valid
3963
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
3964
+ throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
3965
+ }
3966
+ // Validation: currentPrice must be moving toward SL (loss direction)
3967
+ if (this._pendingSignal.position === "long") {
3968
+ // For LONG: currentPrice must be lower than priceOpen (moving toward SL)
3969
+ if (currentPrice >= this._pendingSignal.priceOpen) {
3970
+ throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
3971
+ }
3972
+ }
3973
+ else {
3974
+ // For SHORT: currentPrice must be higher than priceOpen (moving toward SL)
3975
+ if (currentPrice <= this._pendingSignal.priceOpen) {
3976
+ throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
3977
+ }
3978
+ }
3979
+ // Execute partial close logic
3980
+ PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
3981
+ // Persist updated signal state (inline setPendingSignal content)
3982
+ // Note: this._pendingSignal already mutated by PARTIAL_LOSS_FN, no reassignment needed
3983
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
3984
+ pendingSignal: this._pendingSignal,
3985
+ });
3986
+ // Call onWrite callback for testing persist storage
3987
+ if (this.params.callbacks?.onWrite) {
3988
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
3989
+ }
3990
+ if (!backtest) {
3991
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
3992
+ }
3993
+ }
3461
3994
  }
3462
3995
 
3463
3996
  const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
@@ -3859,6 +4392,7 @@ class StrategyConnectionService {
3859
4392
  constructor() {
3860
4393
  this.loggerService = inject(TYPES.loggerService);
3861
4394
  this.executionContextService = inject(TYPES.executionContextService);
4395
+ this.methodContextService = inject(TYPES.methodContextService);
3862
4396
  this.strategySchemaService = inject(TYPES.strategySchemaService);
3863
4397
  this.riskConnectionService = inject(TYPES.riskConnectionService);
3864
4398
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
@@ -3882,7 +4416,7 @@ class StrategyConnectionService {
3882
4416
  symbol,
3883
4417
  interval,
3884
4418
  execution: this.executionContextService,
3885
- method: { context: { strategyName, exchangeName, frameName } },
4419
+ method: this.methodContextService,
3886
4420
  logger: this.loggerService,
3887
4421
  partial: this.partialConnectionService,
3888
4422
  exchange: this.exchangeConnectionService,
@@ -4061,28 +4595,104 @@ class StrategyConnectionService {
4061
4595
  }
4062
4596
  };
4063
4597
  /**
4064
- * Cancels the scheduled signal for the specified strategy.
4598
+ * Cancels the scheduled signal for the specified strategy.
4599
+ *
4600
+ * Delegates to ClientStrategy.cancel() which clears the scheduled signal
4601
+ * without stopping the strategy or affecting pending signals.
4602
+ *
4603
+ * Note: Cancelled event will be emitted on next tick() call when strategy
4604
+ * detects the scheduled signal was cancelled.
4605
+ *
4606
+ * @param backtest - Whether running in backtest mode
4607
+ * @param symbol - Trading pair symbol
4608
+ * @param ctx - Context with strategyName, exchangeName, frameName
4609
+ * @param cancelId - Optional cancellation ID for user-initiated cancellations
4610
+ * @returns Promise that resolves when scheduled signal is cancelled
4611
+ */
4612
+ this.cancel = async (backtest, symbol, context, cancelId) => {
4613
+ this.loggerService.log("strategyConnectionService cancel", {
4614
+ symbol,
4615
+ context,
4616
+ cancelId,
4617
+ });
4618
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4619
+ await strategy.cancel(symbol, backtest, cancelId);
4620
+ };
4621
+ /**
4622
+ * Executes partial close at profit level (moving toward TP).
4623
+ *
4624
+ * Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
4625
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4626
+ *
4627
+ * Delegates to ClientStrategy.partialProfit() with current execution context.
4628
+ *
4629
+ * @param backtest - Whether running in backtest mode
4630
+ * @param symbol - Trading pair symbol
4631
+ * @param context - Execution context with strategyName, exchangeName, frameName
4632
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4633
+ * @param currentPrice - Current market price for this partial close
4634
+ * @returns Promise that resolves when state is updated and persisted
4635
+ *
4636
+ * @example
4637
+ * ```typescript
4638
+ * // Close 30% of position at profit
4639
+ * await strategyConnectionService.partialProfit(
4640
+ * false,
4641
+ * "BTCUSDT",
4642
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4643
+ * 30,
4644
+ * 45000
4645
+ * );
4646
+ * ```
4647
+ */
4648
+ this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
4649
+ this.loggerService.log("strategyConnectionService partialProfit", {
4650
+ symbol,
4651
+ context,
4652
+ percentToClose,
4653
+ currentPrice,
4654
+ backtest,
4655
+ });
4656
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4657
+ await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
4658
+ };
4659
+ /**
4660
+ * Executes partial close at loss level (moving toward SL).
4065
4661
  *
4066
- * Delegates to ClientStrategy.cancel() which clears the scheduled signal
4067
- * without stopping the strategy or affecting pending signals.
4662
+ * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
4663
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4068
4664
  *
4069
- * Note: Cancelled event will be emitted on next tick() call when strategy
4070
- * detects the scheduled signal was cancelled.
4665
+ * Delegates to ClientStrategy.partialLoss() with current execution context.
4071
4666
  *
4072
4667
  * @param backtest - Whether running in backtest mode
4073
4668
  * @param symbol - Trading pair symbol
4074
- * @param ctx - Context with strategyName, exchangeName, frameName
4075
- * @param cancelId - Optional cancellation ID for user-initiated cancellations
4076
- * @returns Promise that resolves when scheduled signal is cancelled
4669
+ * @param context - Execution context with strategyName, exchangeName, frameName
4670
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4671
+ * @param currentPrice - Current market price for this partial close
4672
+ * @returns Promise that resolves when state is updated and persisted
4673
+ *
4674
+ * @example
4675
+ * ```typescript
4676
+ * // Close 40% of position at loss
4677
+ * await strategyConnectionService.partialLoss(
4678
+ * false,
4679
+ * "BTCUSDT",
4680
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4681
+ * 40,
4682
+ * 38000
4683
+ * );
4684
+ * ```
4077
4685
  */
4078
- this.cancel = async (backtest, symbol, context, cancelId) => {
4079
- this.loggerService.log("strategyConnectionService cancel", {
4686
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
4687
+ this.loggerService.log("strategyConnectionService partialLoss", {
4080
4688
  symbol,
4081
4689
  context,
4082
- cancelId,
4690
+ percentToClose,
4691
+ currentPrice,
4692
+ backtest,
4083
4693
  });
4084
4694
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4085
- await strategy.cancel(symbol, backtest, cancelId);
4695
+ await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
4086
4696
  };
4087
4697
  }
4088
4698
  }
@@ -5210,6 +5820,82 @@ class StrategyCoreService {
5210
5820
  }
5211
5821
  return await this.strategyConnectionService.clear(payload);
5212
5822
  };
5823
+ /**
5824
+ * Executes partial close at profit level (moving toward TP).
5825
+ *
5826
+ * Validates strategy existence and delegates to connection service
5827
+ * to close a percentage of the pending position at profit.
5828
+ *
5829
+ * Does not require execution context as this is a direct state mutation.
5830
+ *
5831
+ * @param backtest - Whether running in backtest mode
5832
+ * @param symbol - Trading pair symbol
5833
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
5834
+ * @param currentPrice - Current market price for this partial close (must be in profit direction)
5835
+ * @param context - Execution context with strategyName, exchangeName, frameName
5836
+ * @returns Promise that resolves when state is updated and persisted
5837
+ *
5838
+ * @example
5839
+ * ```typescript
5840
+ * // Close 30% of position at profit
5841
+ * await strategyCoreService.partialProfit(
5842
+ * false,
5843
+ * "BTCUSDT",
5844
+ * 30,
5845
+ * 45000,
5846
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
5847
+ * );
5848
+ * ```
5849
+ */
5850
+ this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
5851
+ this.loggerService.log("strategyCoreService partialProfit", {
5852
+ symbol,
5853
+ percentToClose,
5854
+ currentPrice,
5855
+ context,
5856
+ backtest,
5857
+ });
5858
+ await this.validate(symbol, context);
5859
+ return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
5860
+ };
5861
+ /**
5862
+ * Executes partial close at loss level (moving toward SL).
5863
+ *
5864
+ * Validates strategy existence and delegates to connection service
5865
+ * to close a percentage of the pending position at loss.
5866
+ *
5867
+ * Does not require execution context as this is a direct state mutation.
5868
+ *
5869
+ * @param backtest - Whether running in backtest mode
5870
+ * @param symbol - Trading pair symbol
5871
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
5872
+ * @param currentPrice - Current market price for this partial close (must be in loss direction)
5873
+ * @param context - Execution context with strategyName, exchangeName, frameName
5874
+ * @returns Promise that resolves when state is updated and persisted
5875
+ *
5876
+ * @example
5877
+ * ```typescript
5878
+ * // Close 40% of position at loss
5879
+ * await strategyCoreService.partialLoss(
5880
+ * false,
5881
+ * "BTCUSDT",
5882
+ * 40,
5883
+ * 38000,
5884
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
5885
+ * );
5886
+ * ```
5887
+ */
5888
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
5889
+ this.loggerService.log("strategyCoreService partialLoss", {
5890
+ symbol,
5891
+ percentToClose,
5892
+ currentPrice,
5893
+ context,
5894
+ backtest,
5895
+ });
5896
+ await this.validate(symbol, context);
5897
+ return await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
5898
+ };
5213
5899
  }
5214
5900
  }
5215
5901
 
@@ -6932,6 +7618,21 @@ const backtest_columns = [
6932
7618
  },
6933
7619
  isVisible: () => true,
6934
7620
  },
7621
+ {
7622
+ key: "partialCloses",
7623
+ label: "Partial Closes",
7624
+ format: (data) => {
7625
+ const partial = data.signal._partial;
7626
+ if (!partial || partial.length === 0)
7627
+ return "N/A";
7628
+ const profitCount = partial.filter(p => p.type === "profit").length;
7629
+ const lossCount = partial.filter(p => p.type === "loss").length;
7630
+ const profitPercent = partial.filter(p => p.type === "profit").reduce((sum, p) => sum + p.percent, 0);
7631
+ const lossPercent = partial.filter(p => p.type === "loss").reduce((sum, p) => sum + p.percent, 0);
7632
+ return `${partial.length} (↑${profitCount}: ${profitPercent.toFixed(1)}%, ↓${lossCount}: ${lossPercent.toFixed(1)}%)`;
7633
+ },
7634
+ isVisible: () => true,
7635
+ },
6935
7636
  {
6936
7637
  key: "closeReason",
6937
7638
  label: "Close Reason",
@@ -7007,49 +7708,49 @@ const heat_columns = [
7007
7708
  {
7008
7709
  key: "totalPnl",
7009
7710
  label: "Total PNL",
7010
- format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%+.2f%%") : "N/A",
7711
+ format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
7011
7712
  isVisible: () => true,
7012
7713
  },
7013
7714
  {
7014
7715
  key: "sharpeRatio",
7015
7716
  label: "Sharpe",
7016
- format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio, "%.2f") : "N/A",
7717
+ format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
7017
7718
  isVisible: () => true,
7018
7719
  },
7019
7720
  {
7020
7721
  key: "profitFactor",
7021
7722
  label: "PF",
7022
- format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor, "%.2f") : "N/A",
7723
+ format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
7023
7724
  isVisible: () => true,
7024
7725
  },
7025
7726
  {
7026
7727
  key: "expectancy",
7027
7728
  label: "Expect",
7028
- format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%+.2f%%") : "N/A",
7729
+ format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
7029
7730
  isVisible: () => true,
7030
7731
  },
7031
7732
  {
7032
7733
  key: "winRate",
7033
7734
  label: "WR",
7034
- format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%.1f%%") : "N/A",
7735
+ format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
7035
7736
  isVisible: () => true,
7036
7737
  },
7037
7738
  {
7038
7739
  key: "avgWin",
7039
7740
  label: "Avg Win",
7040
- format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%+.2f%%") : "N/A",
7741
+ format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
7041
7742
  isVisible: () => true,
7042
7743
  },
7043
7744
  {
7044
7745
  key: "avgLoss",
7045
7746
  label: "Avg Loss",
7046
- format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%+.2f%%") : "N/A",
7747
+ format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
7047
7748
  isVisible: () => true,
7048
7749
  },
7049
7750
  {
7050
7751
  key: "maxDrawdown",
7051
7752
  label: "Max DD",
7052
- format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%.2f%%") : "N/A",
7753
+ format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
7053
7754
  isVisible: () => true,
7054
7755
  },
7055
7756
  {
@@ -10324,7 +11025,7 @@ class HeatmapStorage {
10324
11025
  return [
10325
11026
  `# Portfolio Heatmap: ${strategyName}`,
10326
11027
  "",
10327
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
11028
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
10328
11029
  "",
10329
11030
  table
10330
11031
  ].join("\n");
@@ -11428,12 +12129,16 @@ class OptimizerTemplateService {
11428
12129
  ``,
11429
12130
  `listenWalkerComplete((results) => {`,
11430
12131
  ` console.log("Walker completed:", results.bestStrategy);`,
11431
- ` Walker.dump("${escapedSymbol}", results.walkerName);`,
12132
+ ` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
11432
12133
  `});`,
11433
12134
  ``,
11434
12135
  `listenDoneBacktest((event) => {`,
11435
12136
  ` console.log("Backtest completed:", event.symbol);`,
11436
- ` Backtest.dump(event.symbol, event.strategyName);`,
12137
+ ` Backtest.dump(event.symbol, {`,
12138
+ ` strategyName: event.strategyName,`,
12139
+ ` exchangeName: event.exchangeName,`,
12140
+ ` frameName: event.frameName`,
12141
+ ` });`,
11437
12142
  `});`,
11438
12143
  ``,
11439
12144
  `listenError((error) => {`,
@@ -11467,12 +12172,10 @@ class OptimizerTemplateService {
11467
12172
  ` }`,
11468
12173
  ``,
11469
12174
  ` {`,
11470
- ` let summary = "# Outline Result Summary\\n";`,
12175
+ ` let summary = "# Outline Result Summary\\n\\n";`,
11471
12176
  ``,
11472
12177
  ` {`,
11473
- ` summary += "\\n";`,
11474
- ` summary += \`**ResultId**: \${resultId}\\n\`;`,
11475
- ` summary += "\\n";`,
12178
+ ` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
11476
12179
  ` }`,
11477
12180
  ``,
11478
12181
  ` if (result) {`,
@@ -11488,7 +12191,7 @@ class OptimizerTemplateService {
11488
12191
  ` systemMessages.forEach((msg, idx) => {`,
11489
12192
  ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
11490
12193
  ` summary += msg.content;`,
11491
- ` summary += "\\n";`,
12194
+ ` summary += "\\n\\n";`,
11492
12195
  ` });`,
11493
12196
  ` }`,
11494
12197
  ``,
@@ -14519,13 +15222,203 @@ const validateInternal = async (args) => {
14519
15222
  * });
14520
15223
  * ```
14521
15224
  */
14522
- async function validate(args = {}) {
14523
- backtest$1.loggerService.log(METHOD_NAME);
14524
- return await validateInternal(args);
15225
+ async function validate(args = {}) {
15226
+ backtest$1.loggerService.log(METHOD_NAME);
15227
+ return await validateInternal(args);
15228
+ }
15229
+
15230
+ const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
15231
+ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
15232
+ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
15233
+ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
15234
+ const GET_DATE_METHOD_NAME = "exchange.getDate";
15235
+ const GET_MODE_METHOD_NAME = "exchange.getMode";
15236
+ const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
15237
+ /**
15238
+ * Checks if trade context is active (execution and method contexts).
15239
+ *
15240
+ * Returns true when both contexts are active, which is required for calling
15241
+ * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
15242
+ * getDate, and getMode.
15243
+ *
15244
+ * @returns true if trade context is active, false otherwise
15245
+ *
15246
+ * @example
15247
+ * ```typescript
15248
+ * import { hasTradeContext, getCandles } from "backtest-kit";
15249
+ *
15250
+ * if (hasTradeContext()) {
15251
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
15252
+ * } else {
15253
+ * console.log("Trade context not active");
15254
+ * }
15255
+ * ```
15256
+ */
15257
+ function hasTradeContext() {
15258
+ backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
15259
+ return ExecutionContextService.hasContext() && MethodContextService.hasContext();
15260
+ }
15261
+ /**
15262
+ * Fetches historical candle data from the registered exchange.
15263
+ *
15264
+ * Candles are fetched backwards from the current execution context time.
15265
+ * Uses the exchange's getCandles implementation.
15266
+ *
15267
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15268
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
15269
+ * @param limit - Number of candles to fetch
15270
+ * @returns Promise resolving to array of candle data
15271
+ *
15272
+ * @example
15273
+ * ```typescript
15274
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
15275
+ * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
15276
+ * ```
15277
+ */
15278
+ async function getCandles(symbol, interval, limit) {
15279
+ backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
15280
+ symbol,
15281
+ interval,
15282
+ limit,
15283
+ });
15284
+ if (!ExecutionContextService.hasContext()) {
15285
+ throw new Error("getCandles requires an execution context");
15286
+ }
15287
+ if (!MethodContextService.hasContext()) {
15288
+ throw new Error("getCandles requires a method context");
15289
+ }
15290
+ return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
15291
+ }
15292
+ /**
15293
+ * Calculates VWAP (Volume Weighted Average Price) for a symbol.
15294
+ *
15295
+ * Uses the last 5 1-minute candles to calculate:
15296
+ * - Typical Price = (high + low + close) / 3
15297
+ * - VWAP = sum(typical_price * volume) / sum(volume)
15298
+ *
15299
+ * If volume is zero, returns simple average of close prices.
15300
+ *
15301
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15302
+ * @returns Promise resolving to VWAP price
15303
+ *
15304
+ * @example
15305
+ * ```typescript
15306
+ * const vwap = await getAveragePrice("BTCUSDT");
15307
+ * console.log(vwap); // 50125.43
15308
+ * ```
15309
+ */
15310
+ async function getAveragePrice(symbol) {
15311
+ backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
15312
+ symbol,
15313
+ });
15314
+ if (!ExecutionContextService.hasContext()) {
15315
+ throw new Error("getAveragePrice requires an execution context");
15316
+ }
15317
+ if (!MethodContextService.hasContext()) {
15318
+ throw new Error("getAveragePrice requires a method context");
15319
+ }
15320
+ return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
15321
+ }
15322
+ /**
15323
+ * Formats a price value according to exchange rules.
15324
+ *
15325
+ * Uses the exchange's formatPrice implementation for proper decimal places.
15326
+ *
15327
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15328
+ * @param price - Raw price value
15329
+ * @returns Promise resolving to formatted price string
15330
+ *
15331
+ * @example
15332
+ * ```typescript
15333
+ * const formatted = await formatPrice("BTCUSDT", 50000.123456);
15334
+ * console.log(formatted); // "50000.12"
15335
+ * ```
15336
+ */
15337
+ async function formatPrice(symbol, price) {
15338
+ backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
15339
+ symbol,
15340
+ price,
15341
+ });
15342
+ if (!MethodContextService.hasContext()) {
15343
+ throw new Error("formatPrice requires a method context");
15344
+ }
15345
+ return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
15346
+ }
15347
+ /**
15348
+ * Formats a quantity value according to exchange rules.
15349
+ *
15350
+ * Uses the exchange's formatQuantity implementation for proper decimal places.
15351
+ *
15352
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15353
+ * @param quantity - Raw quantity value
15354
+ * @returns Promise resolving to formatted quantity string
15355
+ *
15356
+ * @example
15357
+ * ```typescript
15358
+ * const formatted = await formatQuantity("BTCUSDT", 0.123456789);
15359
+ * console.log(formatted); // "0.12345678"
15360
+ * ```
15361
+ */
15362
+ async function formatQuantity(symbol, quantity) {
15363
+ backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
15364
+ symbol,
15365
+ quantity,
15366
+ });
15367
+ if (!MethodContextService.hasContext()) {
15368
+ throw new Error("formatQuantity requires a method context");
15369
+ }
15370
+ return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
15371
+ }
15372
+ /**
15373
+ * Gets the current date from execution context.
15374
+ *
15375
+ * In backtest mode: returns the current timeframe date being processed
15376
+ * In live mode: returns current real-time date
15377
+ *
15378
+ * @returns Promise resolving to current execution context date
15379
+ *
15380
+ * @example
15381
+ * ```typescript
15382
+ * const date = await getDate();
15383
+ * console.log(date); // 2024-01-01T12:00:00.000Z
15384
+ * ```
15385
+ */
15386
+ async function getDate() {
15387
+ backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
15388
+ if (!ExecutionContextService.hasContext()) {
15389
+ throw new Error("getDate requires an execution context");
15390
+ }
15391
+ const { when } = backtest$1.executionContextService.context;
15392
+ return new Date(when.getTime());
15393
+ }
15394
+ /**
15395
+ * Gets the current execution mode.
15396
+ *
15397
+ * @returns Promise resolving to "backtest" or "live"
15398
+ *
15399
+ * @example
15400
+ * ```typescript
15401
+ * const mode = await getMode();
15402
+ * if (mode === "backtest") {
15403
+ * console.log("Running in backtest mode");
15404
+ * } else {
15405
+ * console.log("Running in live mode");
15406
+ * }
15407
+ * ```
15408
+ */
15409
+ async function getMode() {
15410
+ backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
15411
+ if (!ExecutionContextService.hasContext()) {
15412
+ throw new Error("getMode requires an execution context");
15413
+ }
15414
+ const { backtest: bt } = backtest$1.executionContextService.context;
15415
+ return bt ? "backtest" : "live";
14525
15416
  }
14526
15417
 
14527
15418
  const STOP_METHOD_NAME = "strategy.stop";
14528
15419
  const CANCEL_METHOD_NAME = "strategy.cancel";
15420
+ const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
15421
+ const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
14529
15422
  /**
14530
15423
  * Stops the strategy from generating new signals.
14531
15424
  *
@@ -14602,6 +15495,86 @@ async function cancel(symbol, cancelId) {
14602
15495
  const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
14603
15496
  await backtest$1.strategyCoreService.cancel(isBacktest, symbol, { exchangeName, frameName, strategyName }, cancelId);
14604
15497
  }
15498
+ /**
15499
+ * Executes partial close at profit level (moving toward TP).
15500
+ *
15501
+ * Closes a percentage of the active pending position at profit.
15502
+ * Price must be moving toward take profit (in profit direction).
15503
+ *
15504
+ * Automatically detects backtest/live mode from execution context.
15505
+ *
15506
+ * @param symbol - Trading pair symbol
15507
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
15508
+ * @returns Promise that resolves when state is updated
15509
+ *
15510
+ * @throws Error if currentPrice is not in profit direction:
15511
+ * - LONG: currentPrice must be > priceOpen
15512
+ * - SHORT: currentPrice must be < priceOpen
15513
+ *
15514
+ * @example
15515
+ * ```typescript
15516
+ * import { partialProfit } from "backtest-kit";
15517
+ *
15518
+ * // Close 30% of LONG position at profit
15519
+ * await partialProfit("BTCUSDT", 30, 45000);
15520
+ * ```
15521
+ */
15522
+ async function partialProfit(symbol, percentToClose) {
15523
+ backtest$1.loggerService.info(PARTIAL_PROFIT_METHOD_NAME, {
15524
+ symbol,
15525
+ percentToClose,
15526
+ });
15527
+ if (!ExecutionContextService.hasContext()) {
15528
+ throw new Error("partialProfit requires an execution context");
15529
+ }
15530
+ if (!MethodContextService.hasContext()) {
15531
+ throw new Error("partialProfit requires a method context");
15532
+ }
15533
+ const currentPrice = await getAveragePrice(symbol);
15534
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
15535
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
15536
+ await backtest$1.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
15537
+ }
15538
+ /**
15539
+ * Executes partial close at loss level (moving toward SL).
15540
+ *
15541
+ * Closes a percentage of the active pending position at loss.
15542
+ * Price must be moving toward stop loss (in loss direction).
15543
+ *
15544
+ * Automatically detects backtest/live mode from execution context.
15545
+ *
15546
+ * @param symbol - Trading pair symbol
15547
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
15548
+ * @returns Promise that resolves when state is updated
15549
+ *
15550
+ * @throws Error if currentPrice is not in loss direction:
15551
+ * - LONG: currentPrice must be < priceOpen
15552
+ * - SHORT: currentPrice must be > priceOpen
15553
+ *
15554
+ * @example
15555
+ * ```typescript
15556
+ * import { partialLoss } from "backtest-kit";
15557
+ *
15558
+ * // Close 40% of LONG position at loss
15559
+ * await partialLoss("BTCUSDT", 40, 38000);
15560
+ * ```
15561
+ */
15562
+ async function partialLoss(symbol, percentToClose) {
15563
+ backtest$1.loggerService.info(PARTIAL_LOSS_METHOD_NAME, {
15564
+ symbol,
15565
+ percentToClose,
15566
+ });
15567
+ if (!ExecutionContextService.hasContext()) {
15568
+ throw new Error("partialLoss requires an execution context");
15569
+ }
15570
+ if (!MethodContextService.hasContext()) {
15571
+ throw new Error("partialLoss requires a method context");
15572
+ }
15573
+ const currentPrice = await getAveragePrice(symbol);
15574
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
15575
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
15576
+ await backtest$1.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
15577
+ }
14605
15578
 
14606
15579
  /**
14607
15580
  * Sets custom logger implementation for the framework.
@@ -16284,258 +17257,70 @@ function listenRisk(fn) {
16284
17257
  * cancel();
16285
17258
  * ```
16286
17259
  */
16287
- function listenRiskOnce(filterFn, fn) {
16288
- backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
16289
- return riskSubject.filter(filterFn).once(fn);
16290
- }
16291
- /**
16292
- * Subscribes to ping events during scheduled signal monitoring with queued async processing.
16293
- *
16294
- * Events are emitted every minute when a scheduled signal is being monitored (waiting for activation).
16295
- * Allows tracking of scheduled signal lifecycle and custom monitoring logic.
16296
- *
16297
- * @param fn - Callback function to handle ping events
16298
- * @returns Unsubscribe function to stop listening
16299
- *
16300
- * @example
16301
- * ```typescript
16302
- * import { listenPing } from "./function/event";
16303
- *
16304
- * const unsubscribe = listenPing((event) => {
16305
- * console.log(`Ping for ${event.symbol} at ${new Date(event.timestamp).toISOString()}`);
16306
- * console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
16307
- * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
16308
- * });
16309
- *
16310
- * // Later: stop listening
16311
- * unsubscribe();
16312
- * ```
16313
- */
16314
- function listenPing(fn) {
16315
- backtest$1.loggerService.log(LISTEN_PING_METHOD_NAME);
16316
- return pingSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
16317
- }
16318
- /**
16319
- * Subscribes to filtered ping events with one-time execution.
16320
- *
16321
- * Listens for events matching the filter predicate, then executes callback once
16322
- * and automatically unsubscribes. Useful for waiting for specific ping conditions.
16323
- *
16324
- * @param filterFn - Predicate to filter which events trigger the callback
16325
- * @param fn - Callback function to handle the filtered event (called only once)
16326
- * @returns Unsubscribe function to cancel the listener before it fires
16327
- *
16328
- * @example
16329
- * ```typescript
16330
- * import { listenPingOnce } from "./function/event";
16331
- *
16332
- * // Wait for first ping on BTCUSDT
16333
- * listenPingOnce(
16334
- * (event) => event.symbol === "BTCUSDT",
16335
- * (event) => console.log("First BTCUSDT ping received")
16336
- * );
16337
- *
16338
- * // Wait for ping in backtest mode
16339
- * const cancel = listenPingOnce(
16340
- * (event) => event.backtest === true,
16341
- * (event) => console.log("Backtest ping received at", new Date(event.timestamp))
16342
- * );
16343
- *
16344
- * // Cancel if needed before event fires
16345
- * cancel();
16346
- * ```
16347
- */
16348
- function listenPingOnce(filterFn, fn) {
16349
- backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
16350
- return pingSubject.filter(filterFn).once(fn);
16351
- }
16352
-
16353
- const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
16354
- const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
16355
- const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
16356
- const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
16357
- const GET_DATE_METHOD_NAME = "exchange.getDate";
16358
- const GET_MODE_METHOD_NAME = "exchange.getMode";
16359
- const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
16360
- /**
16361
- * Checks if trade context is active (execution and method contexts).
16362
- *
16363
- * Returns true when both contexts are active, which is required for calling
16364
- * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
16365
- * getDate, and getMode.
16366
- *
16367
- * @returns true if trade context is active, false otherwise
16368
- *
16369
- * @example
16370
- * ```typescript
16371
- * import { hasTradeContext, getCandles } from "backtest-kit";
16372
- *
16373
- * if (hasTradeContext()) {
16374
- * const candles = await getCandles("BTCUSDT", "1m", 100);
16375
- * } else {
16376
- * console.log("Trade context not active");
16377
- * }
16378
- * ```
16379
- */
16380
- function hasTradeContext() {
16381
- backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
16382
- return ExecutionContextService.hasContext() && MethodContextService.hasContext();
16383
- }
16384
- /**
16385
- * Fetches historical candle data from the registered exchange.
16386
- *
16387
- * Candles are fetched backwards from the current execution context time.
16388
- * Uses the exchange's getCandles implementation.
16389
- *
16390
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16391
- * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
16392
- * @param limit - Number of candles to fetch
16393
- * @returns Promise resolving to array of candle data
16394
- *
16395
- * @example
16396
- * ```typescript
16397
- * const candles = await getCandles("BTCUSDT", "1m", 100);
16398
- * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
16399
- * ```
16400
- */
16401
- async function getCandles(symbol, interval, limit) {
16402
- backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
16403
- symbol,
16404
- interval,
16405
- limit,
16406
- });
16407
- if (!ExecutionContextService.hasContext()) {
16408
- throw new Error("getCandles requires an execution context");
16409
- }
16410
- if (!MethodContextService.hasContext()) {
16411
- throw new Error("getCandles requires a method context");
16412
- }
16413
- return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
16414
- }
16415
- /**
16416
- * Calculates VWAP (Volume Weighted Average Price) for a symbol.
16417
- *
16418
- * Uses the last 5 1-minute candles to calculate:
16419
- * - Typical Price = (high + low + close) / 3
16420
- * - VWAP = sum(typical_price * volume) / sum(volume)
16421
- *
16422
- * If volume is zero, returns simple average of close prices.
16423
- *
16424
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16425
- * @returns Promise resolving to VWAP price
16426
- *
16427
- * @example
16428
- * ```typescript
16429
- * const vwap = await getAveragePrice("BTCUSDT");
16430
- * console.log(vwap); // 50125.43
16431
- * ```
16432
- */
16433
- async function getAveragePrice(symbol) {
16434
- backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
16435
- symbol,
16436
- });
16437
- if (!ExecutionContextService.hasContext()) {
16438
- throw new Error("getAveragePrice requires an execution context");
16439
- }
16440
- if (!MethodContextService.hasContext()) {
16441
- throw new Error("getAveragePrice requires a method context");
16442
- }
16443
- return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
16444
- }
16445
- /**
16446
- * Formats a price value according to exchange rules.
16447
- *
16448
- * Uses the exchange's formatPrice implementation for proper decimal places.
16449
- *
16450
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16451
- * @param price - Raw price value
16452
- * @returns Promise resolving to formatted price string
16453
- *
16454
- * @example
16455
- * ```typescript
16456
- * const formatted = await formatPrice("BTCUSDT", 50000.123456);
16457
- * console.log(formatted); // "50000.12"
16458
- * ```
16459
- */
16460
- async function formatPrice(symbol, price) {
16461
- backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
16462
- symbol,
16463
- price,
16464
- });
16465
- if (!MethodContextService.hasContext()) {
16466
- throw new Error("formatPrice requires a method context");
16467
- }
16468
- return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
16469
- }
16470
- /**
16471
- * Formats a quantity value according to exchange rules.
16472
- *
16473
- * Uses the exchange's formatQuantity implementation for proper decimal places.
16474
- *
16475
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16476
- * @param quantity - Raw quantity value
16477
- * @returns Promise resolving to formatted quantity string
16478
- *
16479
- * @example
16480
- * ```typescript
16481
- * const formatted = await formatQuantity("BTCUSDT", 0.123456789);
16482
- * console.log(formatted); // "0.12345678"
16483
- * ```
16484
- */
16485
- async function formatQuantity(symbol, quantity) {
16486
- backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
16487
- symbol,
16488
- quantity,
16489
- });
16490
- if (!MethodContextService.hasContext()) {
16491
- throw new Error("formatQuantity requires a method context");
16492
- }
16493
- return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
17260
+ function listenRiskOnce(filterFn, fn) {
17261
+ backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
17262
+ return riskSubject.filter(filterFn).once(fn);
16494
17263
  }
16495
17264
  /**
16496
- * Gets the current date from execution context.
17265
+ * Subscribes to ping events during scheduled signal monitoring with queued async processing.
16497
17266
  *
16498
- * In backtest mode: returns the current timeframe date being processed
16499
- * In live mode: returns current real-time date
17267
+ * Events are emitted every minute when a scheduled signal is being monitored (waiting for activation).
17268
+ * Allows tracking of scheduled signal lifecycle and custom monitoring logic.
16500
17269
  *
16501
- * @returns Promise resolving to current execution context date
17270
+ * @param fn - Callback function to handle ping events
17271
+ * @returns Unsubscribe function to stop listening
16502
17272
  *
16503
17273
  * @example
16504
17274
  * ```typescript
16505
- * const date = await getDate();
16506
- * console.log(date); // 2024-01-01T12:00:00.000Z
17275
+ * import { listenPing } from "./function/event";
17276
+ *
17277
+ * const unsubscribe = listenPing((event) => {
17278
+ * console.log(`Ping for ${event.symbol} at ${new Date(event.timestamp).toISOString()}`);
17279
+ * console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
17280
+ * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
17281
+ * });
17282
+ *
17283
+ * // Later: stop listening
17284
+ * unsubscribe();
16507
17285
  * ```
16508
17286
  */
16509
- async function getDate() {
16510
- backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
16511
- if (!ExecutionContextService.hasContext()) {
16512
- throw new Error("getDate requires an execution context");
16513
- }
16514
- const { when } = backtest$1.executionContextService.context;
16515
- return new Date(when.getTime());
17287
+ function listenPing(fn) {
17288
+ backtest$1.loggerService.log(LISTEN_PING_METHOD_NAME);
17289
+ return pingSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
16516
17290
  }
16517
17291
  /**
16518
- * Gets the current execution mode.
17292
+ * Subscribes to filtered ping events with one-time execution.
16519
17293
  *
16520
- * @returns Promise resolving to "backtest" or "live"
17294
+ * Listens for events matching the filter predicate, then executes callback once
17295
+ * and automatically unsubscribes. Useful for waiting for specific ping conditions.
17296
+ *
17297
+ * @param filterFn - Predicate to filter which events trigger the callback
17298
+ * @param fn - Callback function to handle the filtered event (called only once)
17299
+ * @returns Unsubscribe function to cancel the listener before it fires
16521
17300
  *
16522
17301
  * @example
16523
17302
  * ```typescript
16524
- * const mode = await getMode();
16525
- * if (mode === "backtest") {
16526
- * console.log("Running in backtest mode");
16527
- * } else {
16528
- * console.log("Running in live mode");
16529
- * }
17303
+ * import { listenPingOnce } from "./function/event";
17304
+ *
17305
+ * // Wait for first ping on BTCUSDT
17306
+ * listenPingOnce(
17307
+ * (event) => event.symbol === "BTCUSDT",
17308
+ * (event) => console.log("First BTCUSDT ping received")
17309
+ * );
17310
+ *
17311
+ * // Wait for ping in backtest mode
17312
+ * const cancel = listenPingOnce(
17313
+ * (event) => event.backtest === true,
17314
+ * (event) => console.log("Backtest ping received at", new Date(event.timestamp))
17315
+ * );
17316
+ *
17317
+ * // Cancel if needed before event fires
17318
+ * cancel();
16530
17319
  * ```
16531
17320
  */
16532
- async function getMode() {
16533
- backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
16534
- if (!ExecutionContextService.hasContext()) {
16535
- throw new Error("getMode requires an execution context");
16536
- }
16537
- const { backtest: bt } = backtest$1.executionContextService.context;
16538
- return bt ? "backtest" : "live";
17321
+ function listenPingOnce(filterFn, fn) {
17322
+ backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
17323
+ return pingSubject.filter(filterFn).once(fn);
16539
17324
  }
16540
17325
 
16541
17326
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
@@ -16625,6 +17410,8 @@ const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
16625
17410
  const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
16626
17411
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
16627
17412
  const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
17413
+ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
17414
+ const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
16628
17415
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
16629
17416
  /**
16630
17417
  * Internal task function that runs backtest and handles completion.
@@ -16651,6 +17438,7 @@ const INSTANCE_TASK_FN$2 = async (symbol, context, self) => {
16651
17438
  await doneBacktestSubject.next({
16652
17439
  exchangeName: context.exchangeName,
16653
17440
  strategyName: context.strategyName,
17441
+ frameName: context.frameName,
16654
17442
  backtest: true,
16655
17443
  symbol,
16656
17444
  });
@@ -16872,6 +17660,7 @@ class BacktestInstance {
16872
17660
  await doneBacktestSubject.next({
16873
17661
  exchangeName: context.exchangeName,
16874
17662
  strategyName: context.strategyName,
17663
+ frameName: context.frameName,
16875
17664
  backtest: true,
16876
17665
  symbol,
16877
17666
  });
@@ -16984,6 +17773,10 @@ class BacktestUtils {
16984
17773
  * ```
16985
17774
  */
16986
17775
  this.getPendingSignal = async (symbol, context) => {
17776
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL, {
17777
+ symbol,
17778
+ context,
17779
+ });
16987
17780
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
16988
17781
  {
16989
17782
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17011,6 +17804,10 @@ class BacktestUtils {
17011
17804
  * ```
17012
17805
  */
17013
17806
  this.getScheduledSignal = async (symbol, context) => {
17807
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
17808
+ symbol,
17809
+ context,
17810
+ });
17014
17811
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
17015
17812
  {
17016
17813
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17044,6 +17841,10 @@ class BacktestUtils {
17044
17841
  * ```
17045
17842
  */
17046
17843
  this.stop = async (symbol, context) => {
17844
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
17845
+ symbol,
17846
+ context,
17847
+ });
17047
17848
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_STOP);
17048
17849
  {
17049
17850
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17078,6 +17879,11 @@ class BacktestUtils {
17078
17879
  * ```
17079
17880
  */
17080
17881
  this.cancel = async (symbol, context, cancelId) => {
17882
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_CANCEL, {
17883
+ symbol,
17884
+ context,
17885
+ cancelId,
17886
+ });
17081
17887
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CANCEL);
17082
17888
  {
17083
17889
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17088,6 +17894,92 @@ class BacktestUtils {
17088
17894
  }
17089
17895
  await backtest$1.strategyCoreService.cancel(true, symbol, context, cancelId);
17090
17896
  };
17897
+ /**
17898
+ * Executes partial close at profit level (moving toward TP).
17899
+ *
17900
+ * Closes a percentage of the active pending position at profit.
17901
+ * Price must be moving toward take profit (in profit direction).
17902
+ *
17903
+ * @param symbol - Trading pair symbol
17904
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
17905
+ * @param currentPrice - Current market price for this partial close
17906
+ * @param context - Execution context with strategyName, exchangeName, and frameName
17907
+ * @returns Promise that resolves when state is updated
17908
+ *
17909
+ * @throws Error if currentPrice is not in profit direction:
17910
+ * - LONG: currentPrice must be > priceOpen
17911
+ * - SHORT: currentPrice must be < priceOpen
17912
+ *
17913
+ * @example
17914
+ * ```typescript
17915
+ * // Close 30% of LONG position at profit
17916
+ * await Backtest.partialProfit("BTCUSDT", 30, 45000, {
17917
+ * exchangeName: "binance",
17918
+ * frameName: "frame1",
17919
+ * strategyName: "my-strategy"
17920
+ * });
17921
+ * ```
17922
+ */
17923
+ this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
17924
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT, {
17925
+ symbol,
17926
+ percentToClose,
17927
+ currentPrice,
17928
+ context,
17929
+ });
17930
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
17931
+ {
17932
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
17933
+ riskName &&
17934
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
17935
+ riskList &&
17936
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
17937
+ }
17938
+ await backtest$1.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
17939
+ };
17940
+ /**
17941
+ * Executes partial close at loss level (moving toward SL).
17942
+ *
17943
+ * Closes a percentage of the active pending position at loss.
17944
+ * Price must be moving toward stop loss (in loss direction).
17945
+ *
17946
+ * @param symbol - Trading pair symbol
17947
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
17948
+ * @param currentPrice - Current market price for this partial close
17949
+ * @param context - Execution context with strategyName, exchangeName, and frameName
17950
+ * @returns Promise that resolves when state is updated
17951
+ *
17952
+ * @throws Error if currentPrice is not in loss direction:
17953
+ * - LONG: currentPrice must be < priceOpen
17954
+ * - SHORT: currentPrice must be > priceOpen
17955
+ *
17956
+ * @example
17957
+ * ```typescript
17958
+ * // Close 40% of LONG position at loss
17959
+ * await Backtest.partialLoss("BTCUSDT", 40, 38000, {
17960
+ * exchangeName: "binance",
17961
+ * frameName: "frame1",
17962
+ * strategyName: "my-strategy"
17963
+ * });
17964
+ * ```
17965
+ */
17966
+ this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
17967
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS, {
17968
+ symbol,
17969
+ percentToClose,
17970
+ currentPrice,
17971
+ context,
17972
+ });
17973
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
17974
+ {
17975
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
17976
+ riskName &&
17977
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
17978
+ riskList &&
17979
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
17980
+ }
17981
+ await backtest$1.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
17982
+ };
17091
17983
  /**
17092
17984
  * Gets statistical data from all closed signals for a symbol-strategy pair.
17093
17985
  *
@@ -17107,6 +17999,10 @@ class BacktestUtils {
17107
17999
  * ```
17108
18000
  */
17109
18001
  this.getData = async (symbol, context) => {
18002
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_DATA, {
18003
+ symbol,
18004
+ context,
18005
+ });
17110
18006
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_DATA);
17111
18007
  {
17112
18008
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17137,6 +18033,10 @@ class BacktestUtils {
17137
18033
  * ```
17138
18034
  */
17139
18035
  this.getReport = async (symbol, context, columns) => {
18036
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
18037
+ symbol,
18038
+ context,
18039
+ });
17140
18040
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_REPORT);
17141
18041
  {
17142
18042
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17174,6 +18074,11 @@ class BacktestUtils {
17174
18074
  * ```
17175
18075
  */
17176
18076
  this.dump = async (symbol, context, path, columns) => {
18077
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
18078
+ symbol,
18079
+ context,
18080
+ path,
18081
+ });
17177
18082
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_DUMP);
17178
18083
  {
17179
18084
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17234,6 +18139,8 @@ const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
17234
18139
  const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
17235
18140
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
17236
18141
  const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
18142
+ const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
18143
+ const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
17237
18144
  /**
17238
18145
  * Internal task function that runs live trading and handles completion.
17239
18146
  * Consumes live trading results and updates instance state flags.
@@ -17259,6 +18166,7 @@ const INSTANCE_TASK_FN$1 = async (symbol, context, self) => {
17259
18166
  await doneLiveSubject.next({
17260
18167
  exchangeName: context.exchangeName,
17261
18168
  strategyName: context.strategyName,
18169
+ frameName: "",
17262
18170
  backtest: false,
17263
18171
  symbol,
17264
18172
  });
@@ -17445,6 +18353,7 @@ class LiveInstance {
17445
18353
  await doneLiveSubject.next({
17446
18354
  exchangeName: context.exchangeName,
17447
18355
  strategyName: context.strategyName,
18356
+ frameName: "",
17448
18357
  backtest: false,
17449
18358
  symbol,
17450
18359
  });
@@ -17564,6 +18473,10 @@ class LiveUtils {
17564
18473
  * ```
17565
18474
  */
17566
18475
  this.getPendingSignal = async (symbol, context) => {
18476
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_PENDING_SIGNAL, {
18477
+ symbol,
18478
+ context,
18479
+ });
17567
18480
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
17568
18481
  {
17569
18482
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17593,6 +18506,10 @@ class LiveUtils {
17593
18506
  * ```
17594
18507
  */
17595
18508
  this.getScheduledSignal = async (symbol, context) => {
18509
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
18510
+ symbol,
18511
+ context,
18512
+ });
17596
18513
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
17597
18514
  {
17598
18515
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17623,6 +18540,10 @@ class LiveUtils {
17623
18540
  * ```
17624
18541
  */
17625
18542
  this.stop = async (symbol, context) => {
18543
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
18544
+ symbol,
18545
+ context,
18546
+ });
17626
18547
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_STOP);
17627
18548
  {
17628
18549
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17659,6 +18580,11 @@ class LiveUtils {
17659
18580
  * ```
17660
18581
  */
17661
18582
  this.cancel = async (symbol, context, cancelId) => {
18583
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_CANCEL, {
18584
+ symbol,
18585
+ context,
18586
+ cancelId,
18587
+ });
17662
18588
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CANCEL);
17663
18589
  {
17664
18590
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17671,6 +18597,94 @@ class LiveUtils {
17671
18597
  frameName: "",
17672
18598
  }, cancelId);
17673
18599
  };
18600
+ /**
18601
+ * Executes partial close at profit level (moving toward TP).
18602
+ *
18603
+ * Closes a percentage of the active pending position at profit.
18604
+ * Price must be moving toward take profit (in profit direction).
18605
+ *
18606
+ * @param symbol - Trading pair symbol
18607
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
18608
+ * @param currentPrice - Current market price for this partial close
18609
+ * @param context - Execution context with strategyName and exchangeName
18610
+ * @returns Promise that resolves when state is updated
18611
+ *
18612
+ * @throws Error if currentPrice is not in profit direction:
18613
+ * - LONG: currentPrice must be > priceOpen
18614
+ * - SHORT: currentPrice must be < priceOpen
18615
+ *
18616
+ * @example
18617
+ * ```typescript
18618
+ * // Close 30% of LONG position at profit
18619
+ * await Live.partialProfit("BTCUSDT", 30, 45000, {
18620
+ * exchangeName: "binance",
18621
+ * strategyName: "my-strategy"
18622
+ * });
18623
+ * ```
18624
+ */
18625
+ this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
18626
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT, {
18627
+ symbol,
18628
+ percentToClose,
18629
+ currentPrice,
18630
+ context,
18631
+ });
18632
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
18633
+ {
18634
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18635
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
18636
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
18637
+ }
18638
+ await backtest$1.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
18639
+ strategyName: context.strategyName,
18640
+ exchangeName: context.exchangeName,
18641
+ frameName: "",
18642
+ });
18643
+ };
18644
+ /**
18645
+ * Executes partial close at loss level (moving toward SL).
18646
+ *
18647
+ * Closes a percentage of the active pending position at loss.
18648
+ * Price must be moving toward stop loss (in loss direction).
18649
+ *
18650
+ * @param symbol - Trading pair symbol
18651
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
18652
+ * @param currentPrice - Current market price for this partial close
18653
+ * @param context - Execution context with strategyName and exchangeName
18654
+ * @returns Promise that resolves when state is updated
18655
+ *
18656
+ * @throws Error if currentPrice is not in loss direction:
18657
+ * - LONG: currentPrice must be < priceOpen
18658
+ * - SHORT: currentPrice must be > priceOpen
18659
+ *
18660
+ * @example
18661
+ * ```typescript
18662
+ * // Close 40% of LONG position at loss
18663
+ * await Live.partialLoss("BTCUSDT", 40, 38000, {
18664
+ * exchangeName: "binance",
18665
+ * strategyName: "my-strategy"
18666
+ * });
18667
+ * ```
18668
+ */
18669
+ this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
18670
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS, {
18671
+ symbol,
18672
+ percentToClose,
18673
+ currentPrice,
18674
+ context,
18675
+ });
18676
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS);
18677
+ {
18678
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18679
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
18680
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
18681
+ }
18682
+ await backtest$1.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
18683
+ strategyName: context.strategyName,
18684
+ exchangeName: context.exchangeName,
18685
+ frameName: "",
18686
+ });
18687
+ };
17674
18688
  /**
17675
18689
  * Gets statistical data from all live trading events for a symbol-strategy pair.
17676
18690
  *
@@ -17690,7 +18704,11 @@ class LiveUtils {
17690
18704
  * ```
17691
18705
  */
17692
18706
  this.getData = async (symbol, context) => {
17693
- backtest$1.strategyValidationService.validate(context.strategyName, "LiveUtils.getData");
18707
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_DATA, {
18708
+ symbol,
18709
+ context,
18710
+ });
18711
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_DATA);
17694
18712
  {
17695
18713
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
17696
18714
  riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
@@ -17718,6 +18736,10 @@ class LiveUtils {
17718
18736
  * ```
17719
18737
  */
17720
18738
  this.getReport = async (symbol, context, columns) => {
18739
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
18740
+ symbol,
18741
+ context,
18742
+ });
17721
18743
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_REPORT);
17722
18744
  {
17723
18745
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17753,6 +18775,11 @@ class LiveUtils {
17753
18775
  * ```
17754
18776
  */
17755
18777
  this.dump = async (symbol, context, path, columns) => {
18778
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
18779
+ symbol,
18780
+ context,
18781
+ path,
18782
+ });
17756
18783
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_DUMP);
17757
18784
  {
17758
18785
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -18099,6 +19126,7 @@ const INSTANCE_TASK_FN = async (symbol, context, self) => {
18099
19126
  await doneWalkerSubject.next({
18100
19127
  exchangeName: walkerSchema.exchangeName,
18101
19128
  strategyName: context.walkerName,
19129
+ frameName: walkerSchema.frameName,
18102
19130
  backtest: true,
18103
19131
  symbol,
18104
19132
  });
@@ -18289,6 +19317,7 @@ class WalkerInstance {
18289
19317
  doneWalkerSubject.next({
18290
19318
  exchangeName: walkerSchema.exchangeName,
18291
19319
  strategyName: context.walkerName,
19320
+ frameName: walkerSchema.frameName,
18292
19321
  backtest: true,
18293
19322
  symbol,
18294
19323
  });
@@ -18397,18 +19426,22 @@ class WalkerUtils {
18397
19426
  * Stop signal is filtered by walkerName to prevent interference.
18398
19427
  *
18399
19428
  * @param symbol - Trading pair symbol
18400
- * @param walkerName - Walker name to stop
19429
+ * @param context - Execution context with walker name
18401
19430
  * @returns Promise that resolves when all stop flags are set
18402
19431
  *
18403
19432
  * @example
18404
19433
  * ```typescript
18405
19434
  * // Stop walker and all its strategies
18406
- * await Walker.stop("BTCUSDT", "my-walker");
19435
+ * await Walker.stop("BTCUSDT", { walkerName: "my-walker" });
18407
19436
  * ```
18408
19437
  */
18409
- this.stop = async (symbol, walkerName) => {
18410
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_STOP);
18411
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
19438
+ this.stop = async (symbol, context) => {
19439
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_STOP, {
19440
+ symbol,
19441
+ context,
19442
+ });
19443
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_STOP);
19444
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18412
19445
  for (const strategyName of walkerSchema.strategies) {
18413
19446
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_STOP);
18414
19447
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18418,7 +19451,7 @@ class WalkerUtils {
18418
19451
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_STOP));
18419
19452
  }
18420
19453
  for (const strategyName of walkerSchema.strategies) {
18421
- await walkerStopSubject.next({ symbol, strategyName, walkerName });
19454
+ await walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
18422
19455
  await backtest$1.strategyCoreService.stop(true, symbol, {
18423
19456
  strategyName,
18424
19457
  exchangeName: walkerSchema.exchangeName,
@@ -18430,18 +19463,22 @@ class WalkerUtils {
18430
19463
  * Gets walker results data from all strategy comparisons.
18431
19464
  *
18432
19465
  * @param symbol - Trading symbol
18433
- * @param walkerName - Walker name to get data for
19466
+ * @param context - Execution context with walker name
18434
19467
  * @returns Promise resolving to walker results data object
18435
19468
  *
18436
19469
  * @example
18437
19470
  * ```typescript
18438
- * const results = await Walker.getData("BTCUSDT", "my-walker");
19471
+ * const results = await Walker.getData("BTCUSDT", { walkerName: "my-walker" });
18439
19472
  * console.log(results.bestStrategy, results.bestMetric);
18440
19473
  * ```
18441
19474
  */
18442
- this.getData = async (symbol, walkerName) => {
18443
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_DATA);
18444
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
19475
+ this.getData = async (symbol, context) => {
19476
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_DATA, {
19477
+ symbol,
19478
+ context,
19479
+ });
19480
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_DATA);
19481
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18445
19482
  for (const strategyName of walkerSchema.strategies) {
18446
19483
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_DATA);
18447
19484
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18450,7 +19487,7 @@ class WalkerUtils {
18450
19487
  riskList &&
18451
19488
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_DATA));
18452
19489
  }
18453
- return await backtest$1.walkerMarkdownService.getData(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
19490
+ return await backtest$1.walkerMarkdownService.getData(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18454
19491
  exchangeName: walkerSchema.exchangeName,
18455
19492
  frameName: walkerSchema.frameName,
18456
19493
  });
@@ -18459,20 +19496,24 @@ class WalkerUtils {
18459
19496
  * Generates markdown report with all strategy comparisons for a walker.
18460
19497
  *
18461
19498
  * @param symbol - Trading symbol
18462
- * @param walkerName - Walker name to generate report for
19499
+ * @param context - Execution context with walker name
18463
19500
  * @param strategyColumns - Optional strategy columns configuration
18464
19501
  * @param pnlColumns - Optional PNL columns configuration
18465
19502
  * @returns Promise resolving to markdown formatted report string
18466
19503
  *
18467
19504
  * @example
18468
19505
  * ```typescript
18469
- * const markdown = await Walker.getReport("BTCUSDT", "my-walker");
19506
+ * const markdown = await Walker.getReport("BTCUSDT", { walkerName: "my-walker" });
18470
19507
  * console.log(markdown);
18471
19508
  * ```
18472
19509
  */
18473
- this.getReport = async (symbol, walkerName, strategyColumns, pnlColumns) => {
18474
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_REPORT);
18475
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
19510
+ this.getReport = async (symbol, context, strategyColumns, pnlColumns) => {
19511
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_REPORT, {
19512
+ symbol,
19513
+ context,
19514
+ });
19515
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_REPORT);
19516
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18476
19517
  for (const strategyName of walkerSchema.strategies) {
18477
19518
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_REPORT);
18478
19519
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18481,7 +19522,7 @@ class WalkerUtils {
18481
19522
  riskList &&
18482
19523
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_REPORT));
18483
19524
  }
18484
- return await backtest$1.walkerMarkdownService.getReport(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
19525
+ return await backtest$1.walkerMarkdownService.getReport(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18485
19526
  exchangeName: walkerSchema.exchangeName,
18486
19527
  frameName: walkerSchema.frameName,
18487
19528
  }, strategyColumns, pnlColumns);
@@ -18490,7 +19531,7 @@ class WalkerUtils {
18490
19531
  * Saves walker report to disk.
18491
19532
  *
18492
19533
  * @param symbol - Trading symbol
18493
- * @param walkerName - Walker name to save report for
19534
+ * @param context - Execution context with walker name
18494
19535
  * @param path - Optional directory path to save report (default: "./dump/walker")
18495
19536
  * @param strategyColumns - Optional strategy columns configuration
18496
19537
  * @param pnlColumns - Optional PNL columns configuration
@@ -18498,15 +19539,20 @@ class WalkerUtils {
18498
19539
  * @example
18499
19540
  * ```typescript
18500
19541
  * // Save to default path: ./dump/walker/my-walker.md
18501
- * await Walker.dump("BTCUSDT", "my-walker");
19542
+ * await Walker.dump("BTCUSDT", { walkerName: "my-walker" });
18502
19543
  *
18503
19544
  * // Save to custom path: ./custom/path/my-walker.md
18504
- * await Walker.dump("BTCUSDT", "my-walker", "./custom/path");
19545
+ * await Walker.dump("BTCUSDT", { walkerName: "my-walker" }, "./custom/path");
18505
19546
  * ```
18506
19547
  */
18507
- this.dump = async (symbol, walkerName, path, strategyColumns, pnlColumns) => {
18508
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_DUMP);
18509
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
19548
+ this.dump = async (symbol, context, path, strategyColumns, pnlColumns) => {
19549
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_DUMP, {
19550
+ symbol,
19551
+ context,
19552
+ path,
19553
+ });
19554
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_DUMP);
19555
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18510
19556
  for (const strategyName of walkerSchema.strategies) {
18511
19557
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_DUMP);
18512
19558
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18515,7 +19561,7 @@ class WalkerUtils {
18515
19561
  riskList &&
18516
19562
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_DUMP));
18517
19563
  }
18518
- await backtest$1.walkerMarkdownService.dump(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
19564
+ await backtest$1.walkerMarkdownService.dump(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18519
19565
  exchangeName: walkerSchema.exchangeName,
18520
19566
  frameName: walkerSchema.frameName,
18521
19567
  }, path, strategyColumns, pnlColumns);
@@ -20335,6 +21381,8 @@ exports.listenWalker = listenWalker;
20335
21381
  exports.listenWalkerComplete = listenWalkerComplete;
20336
21382
  exports.listenWalkerOnce = listenWalkerOnce;
20337
21383
  exports.listenWalkerProgress = listenWalkerProgress;
21384
+ exports.partialLoss = partialLoss;
21385
+ exports.partialProfit = partialProfit;
20338
21386
  exports.setColumns = setColumns;
20339
21387
  exports.setConfig = setConfig;
20340
21388
  exports.setLogger = setLogger;