dexbot 1.4.17 → 1.4.18

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 (209) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/analysis/README.md +43 -43
  3. package/analysis/ama_fitting/README.md +8 -8
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/bot_fitting/README.md +4 -4
  6. package/analysis/bot_fitting/backtest_ama_sweep.ts +1 -1
  7. package/analysis/bot_usage/discover_bot_accounts.ts +11 -11
  8. package/analysis/tradingview/README.md +4 -4
  9. package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +2 -2
  10. package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +1 -1
  11. package/analysis/trend_detection/package.json +1 -1
  12. package/claw/README.md +45 -45
  13. package/claw/docs/AI_BOT_LIBRARY_API.md +6 -6
  14. package/claw/examples/short_mpa_bts_strategy.ts +3 -3
  15. package/claw/package.json +21 -21
  16. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  17. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  18. package/claw/scripts/claw_bridge.ts +2 -2
  19. package/claw/scripts/claw_skill_md.ts +1 -1
  20. package/claw/skills/launcher-ops/references/launcher-workflow.md +5 -5
  21. package/claw/skills/margin-trading/references/honest-asset-list.md +1 -1
  22. package/claw/skills/memu-memory/SKILL.md +3 -3
  23. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  24. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts +3 -0
  25. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts.map +1 -0
  26. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js +148 -0
  27. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js.map +1 -0
  28. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts +12 -0
  29. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -0
  30. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +389 -0
  31. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -0
  32. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts +3 -0
  33. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts.map +1 -0
  34. package/dist/analysis/ama_fitting/calibrate_convergence_er.js +200 -0
  35. package/dist/analysis/ama_fitting/calibrate_convergence_er.js.map +1 -0
  36. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts +3 -0
  37. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts.map +1 -0
  38. package/dist/analysis/ama_fitting/fetch_lp_candles.js +213 -0
  39. package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -0
  40. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts +25 -0
  41. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts.map +1 -0
  42. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js +247 -0
  43. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js.map +1 -0
  44. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts +35 -0
  45. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts.map +1 -0
  46. package/dist/analysis/ama_fitting/optimizer_high_resolution.js +782 -0
  47. package/dist/analysis/ama_fitting/optimizer_high_resolution.js.map +1 -0
  48. package/dist/analysis/analyze_derivatives.d.ts +86 -0
  49. package/dist/analysis/analyze_derivatives.d.ts.map +1 -0
  50. package/dist/analysis/analyze_derivatives.js +285 -0
  51. package/dist/analysis/analyze_derivatives.js.map +1 -0
  52. package/dist/analysis/analyze_dynamic_weight.d.ts +14 -0
  53. package/dist/analysis/analyze_dynamic_weight.d.ts.map +1 -0
  54. package/dist/analysis/analyze_dynamic_weight.js +197 -0
  55. package/dist/analysis/analyze_dynamic_weight.js.map +1 -0
  56. package/dist/analysis/analyze_kalman.d.ts +13 -0
  57. package/dist/analysis/analyze_kalman.d.ts.map +1 -0
  58. package/dist/analysis/analyze_kalman.js +118 -0
  59. package/dist/analysis/analyze_kalman.js.map +1 -0
  60. package/dist/analysis/analyze_regime.d.ts +20 -0
  61. package/dist/analysis/analyze_regime.d.ts.map +1 -0
  62. package/dist/analysis/analyze_regime.js +139 -0
  63. package/dist/analysis/analyze_regime.js.map +1 -0
  64. package/dist/analysis/analyze_regime_windows.d.ts +19 -0
  65. package/dist/analysis/analyze_regime_windows.d.ts.map +1 -0
  66. package/dist/analysis/analyze_regime_windows.js +414 -0
  67. package/dist/analysis/analyze_regime_windows.js.map +1 -0
  68. package/dist/analysis/analyze_risk_profile.d.ts +3 -0
  69. package/dist/analysis/analyze_risk_profile.d.ts.map +1 -0
  70. package/dist/analysis/analyze_risk_profile.js +186 -0
  71. package/dist/analysis/analyze_risk_profile.js.map +1 -0
  72. package/dist/analysis/analyze_trade_heatmap.d.ts +3 -0
  73. package/dist/analysis/analyze_trade_heatmap.d.ts.map +1 -0
  74. package/dist/analysis/analyze_trade_heatmap.js +358 -0
  75. package/dist/analysis/analyze_trade_heatmap.js.map +1 -0
  76. package/dist/analysis/analyze_volatility.d.ts +22 -0
  77. package/dist/analysis/analyze_volatility.d.ts.map +1 -0
  78. package/dist/analysis/analyze_volatility.js +165 -0
  79. package/dist/analysis/analyze_volatility.js.map +1 -0
  80. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts +91 -0
  81. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts.map +1 -0
  82. package/dist/analysis/bot_fitting/backtest_ama_sweep.js +792 -0
  83. package/dist/analysis/bot_fitting/backtest_ama_sweep.js.map +1 -0
  84. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts +2 -0
  85. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts.map +1 -0
  86. package/dist/analysis/bot_fitting/backtest_bot_fitting.js +380 -0
  87. package/dist/analysis/bot_fitting/backtest_bot_fitting.js.map +1 -0
  88. package/dist/analysis/bot_fitting/shared_utils.d.ts +26 -0
  89. package/dist/analysis/bot_fitting/shared_utils.d.ts.map +1 -0
  90. package/dist/analysis/bot_fitting/shared_utils.js +42 -0
  91. package/dist/analysis/bot_fitting/shared_utils.js.map +1 -0
  92. package/dist/analysis/bot_key_utils.d.ts +11 -0
  93. package/dist/analysis/bot_key_utils.d.ts.map +1 -0
  94. package/dist/analysis/bot_key_utils.js +153 -0
  95. package/dist/analysis/bot_key_utils.js.map +1 -0
  96. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts +3 -0
  97. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts.map +1 -0
  98. package/dist/analysis/bot_usage/discover_bot_accounts.js +499 -0
  99. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -0
  100. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +531 -0
  101. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -0
  102. package/dist/analysis/bot_usage/kibana_bot_queries.js +399 -0
  103. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -0
  104. package/dist/analysis/chart_css.d.ts +17 -0
  105. package/dist/analysis/chart_css.d.ts.map +1 -0
  106. package/dist/analysis/chart_css.js +49 -0
  107. package/dist/analysis/chart_css.js.map +1 -0
  108. package/dist/analysis/chart_ui.d.ts +45 -0
  109. package/dist/analysis/chart_ui.d.ts.map +1 -0
  110. package/dist/analysis/chart_ui.js +97 -0
  111. package/dist/analysis/chart_ui.js.map +1 -0
  112. package/dist/analysis/chart_utils.d.ts +27 -0
  113. package/dist/analysis/chart_utils.d.ts.map +1 -0
  114. package/dist/analysis/chart_utils.js +153 -0
  115. package/dist/analysis/chart_utils.js.map +1 -0
  116. package/dist/analysis/derivative_chart_generator.d.ts +12 -0
  117. package/dist/analysis/derivative_chart_generator.d.ts.map +1 -0
  118. package/dist/analysis/derivative_chart_generator.js +891 -0
  119. package/dist/analysis/derivative_chart_generator.js.map +1 -0
  120. package/dist/analysis/math_utils.d.ts +22 -0
  121. package/dist/analysis/math_utils.d.ts.map +1 -0
  122. package/dist/analysis/math_utils.js +43 -0
  123. package/dist/analysis/math_utils.js.map +1 -0
  124. package/dist/analysis/price_sources.d.ts +40 -0
  125. package/dist/analysis/price_sources.d.ts.map +1 -0
  126. package/dist/analysis/price_sources.js +94 -0
  127. package/dist/analysis/price_sources.js.map +1 -0
  128. package/dist/analysis/resolve_source.d.ts +25 -0
  129. package/dist/analysis/resolve_source.d.ts.map +1 -0
  130. package/dist/analysis/resolve_source.js +69 -0
  131. package/dist/analysis/resolve_source.js.map +1 -0
  132. package/dist/analysis/trade_profitability.d.ts +115 -0
  133. package/dist/analysis/trade_profitability.d.ts.map +1 -0
  134. package/dist/analysis/trade_profitability.js +1240 -0
  135. package/dist/analysis/trade_profitability.js.map +1 -0
  136. package/dist/analysis/tradingview/analyze_tradingview.d.ts +31 -0
  137. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -0
  138. package/dist/analysis/tradingview/analyze_tradingview.js +146 -0
  139. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -0
  140. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -0
  141. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -0
  142. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1714 -0
  143. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -0
  144. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +247 -0
  145. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +1 -0
  146. package/dist/analysis/trend_detection/derivative_analyzer.js +901 -0
  147. package/dist/analysis/trend_detection/derivative_analyzer.js.map +1 -0
  148. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts +3 -0
  149. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -0
  150. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +1361 -0
  151. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -0
  152. package/dist/analysis/trend_detection/hurst_analyzer.d.ts +6 -0
  153. package/dist/analysis/trend_detection/hurst_analyzer.d.ts.map +1 -0
  154. package/dist/analysis/trend_detection/hurst_analyzer.js +7 -0
  155. package/dist/analysis/trend_detection/hurst_analyzer.js.map +1 -0
  156. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts +3 -0
  157. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -0
  158. package/dist/analysis/trend_detection/kalman_chart_generator.js +360 -0
  159. package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -0
  160. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts +8 -0
  161. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts.map +1 -0
  162. package/dist/analysis/trend_detection/kalman_trend_analyzer.js +8 -0
  163. package/dist/analysis/trend_detection/kalman_trend_analyzer.js.map +1 -0
  164. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts +6 -0
  165. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts.map +1 -0
  166. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js +7 -0
  167. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js.map +1 -0
  168. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts +6 -0
  169. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts.map +1 -0
  170. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js +7 -0
  171. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js.map +1 -0
  172. package/dist/analysis/trend_detection/regime_chart_generator.d.ts +3 -0
  173. package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -0
  174. package/dist/analysis/trend_detection/regime_chart_generator.js +310 -0
  175. package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -0
  176. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts +2 -0
  177. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts.map +1 -0
  178. package/dist/analysis/trend_detection/tests/test_kalman_trend.js +128 -0
  179. package/dist/analysis/trend_detection/tests/test_kalman_trend.js.map +1 -0
  180. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts +2 -0
  181. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts.map +1 -0
  182. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js +37 -0
  183. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js.map +1 -0
  184. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts +3 -0
  185. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -0
  186. package/dist/analysis/trend_detection/volatility_chart_generator.js +646 -0
  187. package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -0
  188. package/dist/bot.js +1 -1
  189. package/dist/bot.js.map +1 -1
  190. package/dist/market_adapter/ama_signal_runner.js +1 -1
  191. package/dist/market_adapter/ama_signal_runner.js.map +1 -1
  192. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
  193. package/dist/scripts/generate_lp_chart.js +1 -1
  194. package/dist/scripts/test-credit-renewal.js +1 -1
  195. package/docs/DEXBOT_COMPARISON.md +3 -3
  196. package/docs/EVOLUTION.md +5 -4
  197. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  198. package/docs/GRID_RECALCULATION.md +2 -2
  199. package/docs/README.md +1 -1
  200. package/market_adapter/README.md +23 -23
  201. package/package.json +17 -21
  202. package/scripts/README.md +12 -12
  203. package/scripts/bots +5 -7
  204. package/scripts/clear-market-adapter.sh +1 -1
  205. package/scripts/dexbot +5 -7
  206. package/scripts/keys +5 -7
  207. package/scripts/pm2 +5 -7
  208. package/scripts/unlock +5 -7
  209. package/scripts/update.js +6 -7
@@ -0,0 +1,792 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
5
+ import { calculateAMA } from '../../market_adapter/core/strategies/ama.js';
6
+ import { range } from '../math_utils.js';
7
+ import { parseListOrRange, loadLpData, fmt } from './shared_utils.js';
8
+ import { getStorage } from '../../modules/storage/index.js';
9
+ const { readJSON, writeJSON } = getStorage();
10
+ 'use strict';
11
+ /**
12
+ * AMA SWEEP BACKTEST — persistent grid simulation
13
+ *
14
+ * Models the real bot mechanics:
15
+ * - Orders sit at FIXED chain prices until canceled or filled
16
+ * - When AMA drifts past reposition threshold, grid re-centers
17
+ * - Grid compression: AMA shift pushes one side's orders closer to market
18
+ * - Order sizing depends on capital, ratio (range width), and weight profile
19
+ * - Three weight profiles: valley, neutral, mountain (symmetric buy/sell)
20
+ *
21
+ * Usage:
22
+ * tsx analysis/bot_fitting/backtest_ama_sweep.ts --data <path-to-lp-candles.json>
23
+ * tsx analysis/bot_fitting/backtest_ama_sweep.ts --data <path-to-lp-candles.json> --spread 4:16:1 --increment 0.5:4:0.25
24
+ */
25
+ const DEFAULT_MAX_ORDERS = 20; // matches bot default activeOrders per side
26
+ const DEFAULT_FEE_ROUNDTRIP_PCT = 0.20;
27
+ const DEFAULT_MIN_SPREAD_FACTOR = 2.1;
28
+ const DEFAULT_CAPITAL = 10000; // notional units per side
29
+ const DEFAULT_BTS_CREATE_FEE = 0.48260;
30
+ const DEFAULT_BTS_CANCEL_FEE = 0.00482;
31
+ const DEFAULT_BTS_MAKER_CREATE_FACTOR = 0.10;
32
+ const DEFAULT_TX_FEE_PRICE = 1.0;
33
+ // Weight profiles (symmetric for both sides)
34
+ // valley: heavier at edges (outer levels), lighter near center
35
+ // neutral: equal across all levels
36
+ // mountain: heavier near center, lighter at edges
37
+ const WEIGHT_PROFILES = {
38
+ valley: -0.8, // negative = invert decay → outer levels get more
39
+ neutral: 0, // flat = equal distribution
40
+ mountain: 1.5, // strong decay → inner levels get more
41
+ };
42
+ // Search grid defaults — centered around bot defaults (spread=2%, increment=0.5%)
43
+ // Spread = distance from center to first order on each side (half the bid-ask gap)
44
+ // Increment = distance between successive orders on the same side
45
+ const DEFAULT_SPREAD_VALUES = [...range(0.5, 4, 0.25), ...range(5, 12, 1)];
46
+ const DEFAULT_INCREMENT_VALUES = [...range(0.2, 2, 0.1), ...range(2.5, 8, 0.5)];
47
+ const DEFAULT_RATIO_VALUES = [1.05, 1.1, 1.15, 1.2, 1.3, 1.5, 2, 3, 5, 10];
48
+ // Reposition threshold: AMA must move this fraction from last grid center to trigger re-center
49
+ const DEFAULT_REPOSITION_PCT = 2.5;
50
+ function parseArgs() {
51
+ const args = process.argv.slice(2);
52
+ const out = {
53
+ dataPath: null,
54
+ resultsPath: null,
55
+ spreadValues: DEFAULT_SPREAD_VALUES,
56
+ incrementValues: DEFAULT_INCREMENT_VALUES,
57
+ ratioValues: DEFAULT_RATIO_VALUES,
58
+ maxOrders: DEFAULT_MAX_ORDERS,
59
+ feeRoundtripPct: DEFAULT_FEE_ROUNDTRIP_PCT,
60
+ minSpreadFactor: DEFAULT_MIN_SPREAD_FACTOR,
61
+ capital: DEFAULT_CAPITAL,
62
+ repositionPct: DEFAULT_REPOSITION_PCT,
63
+ btsCreateFee: DEFAULT_BTS_CREATE_FEE,
64
+ btsCancelFee: DEFAULT_BTS_CANCEL_FEE,
65
+ makerCreateFactor: DEFAULT_BTS_MAKER_CREATE_FACTOR,
66
+ txFeePrice: DEFAULT_TX_FEE_PRICE,
67
+ topN: 15,
68
+ };
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ const val = args[i + 1];
72
+ if (arg === '--help' || arg === '-h') {
73
+ printHelp();
74
+ process.exit(0);
75
+ }
76
+ if (!val)
77
+ continue;
78
+ switch (arg) {
79
+ case '--data':
80
+ out.dataPath = path.resolve(val);
81
+ i++;
82
+ break;
83
+ case '--results':
84
+ out.resultsPath = path.resolve(val);
85
+ i++;
86
+ break;
87
+ case '--spread':
88
+ out.spreadValues = parseListOrRange(val, DEFAULT_SPREAD_VALUES);
89
+ i++;
90
+ break;
91
+ case '--increment':
92
+ out.incrementValues = parseListOrRange(val, DEFAULT_INCREMENT_VALUES);
93
+ i++;
94
+ break;
95
+ case '--ratio':
96
+ out.ratioValues = parseListOrRange(val, DEFAULT_RATIO_VALUES);
97
+ i++;
98
+ break;
99
+ case '--max-orders':
100
+ out.maxOrders = Number(val);
101
+ i++;
102
+ break;
103
+ case '--fee':
104
+ out.feeRoundtripPct = Number(val);
105
+ i++;
106
+ break;
107
+ case '--min-spread-factor':
108
+ out.minSpreadFactor = Number(val);
109
+ i++;
110
+ break;
111
+ case '--capital':
112
+ out.capital = Number(val);
113
+ i++;
114
+ break;
115
+ case '--reposition':
116
+ out.repositionPct = Number(val);
117
+ i++;
118
+ break;
119
+ case '--bts-create-fee':
120
+ out.btsCreateFee = Number(val);
121
+ i++;
122
+ break;
123
+ case '--bts-cancel-fee':
124
+ out.btsCancelFee = Number(val);
125
+ i++;
126
+ break;
127
+ case '--maker-create-factor':
128
+ out.makerCreateFactor = Number(val);
129
+ i++;
130
+ break;
131
+ case '--tx-fee-price':
132
+ out.txFeePrice = Number(val);
133
+ i++;
134
+ break;
135
+ case '--top':
136
+ out.topN = Number(val);
137
+ i++;
138
+ break;
139
+ }
140
+ }
141
+ if (!out.dataPath) {
142
+ throw new Error('--data <path-to-lp-candles.json> is required');
143
+ }
144
+ if (!out.resultsPath) {
145
+ throw new Error('--results <path-to-optimization-results.json> is required');
146
+ }
147
+ return out;
148
+ }
149
+ function printHelp() {
150
+ console.log('AMA Sweep Backtest — persistent grid simulation with weight profiles');
151
+ console.log('');
152
+ console.log('Usage:');
153
+ console.log(' node dist/analysis/bot_fitting/backtest_ama_sweep.js [options]');
154
+ console.log('');
155
+ console.log('Options:');
156
+ console.log(' --data <path> LP candle JSON');
157
+ console.log(' --results <path> AMA optimizer results JSON');
158
+ console.log(' --spread <spec> Spread values (% ): 1:10:0.5 or 2,4,8');
159
+ console.log(' --increment <spec> Increment values (%): 0.5:5:0.5 or 1,2,3');
160
+ console.log(' --ratio <spec> Max/min ratio: 1.5,2,3,5');
161
+ console.log(' --max-orders <n> Max orders per side (default: 20, actual limited by ratio+increment)');
162
+ console.log(' --fee <pct> Round-trip fee % (default: 0.20)');
163
+ console.log(' --min-spread-factor <n> Spread >= factor * increment (default: 2.1)');
164
+ console.log(' --capital <n> Notional capital per side (default: 10000)');
165
+ console.log(' --reposition <pct> AMA drift % to trigger re-center (default: 2.5)');
166
+ console.log(' --bts-create-fee <n> BTS create fee (default: 0.48260)');
167
+ console.log(' --bts-cancel-fee <n> BTS cancel fee (default: 0.00482)');
168
+ console.log(' --maker-create-factor Maker share of create fee (default: 0.10)');
169
+ console.log(' --tx-fee-price <n> Convert BTS fees into backtest units (default: 1.0)');
170
+ console.log(' --top <n> Show top N results (default: 15)');
171
+ }
172
+ function loadAmaStrategies(resultsPath) {
173
+ const json = readJSON(resultsPath);
174
+ const amas = json.meta?.amas;
175
+ if (!amas)
176
+ throw new Error('No meta.amas found in results file.');
177
+ const out = [];
178
+ for (const [key, val] of Object.entries(amas)) {
179
+ const v = val;
180
+ if (!v || !Number.isFinite(v.er))
181
+ continue;
182
+ out.push({ id: key, name: v.label || key, er: v.er, fast: v.fast, slow: v.slow });
183
+ }
184
+ out.sort((a, b) => a.id.localeCompare(b.id));
185
+ if (out.length === 0)
186
+ throw new Error('No valid AMA strategies found');
187
+ return out;
188
+ }
189
+ // ── Order sizing with weight profiles ────────────────────────────────────────
190
+ /**
191
+ * Allocate capital across N levels using weight profile.
192
+ * weight > 0: mountain (more near center, exponential decay outward)
193
+ * weight = 0: neutral (equal)
194
+ * weight < 0: valley (more at edges, inverted decay)
195
+ *
196
+ * @param {number} totalFunds Capital for this side
197
+ * @param {number} n Number of levels
198
+ * @param {number} weight Profile weight factor
199
+ * @param {number} incrementFactor Increment as fraction (e.g. 0.02 for 2%)
200
+ * @returns {number[]} Size per level, index 0 = closest to center
201
+ */
202
+ function allocateFundsByWeights(totalFunds, n, weight, incrementFactor) {
203
+ if (n <= 0)
204
+ return [];
205
+ if (weight === 0) {
206
+ const sz = totalFunds / n;
207
+ return new Array(n).fill(sz);
208
+ }
209
+ const base = 1 - incrementFactor;
210
+ const absWeight = Math.abs(weight);
211
+ const raw = new Array(n);
212
+ for (let i = 0; i < n; i++) {
213
+ // i=0 is closest to center
214
+ raw[i] = Math.pow(base, i * absWeight);
215
+ }
216
+ if (weight < 0) {
217
+ // Valley: reverse so outer levels (high i) get the large weights
218
+ raw.reverse();
219
+ }
220
+ const total = raw.reduce((s, w) => s + w, 0) || 1;
221
+ return raw.map((w) => (w / total) * totalFunds);
222
+ }
223
+ // ── Persistent grid simulation ───────────────────────────────────────────────
224
+ /**
225
+ * Build a fresh grid centered at `center`.
226
+ *
227
+ * Grid placement (matches real bot):
228
+ * Level k (1-based):
229
+ * offset_k = spreadPct/2/100 + (k-1) * incrementPct
230
+ * buyPrice = center * (1 - offset_k)
231
+ * sellPrice = center * (1 + offset_k)
232
+ *
233
+ * So spread controls the dead zone around center (no orders within spread/2),
234
+ * and increment is the gap between successive orders on the same side.
235
+ *
236
+ * Returns arrays of buy and sell order objects with fixed chain prices and sizes.
237
+ */
238
+ function buildGrid(center, params, capitalPerSide, weightFactor) {
239
+ const { incrementPct, maxMinRatio, maxOrders, spreadPct } = params;
240
+ const minBound = center / maxMinRatio;
241
+ const maxBound = center * maxMinRatio;
242
+ const halfSpread = spreadPct / 200; // as fraction
243
+ const buys = [];
244
+ const sells = [];
245
+ // Fill as many levels as fit within ratio bounds (up to maxOrders cap)
246
+ for (let k = 1; k <= maxOrders; k++) {
247
+ const offset = halfSpread + (k - 1) * incrementPct;
248
+ if (offset >= 0.95)
249
+ continue;
250
+ const buyPrice = center * (1 - offset);
251
+ const sellPrice = center * (1 + offset);
252
+ // Bounds check — skip levels outside ratio range
253
+ if (buyPrice < minBound || sellPrice > maxBound)
254
+ continue;
255
+ buys.push({ level: k, price: buyPrice, filledBar: -1, size: 0 });
256
+ sells.push({ level: k, price: sellPrice, filledBar: -1, size: 0 });
257
+ }
258
+ // Size allocation — index 0 = closest to center
259
+ const buySizes = allocateFundsByWeights(capitalPerSide, buys.length, weightFactor, incrementPct);
260
+ const sellSizes = allocateFundsByWeights(capitalPerSide, sells.length, weightFactor, incrementPct);
261
+ buys.forEach((o, i) => { o.size = buySizes[i] || 0; });
262
+ sells.forEach((o, i) => { o.size = sellSizes[i] || 0; });
263
+ return { buys, sells };
264
+ }
265
+ function closeFilledInventoryAtPrice(openBuys, openSells, exitPrice, feeRoundtripPct) {
266
+ if (!Number.isFinite(exitPrice) || exitPrice <= 0) {
267
+ return { grossUnits: 0, profitUnits: 0, closedOrders: 0 };
268
+ }
269
+ let grossUnits = 0;
270
+ let profitUnits = 0;
271
+ let closedOrders = 0;
272
+ for (const order of openBuys.values()) {
273
+ if (!order?.filled || !Number.isFinite(order.price) || order.price <= 0)
274
+ continue;
275
+ if (!Number.isFinite(order.size) || order.size <= 0)
276
+ continue;
277
+ const grossPct = (exitPrice / order.price - 1) * 100;
278
+ const netPct = grossPct - feeRoundtripPct;
279
+ grossUnits += order.size * (grossPct / 100);
280
+ profitUnits += order.size * (netPct / 100);
281
+ closedOrders++;
282
+ }
283
+ for (const order of openSells.values()) {
284
+ if (!order?.filled || !Number.isFinite(order.price) || order.price <= 0)
285
+ continue;
286
+ if (!Number.isFinite(order.size) || order.size <= 0)
287
+ continue;
288
+ const grossPct = (order.price / exitPrice - 1) * 100;
289
+ const netPct = grossPct - feeRoundtripPct;
290
+ grossUnits += order.size * (grossPct / 100);
291
+ profitUnits += order.size * (netPct / 100);
292
+ closedOrders++;
293
+ }
294
+ return { grossUnits, profitUnits, closedOrders };
295
+ }
296
+ function countCancelableOrders(openBuys, openSells) {
297
+ let count = 0;
298
+ for (const order of openBuys.values()) {
299
+ if (!order?.filled)
300
+ count++;
301
+ }
302
+ for (const order of openSells.values()) {
303
+ if (!order?.filled)
304
+ count++;
305
+ }
306
+ return count;
307
+ }
308
+ function simulatePersistentGrid(candles, amaValues, params, weightName, weightFactor) {
309
+ const { spreadPct, incrementPct, maxMinRatio, maxOrders, feeRoundtripPct, capital, repositionThreshold, btsCreateFee, btsCancelFee, makerCreateFactor, txFeePrice } = params;
310
+ const skip = Math.max(20, Math.floor(candles.length * 0.1));
311
+ const capitalPerSide = capital;
312
+ // State
313
+ let gridCenter = amaValues[skip];
314
+ let grid = buildGrid(gridCenter, params, capitalPerSide, weightFactor);
315
+ let lastRepositionBar = skip;
316
+ // Open orders: maps level -> { price, size, bar, side }
317
+ const openBuys = new Map();
318
+ const openSells = new Map();
319
+ // Place initial orders
320
+ for (const o of grid.buys)
321
+ openBuys.set(o.level, { price: o.price, size: o.size, bar: skip });
322
+ for (const o of grid.sells)
323
+ openSells.set(o.level, { price: o.price, size: o.size, bar: skip });
324
+ let matchedPairs = 0;
325
+ let totalProfitUnits = 0; // profit in capital units (size * netPct)
326
+ let totalGrossUnits = 0;
327
+ let touchedOrders = 0;
328
+ let canceledOnReposition = 0;
329
+ let repositionCount = 0;
330
+ let peakOpenOrders = 0;
331
+ let imbalanceSum = 0;
332
+ let imbalanceSamples = 0;
333
+ let matchedOpenDurationBars = 0;
334
+ let gridAgeSumBars = 0;
335
+ let maxGridAgeBars = 0;
336
+ let centerDriftSumPct = 0;
337
+ let maxCenterDriftPct = 0;
338
+ let nearThresholdBars = 0;
339
+ let triggerDriftSumPct = 0;
340
+ let runningProfit = 0;
341
+ let peakEquityProfit = 0;
342
+ let maxDrawdown = 0;
343
+ // Track inventory risk: net exposure from filled-but-unmatched orders
344
+ let inventoryExposure = 0; // positive = long (bought), negative = short (sold)
345
+ let maxInventoryExposure = 0;
346
+ const liveBars = candles.length - skip - 1;
347
+ const ordersPerSide = grid.buys.length; // actual orders placed per side (clipped by ratio)
348
+ const makerCreateFeeBts = btsCreateFee * makerCreateFactor;
349
+ const newOrdersPerReposition = ordersPerSide * 2;
350
+ for (let i = skip + 1; i < candles.length; i++) {
351
+ const ama = amaValues[i];
352
+ const hi = candles[i].high;
353
+ const lo = candles[i].low;
354
+ const gridAgeBars = i - lastRepositionBar;
355
+ // ── Reposition check: AMA drifted too far from grid center ──────
356
+ const drift = Math.abs(ama - gridCenter) / gridCenter;
357
+ const driftPct = drift * 100;
358
+ centerDriftSumPct += driftPct;
359
+ if (driftPct > maxCenterDriftPct)
360
+ maxCenterDriftPct = driftPct;
361
+ gridAgeSumBars += gridAgeBars;
362
+ if (gridAgeBars > maxGridAgeBars)
363
+ maxGridAgeBars = gridAgeBars;
364
+ if (drift >= repositionThreshold * 0.5)
365
+ nearThresholdBars++;
366
+ if (drift >= repositionThreshold) {
367
+ triggerDriftSumPct += driftPct;
368
+ canceledOnReposition += countCancelableOrders(openBuys, openSells);
369
+ repositionCount++;
370
+ const exitPrice = Number.isFinite(ama) && ama > 0 ? ama : candles[i].close;
371
+ const forcedClose = closeFilledInventoryAtPrice(openBuys, openSells, exitPrice, feeRoundtripPct);
372
+ totalGrossUnits += forcedClose.grossUnits;
373
+ totalProfitUnits += forcedClose.profitUnits;
374
+ runningProfit += forcedClose.profitUnits;
375
+ // Unmatched inventory is realized on reposition (position closed at market)
376
+ inventoryExposure = 0;
377
+ openBuys.clear();
378
+ openSells.clear();
379
+ // Re-center grid
380
+ gridCenter = ama;
381
+ grid = buildGrid(gridCenter, params, capitalPerSide, weightFactor);
382
+ lastRepositionBar = i;
383
+ for (const o of grid.buys)
384
+ openBuys.set(o.level, { price: o.price, size: o.size, bar: i });
385
+ for (const o of grid.sells)
386
+ openSells.set(o.level, { price: o.price, size: o.size, bar: i });
387
+ }
388
+ // ── Track imbalance & peak ──────────────────────────────────────
389
+ const currentOpen = openBuys.size + openSells.size;
390
+ if (currentOpen > peakOpenOrders)
391
+ peakOpenOrders = currentOpen;
392
+ imbalanceSum += Math.abs(openBuys.size - openSells.size);
393
+ imbalanceSamples++;
394
+ // ── Check fills on PERSISTENT orders (fixed chain prices) ───────
395
+ // Only check UNFILLED orders (skip already-filled pending match)
396
+ const filledBuysThisBar = [];
397
+ const filledSellsThisBar = [];
398
+ for (const [lvl, order] of openBuys) {
399
+ if (!order.filled && lo <= order.price) {
400
+ filledBuysThisBar.push({ lvl, order });
401
+ }
402
+ }
403
+ for (const [lvl, order] of openSells) {
404
+ if (!order.filled && hi >= order.price) {
405
+ filledSellsThisBar.push({ lvl, order });
406
+ }
407
+ }
408
+ // Process buy fills
409
+ for (const fb of filledBuysThisBar) {
410
+ touchedOrders++;
411
+ inventoryExposure += fb.order.size; // bought → long exposure
412
+ // Mark as filled, waiting for paired sell at same level
413
+ openBuys.set(fb.lvl, { ...fb.order, filled: true, filledBar: i, side: 'buy' });
414
+ }
415
+ // Process sell fills
416
+ for (const fs of filledSellsThisBar) {
417
+ touchedOrders++;
418
+ inventoryExposure -= fs.order.size; // sold → short exposure
419
+ // Mark as filled, waiting for paired buy at same level
420
+ openSells.set(fs.lvl, { ...fs.order, filled: true, filledBar: i, side: 'sell' });
421
+ }
422
+ // Match completed pairs: both buy AND sell at same level are filled
423
+ for (let k = 1; k <= maxOrders; k++) {
424
+ const buyEntry = openBuys.get(k);
425
+ const sellEntry = openSells.get(k);
426
+ if (!buyEntry?.filled || !sellEntry?.filled)
427
+ continue;
428
+ // Matched pair!
429
+ openBuys.delete(k);
430
+ openSells.delete(k);
431
+ matchedPairs++;
432
+ const duration = Math.abs(sellEntry.filledBar - buyEntry.filledBar);
433
+ matchedOpenDurationBars += duration;
434
+ const avgSize = (buyEntry.size + sellEntry.size) / 2;
435
+ const pairGrossPct = (sellEntry.price / buyEntry.price - 1) * 100;
436
+ const pairNetPct = pairGrossPct - feeRoundtripPct;
437
+ const profitUnits = avgSize * (pairNetPct / 100);
438
+ totalGrossUnits += avgSize * (pairGrossPct / 100);
439
+ totalProfitUnits += profitUnits;
440
+ runningProfit += profitUnits;
441
+ }
442
+ // Track max inventory exposure
443
+ const absExposure = Math.abs(inventoryExposure);
444
+ if (absExposure > maxInventoryExposure)
445
+ maxInventoryExposure = absExposure;
446
+ // ── Drawdown tracking ───────────────────────────────────────────
447
+ const markPrice = Number.isFinite(candles[i].close) && candles[i].close > 0
448
+ ? candles[i].close
449
+ : ((Number.isFinite(ama) && ama > 0) ? ama : gridCenter);
450
+ const unrealized = closeFilledInventoryAtPrice(openBuys, openSells, markPrice, feeRoundtripPct).profitUnits;
451
+ const equityProfit = runningProfit + unrealized;
452
+ if (equityProfit > peakEquityProfit)
453
+ peakEquityProfit = equityProfit;
454
+ const dd = peakEquityProfit - equityProfit;
455
+ if (dd > maxDrawdown)
456
+ maxDrawdown = dd;
457
+ }
458
+ const fillEfficiency = touchedOrders > 0 ? (matchedPairs / touchedOrders) * 100 : 0;
459
+ const pairsPerDay = liveBars > 0 ? matchedPairs / (liveBars / 24) : 0;
460
+ const avgOpenDurationBars = matchedPairs > 0 ? matchedOpenDurationBars / matchedPairs : 0;
461
+ const avgImbalance = imbalanceSamples > 0 ? imbalanceSum / imbalanceSamples : 0;
462
+ const avgProfitPerPair = matchedPairs > 0 ? totalProfitUnits / matchedPairs : 0;
463
+ const profitPerCapital = capital > 0 ? totalProfitUnits / (capital * 2) : 0; // total capital = 2 sides
464
+ const maxDrawdownPct = capital > 0 ? (maxDrawdown / (capital * 2)) * 100 : 0;
465
+ const avgGridAgeBars = liveBars > 0 ? gridAgeSumBars / liveBars : 0;
466
+ const avgCenterDriftPct = liveBars > 0 ? centerDriftSumPct / liveBars : 0;
467
+ const nearThresholdBarsPct = liveBars > 0 ? (nearThresholdBars / liveBars) * 100 : 0;
468
+ const avgTriggerDriftPct = repositionCount > 0 ? triggerDriftSumPct / repositionCount : 0;
469
+ const avgCancelOrdersPerReposition = repositionCount > 0 ? canceledOnReposition / repositionCount : 0;
470
+ const estimatedFeePerRepositionBts = newOrdersPerReposition * makerCreateFeeBts + avgCancelOrdersPerReposition * btsCancelFee;
471
+ const totalRepositionFeesBts = repositionCount * estimatedFeePerRepositionBts;
472
+ const feePerDayBts = liveBars > 0 ? totalRepositionFeesBts / (liveBars / 24) : 0;
473
+ const totalRepositionFeeUnits = totalRepositionFeesBts * txFeePrice;
474
+ const netProfitUnits = totalProfitUnits - totalRepositionFeeUnits;
475
+ const netProfitPerCapital = capital > 0 ? netProfitUnits / (capital * 2) : 0;
476
+ // Score: profit per capital scaled by activity (log pairs to avoid pure frequency chasing)
477
+ const activityBonus = matchedPairs > 0 ? Math.log10(matchedPairs) : null;
478
+ const grossScore = activityBonus == null ? -Infinity : (profitPerCapital * 100 * activityBonus - maxDrawdownPct * 0.5);
479
+ const netScore = activityBonus == null ? -Infinity : (netProfitPerCapital * 100 * activityBonus - maxDrawdownPct * 0.5);
480
+ const score = netScore;
481
+ return {
482
+ weightName,
483
+ spreadPct,
484
+ incrementPct: incrementPct * 100, // store as %
485
+ maxMinRatio,
486
+ matchedPairs,
487
+ touchedOrders,
488
+ fillEfficiency,
489
+ totalProfitUnits,
490
+ totalGrossUnits,
491
+ profitPerCapital,
492
+ pairsPerDay,
493
+ avgOpenDurationBars,
494
+ avgProfitPerPair,
495
+ peakOpenOrders,
496
+ avgImbalance,
497
+ canceledOnReposition,
498
+ repositionCount,
499
+ maxDrawdown,
500
+ maxDrawdownPct,
501
+ maxInventoryExposure,
502
+ avgGridAgeBars,
503
+ maxGridAgeBars,
504
+ avgCenterDriftPct,
505
+ maxCenterDriftPct,
506
+ nearThresholdBarsPct,
507
+ avgTriggerDriftPct,
508
+ makerCreateFeeBts,
509
+ btsCancelFee,
510
+ avgCancelOrdersPerReposition,
511
+ estimatedFeePerRepositionBts,
512
+ totalRepositionFeesBts,
513
+ feePerDayBts,
514
+ netProfitUnits,
515
+ netProfitPerCapital,
516
+ ordersPerSide,
517
+ grossScore,
518
+ netScore,
519
+ score,
520
+ };
521
+ }
522
+ // ── Per-AMA sweep logic (runs in main thread or worker) ─────────────────────
523
+ function sweepOneAma(strategy, candles, closes, weightEntries, cfg) {
524
+ const amaValues = calculateAMA(closes, { erPeriod: strategy.er, fastPeriod: strategy.fast, slowPeriod: strategy.slow });
525
+ let best = null;
526
+ const top5 = [];
527
+ const allSims = [];
528
+ let evaluated = 0;
529
+ const minSpreadFactor = Number.isFinite(cfg.minSpreadFactor) && cfg.minSpreadFactor > 0 ? cfg.minSpreadFactor : null;
530
+ for (const spreadPct of cfg.spreadValues) {
531
+ for (const incrementPctRaw of cfg.incrementValues) {
532
+ if (spreadPct < cfg.feeRoundtripPct + 0.01)
533
+ continue;
534
+ if (minSpreadFactor != null && spreadPct < (incrementPctRaw * minSpreadFactor))
535
+ continue;
536
+ for (const maxMinRatio of cfg.ratioValues) {
537
+ for (const [weightName, weightFactor] of weightEntries) {
538
+ evaluated++;
539
+ const sim = simulatePersistentGrid(candles, amaValues, {
540
+ spreadPct,
541
+ incrementPct: incrementPctRaw / 100,
542
+ maxMinRatio,
543
+ maxOrders: cfg.maxOrders,
544
+ feeRoundtripPct: cfg.feeRoundtripPct,
545
+ capital: cfg.capital,
546
+ repositionThreshold: cfg.repositionPct / 100,
547
+ btsCreateFee: cfg.btsCreateFee,
548
+ btsCancelFee: cfg.btsCancelFee,
549
+ makerCreateFactor: cfg.makerCreateFactor,
550
+ txFeePrice: cfg.txFeePrice,
551
+ }, weightName, weightFactor);
552
+ if (!best || sim.score > best.score)
553
+ best = sim;
554
+ if (sim.matchedPairs > 0) {
555
+ const t5key = `${sim.spreadPct}|${sim.incrementPct}|${sim.maxMinRatio}|${sim.weightName}`;
556
+ const existing = top5.findIndex((t) => `${t.spreadPct}|${t.incrementPct}|${t.maxMinRatio}|${t.weightName}` === t5key);
557
+ if (existing < 0) {
558
+ top5.push(sim);
559
+ top5.sort((a, b) => b.score - a.score);
560
+ if (top5.length > 5)
561
+ top5.length = 5;
562
+ }
563
+ allSims.push(sim);
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ return { strategy, best, top5, allSims, evaluated };
570
+ }
571
+ // ── Worker thread handler ───────────────────────────────────────────────────
572
+ if (!isMainThread) {
573
+ const { strategy, candles, closes, weightEntries, cfg } = workerData;
574
+ const result = sweepOneAma(strategy, candles, closes, weightEntries, cfg);
575
+ parentPort.postMessage(result);
576
+ process.exit(0);
577
+ }
578
+ // ── Parallel dispatch (main thread) ─────────────────────────────────────────
579
+ function runParallel(strategies, candles, closes, weightEntries, cfg) {
580
+ const numCpus = Math.min(os.cpus().length, strategies.length);
581
+ console.log(` Workers: ${numCpus} threads (${os.cpus().length} CPUs available)\n`);
582
+ return Promise.all(strategies.map((strategy) => {
583
+ return new Promise((resolve, reject) => {
584
+ const worker = new Worker(__filename, {
585
+ workerData: { strategy, candles, closes, weightEntries, cfg },
586
+ });
587
+ worker.on('message', resolve);
588
+ worker.on('error', reject);
589
+ worker.on('exit', (code) => {
590
+ if (code !== 0)
591
+ reject(new Error(`Worker exited with code ${code}`));
592
+ });
593
+ });
594
+ }));
595
+ }
596
+ // ── Main ────────────────────────────────────────────────────────────────────
597
+ async function run() {
598
+ const cfg = parseArgs();
599
+ const loaded = loadLpData(cfg.dataPath);
600
+ const candles = loaded.candles;
601
+ const closes = candles.map((c) => c.close);
602
+ const strategies = loadAmaStrategies(cfg.resultsPath);
603
+ const weightEntries = Object.entries(WEIGHT_PROFILES);
604
+ const totalCombos = cfg.spreadValues.length * cfg.incrementValues.length *
605
+ cfg.ratioValues.length * weightEntries.length;
606
+ console.log('================================================================================');
607
+ console.log(' AMA SWEEP BACKTEST — persistent grid + weight profiles');
608
+ console.log('================================================================================');
609
+ console.log(` Data: ${path.basename(cfg.dataPath)} (${candles.length} candles, ~${(candles.length / 24).toFixed(0)} days)`);
610
+ console.log(` AMAs: ${strategies.map((s) => s.id).join(', ')}`);
611
+ console.log(` Weights: ${weightEntries.map(([n, w]) => `${n}(${w})`).join(', ')}`);
612
+ console.log(` Spread: ${cfg.spreadValues[0]}..${cfg.spreadValues[cfg.spreadValues.length - 1]}% (${cfg.spreadValues.length})`);
613
+ console.log(` Increment: ${cfg.incrementValues[0]}..${cfg.incrementValues[cfg.incrementValues.length - 1]}% (${cfg.incrementValues.length})`);
614
+ console.log(` Ratio: ${cfg.ratioValues[0]}..${cfg.ratioValues[cfg.ratioValues.length - 1]} (${cfg.ratioValues.length})`);
615
+ console.log(` Max orders: ${cfg.maxOrders}/side (actual count from ratio+increment) | Capital: ${cfg.capital}/side | Fee: ${cfg.feeRoundtripPct}%`);
616
+ console.log(` Spread floor: > fee (${cfg.feeRoundtripPct}%)`);
617
+ console.log(` Reposition: ${cfg.repositionPct}% AMA drift from grid center`);
618
+ console.log(` Tx model: create=${fmt(cfg.btsCreateFee, 5)} BTS, cancel=${fmt(cfg.btsCancelFee, 5)} BTS, maker=${fmt(cfg.makerCreateFactor * 100, 1)}%, 1 BTS=${fmt(cfg.txFeePrice, 2)} units`);
619
+ console.log(` Combos/AMA: ${totalCombos} | Total: ${totalCombos * strategies.length}\n`);
620
+ // ── Run AMA sweeps in parallel (one worker per AMA strategy) ──────
621
+ const byAma = [];
622
+ const allResults = [];
623
+ const workerResults = await runParallel(strategies, candles, closes, weightEntries, cfg);
624
+ for (const wr of workerResults) {
625
+ byAma.push(wr);
626
+ for (const sim of wr.allSims) {
627
+ allResults.push({ strategy: wr.strategy, sim });
628
+ }
629
+ const b = wr.best;
630
+ const bestLabel = b && b.matchedPairs > 0
631
+ ? `score=${fmt(b.score, 1)} pairs=${b.matchedPairs} net/cap=${fmt(b.netProfitPerCapital * 100, 1)}% gross=${fmt(b.profitPerCapital * 100, 1)}%`
632
+ : 'no fills';
633
+ process.stdout.write(` ${wr.strategy.id} (ER=${wr.strategy.er}, F=${wr.strategy.fast}, S=${wr.strategy.slow}): ${wr.evaluated} combos, ${bestLabel}\n`);
634
+ }
635
+ // ── Per-AMA best ────────────────────────────────────────────────────────
636
+ console.log('\n================================================================================');
637
+ console.log(' BEST PARAMS PER AMA');
638
+ console.log('================================================================================');
639
+ console.log('AMA | wt | spr% | inc% | ratio | nOrd | pairs | net/cap | gross/cap | drift | fee/d | score');
640
+ console.log('------+-------+-------+-------+-------+------+--------+---------+-----------+-------+-------+------');
641
+ for (const row of byAma) {
642
+ const b = row.best;
643
+ if (!b || b.matchedPairs === 0) {
644
+ console.log(`${row.strategy.id.padEnd(5)} | (no fills)`);
645
+ continue;
646
+ }
647
+ console.log(`${row.strategy.id.padEnd(5)} | ` +
648
+ `${b.weightName.padEnd(5)} | ` +
649
+ `${fmt(b.spreadPct, 1).padStart(5)} | ` +
650
+ `${fmt(b.incrementPct, 1).padStart(5)} | ` +
651
+ `${fmt(b.maxMinRatio, 2).padStart(5)} | ` +
652
+ `${String(b.ordersPerSide).padStart(4)} | ` +
653
+ `${String(b.matchedPairs).padStart(6)} | ` +
654
+ `${fmt(b.netProfitPerCapital * 100, 2).padStart(7)}% | ` +
655
+ `${fmt(b.profitPerCapital * 100, 2).padStart(10)}% | ` +
656
+ `${fmt(b.avgCenterDriftPct, 2).padStart(5)}% | ` +
657
+ `${fmt(b.feePerDayBts, 2).padStart(5)} | ` +
658
+ `${fmt(b.score, 1).padStart(6)}`);
659
+ }
660
+ // ── Top 5 per AMA ───────────────────────────────────────────────────────
661
+ for (const row of byAma) {
662
+ if (row.top5.length === 0)
663
+ continue;
664
+ console.log(`\n ${row.strategy.id} — Top 5:`);
665
+ console.log(' # | wt | spr% | inc% | ratio | nOrd | pairs | net/cap | drift | gAge | fee/d | score');
666
+ console.log(' --+-------+-------+-------+-------+------+--------+---------+-------+------+-------+------');
667
+ row.top5.forEach((b, idx) => {
668
+ console.log(` ${idx + 1} | ` +
669
+ `${b.weightName.padEnd(5)} | ` +
670
+ `${fmt(b.spreadPct, 1).padStart(5)} | ` +
671
+ `${fmt(b.incrementPct, 1).padStart(5)} | ` +
672
+ `${fmt(b.maxMinRatio, 2).padStart(5)} | ` +
673
+ `${String(b.ordersPerSide).padStart(4)} | ` +
674
+ `${String(b.matchedPairs).padStart(6)} | ` +
675
+ `${fmt(b.netProfitPerCapital * 100, 2).padStart(7)}% | ` +
676
+ `${fmt(b.avgCenterDriftPct, 2).padStart(5)}% | ` +
677
+ `${fmt(b.avgGridAgeBars, 0).padStart(4)} | ` +
678
+ `${fmt(b.feePerDayBts, 2).padStart(5)} | ` +
679
+ `${fmt(b.score, 1).padStart(6)}`);
680
+ });
681
+ }
682
+ // ── Global ranking (deduplicated) ───────────────────────────────────────
683
+ allResults.sort((a, b) => b.sim.score - a.sim.score);
684
+ const seen = new Set();
685
+ const deduped = [];
686
+ for (const r of allResults) {
687
+ const key = `${r.strategy.id}|${r.sim.spreadPct}|${r.sim.incrementPct}|${r.sim.maxMinRatio}|${r.sim.weightName}`;
688
+ if (seen.has(key))
689
+ continue;
690
+ seen.add(key);
691
+ deduped.push(r);
692
+ }
693
+ if (deduped.length === 0) {
694
+ console.log('\n NO CONFIGURATIONS PRODUCED ANY MATCHED PAIRS.');
695
+ return;
696
+ }
697
+ const topN = Math.min(cfg.topN, deduped.length);
698
+ console.log(`\n================================================================================`);
699
+ console.log(` GLOBAL TOP ${topN}`);
700
+ console.log('================================================================================');
701
+ console.log('# | AMA | wt | spr% | inc% | ratio | nOrd | pairs | net/cap | drift | fee/d | score');
702
+ console.log('---+-------+-------+-------+-------+-------+------+--------+---------+-------+-------+------');
703
+ for (let i = 0; i < topN; i++) {
704
+ const { strategy, sim } = deduped[i];
705
+ console.log(`${String(i + 1).padStart(2)} | ` +
706
+ `${strategy.id.padEnd(5)} | ` +
707
+ `${sim.weightName.padEnd(5)} | ` +
708
+ `${fmt(sim.spreadPct, 1).padStart(5)} | ` +
709
+ `${fmt(sim.incrementPct, 1).padStart(5)} | ` +
710
+ `${fmt(sim.maxMinRatio, 2).padStart(5)} | ` +
711
+ `${String(sim.ordersPerSide).padStart(4)} | ` +
712
+ `${String(sim.matchedPairs).padStart(6)} | ` +
713
+ `${fmt(sim.netProfitPerCapital * 100, 2).padStart(7)}% | ` +
714
+ `${fmt(sim.avgCenterDriftPct, 2).padStart(5)}% | ` +
715
+ `${fmt(sim.feePerDayBts, 2).padStart(5)} | ` +
716
+ `${fmt(sim.score, 1).padStart(6)}`);
717
+ }
718
+ // ── Winner ──────────────────────────────────────────────────────────────
719
+ const winner = deduped[0];
720
+ console.log(`\n================================================================================`);
721
+ console.log(` WINNER`);
722
+ console.log('================================================================================');
723
+ console.log(` AMA: ${winner.strategy.id} (ER=${winner.strategy.er}, Fast=${winner.strategy.fast}, Slow=${winner.strategy.slow})`);
724
+ console.log(` Weight: ${winner.sim.weightName} (${WEIGHT_PROFILES[winner.sim.weightName]})`);
725
+ console.log(` Spread: ${fmt(winner.sim.spreadPct, 1)}%`);
726
+ console.log(` Increment: ${fmt(winner.sim.incrementPct, 1)}%`);
727
+ console.log(` Ratio: ${fmt(winner.sim.maxMinRatio, 1)}x`);
728
+ console.log(` Pairs: ${winner.sim.matchedPairs} (${fmt(winner.sim.pairsPerDay, 2)}/day)`);
729
+ console.log(` Fill eff: ${fmt(winner.sim.fillEfficiency, 1)}%`);
730
+ console.log(` Profit/cap: gross ${fmt(winner.sim.profitPerCapital * 100, 2)}% | net ${fmt(winner.sim.netProfitPerCapital * 100, 2)}%`);
731
+ console.log(` Net profit: ${fmt(winner.sim.netProfitUnits, 0)} units after ${fmt(winner.sim.totalRepositionFeesBts, 1)} BTS reposition fees`);
732
+ console.log(` Avg/pair: ${fmt(winner.sim.avgProfitPerPair, 2)} units`);
733
+ console.log(` Max DD: ${fmt(winner.sim.maxDrawdownPct, 2)}%`);
734
+ console.log(` Repositions: ${winner.sim.repositionCount}`);
735
+ console.log(` Grid age: avg ${fmt(winner.sim.avgGridAgeBars, 1)} bars | max ${fmt(winner.sim.maxGridAgeBars, 0)} bars`);
736
+ console.log(` Drift: avg ${fmt(winner.sim.avgCenterDriftPct, 2)}% | max ${fmt(winner.sim.maxCenterDriftPct, 2)}% | near-threshold ${fmt(winner.sim.nearThresholdBarsPct, 1)}%`);
737
+ console.log(` Tx burn: ${fmt(winner.sim.estimatedFeePerRepositionBts, 4)} BTS/reposition | ${fmt(winner.sim.feePerDayBts, 2)} BTS/day`);
738
+ console.log(` Score: ${fmt(winner.sim.score, 2)}`);
739
+ // ── Save JSON ───────────────────────────────────────────────────────────
740
+ const outName = `ama_sweep_results_${path.basename(cfg.dataPath, '.json')}.json`;
741
+ const outPath = path.join(__dirname, outName);
742
+ writeJSON(outPath, {
743
+ meta: {
744
+ generatedAt: new Date().toISOString(),
745
+ dataPath: path.relative(process.cwd(), cfg.dataPath),
746
+ resultsPath: path.relative(process.cwd(), cfg.resultsPath),
747
+ candles: candles.length,
748
+ days: candles.length / 24,
749
+ maxOrders: cfg.maxOrders,
750
+ capitalPerSide: cfg.capital,
751
+ feeRoundtripPct: cfg.feeRoundtripPct,
752
+ repositionPct: cfg.repositionPct,
753
+ btsCreateFee: cfg.btsCreateFee,
754
+ btsCancelFee: cfg.btsCancelFee,
755
+ makerCreateFactor: cfg.makerCreateFactor,
756
+ txFeePrice: cfg.txFeePrice,
757
+ weightProfiles: WEIGHT_PROFILES,
758
+ search: {
759
+ spreadValues: cfg.spreadValues,
760
+ incrementValues: cfg.incrementValues,
761
+ ratioValues: cfg.ratioValues,
762
+ minSpreadFactor: cfg.minSpreadFactor,
763
+ combosPerAma: totalCombos,
764
+ totalCombos: totalCombos * strategies.length,
765
+ },
766
+ scoring: 'netProfitPerCapital * 100 * log10(max(1, matchedPairs)) - maxDrawdownPct * 0.5',
767
+ },
768
+ strategies,
769
+ perAma: byAma.map((row) => ({
770
+ strategy: row.strategy,
771
+ evaluated: row.evaluated,
772
+ best: row.best,
773
+ top5: row.top5,
774
+ })),
775
+ globalTop: deduped.slice(0, Math.max(cfg.topN, 20)).map(({ strategy, sim }) => ({
776
+ ama: strategy.id,
777
+ amaParams: { er: strategy.er, fast: strategy.fast, slow: strategy.slow },
778
+ ...sim,
779
+ })),
780
+ winner: {
781
+ ama: winner.strategy.id,
782
+ amaParams: { er: winner.strategy.er, fast: winner.strategy.fast, slow: winner.strategy.slow },
783
+ ...winner.sim,
784
+ },
785
+ });
786
+ console.log(`\nSaved: ${path.relative(process.cwd(), outPath)}`);
787
+ }
788
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
789
+ run().catch((err) => { console.error(err); process.exit(1); });
790
+ }
791
+ export { WEIGHT_PROFILES, allocateFundsByWeights, buildGrid, closeFilledInventoryAtPrice, countCancelableOrders, simulatePersistentGrid, sweepOneAma };
792
+ //# sourceMappingURL=backtest_ama_sweep.js.map