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.
- package/CHANGELOG.md +6 -0
- package/analysis/README.md +43 -43
- package/analysis/ama_fitting/README.md +8 -8
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/bot_fitting/README.md +4 -4
- package/analysis/bot_fitting/backtest_ama_sweep.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +11 -11
- package/analysis/tradingview/README.md +4 -4
- package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +2 -2
- package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +1 -1
- package/analysis/trend_detection/package.json +1 -1
- package/claw/README.md +45 -45
- package/claw/docs/AI_BOT_LIBRARY_API.md +6 -6
- package/claw/examples/short_mpa_bts_strategy.ts +3 -3
- package/claw/package.json +21 -21
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/scripts/claw_bridge.ts +2 -2
- package/claw/scripts/claw_skill_md.ts +1 -1
- package/claw/skills/launcher-ops/references/launcher-workflow.md +5 -5
- package/claw/skills/margin-trading/references/honest-asset-list.md +1 -1
- package/claw/skills/memu-memory/SKILL.md +3 -3
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts +3 -0
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.js +148 -0
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.js.map +1 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts +12 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +389 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -0
- package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts +3 -0
- package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/calibrate_convergence_er.js +200 -0
- package/dist/analysis/ama_fitting/calibrate_convergence_er.js.map +1 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts +3 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +213 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -0
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts +25 -0
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js +247 -0
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js.map +1 -0
- package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts +35 -0
- package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts.map +1 -0
- package/dist/analysis/ama_fitting/optimizer_high_resolution.js +782 -0
- package/dist/analysis/ama_fitting/optimizer_high_resolution.js.map +1 -0
- package/dist/analysis/analyze_derivatives.d.ts +86 -0
- package/dist/analysis/analyze_derivatives.d.ts.map +1 -0
- package/dist/analysis/analyze_derivatives.js +285 -0
- package/dist/analysis/analyze_derivatives.js.map +1 -0
- package/dist/analysis/analyze_dynamic_weight.d.ts +14 -0
- package/dist/analysis/analyze_dynamic_weight.d.ts.map +1 -0
- package/dist/analysis/analyze_dynamic_weight.js +197 -0
- package/dist/analysis/analyze_dynamic_weight.js.map +1 -0
- package/dist/analysis/analyze_kalman.d.ts +13 -0
- package/dist/analysis/analyze_kalman.d.ts.map +1 -0
- package/dist/analysis/analyze_kalman.js +118 -0
- package/dist/analysis/analyze_kalman.js.map +1 -0
- package/dist/analysis/analyze_regime.d.ts +20 -0
- package/dist/analysis/analyze_regime.d.ts.map +1 -0
- package/dist/analysis/analyze_regime.js +139 -0
- package/dist/analysis/analyze_regime.js.map +1 -0
- package/dist/analysis/analyze_regime_windows.d.ts +19 -0
- package/dist/analysis/analyze_regime_windows.d.ts.map +1 -0
- package/dist/analysis/analyze_regime_windows.js +414 -0
- package/dist/analysis/analyze_regime_windows.js.map +1 -0
- package/dist/analysis/analyze_risk_profile.d.ts +3 -0
- package/dist/analysis/analyze_risk_profile.d.ts.map +1 -0
- package/dist/analysis/analyze_risk_profile.js +186 -0
- package/dist/analysis/analyze_risk_profile.js.map +1 -0
- package/dist/analysis/analyze_trade_heatmap.d.ts +3 -0
- package/dist/analysis/analyze_trade_heatmap.d.ts.map +1 -0
- package/dist/analysis/analyze_trade_heatmap.js +358 -0
- package/dist/analysis/analyze_trade_heatmap.js.map +1 -0
- package/dist/analysis/analyze_volatility.d.ts +22 -0
- package/dist/analysis/analyze_volatility.d.ts.map +1 -0
- package/dist/analysis/analyze_volatility.js +165 -0
- package/dist/analysis/analyze_volatility.js.map +1 -0
- package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts +91 -0
- package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts.map +1 -0
- package/dist/analysis/bot_fitting/backtest_ama_sweep.js +792 -0
- package/dist/analysis/bot_fitting/backtest_ama_sweep.js.map +1 -0
- package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts +2 -0
- package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts.map +1 -0
- package/dist/analysis/bot_fitting/backtest_bot_fitting.js +380 -0
- package/dist/analysis/bot_fitting/backtest_bot_fitting.js.map +1 -0
- package/dist/analysis/bot_fitting/shared_utils.d.ts +26 -0
- package/dist/analysis/bot_fitting/shared_utils.d.ts.map +1 -0
- package/dist/analysis/bot_fitting/shared_utils.js +42 -0
- package/dist/analysis/bot_fitting/shared_utils.js.map +1 -0
- package/dist/analysis/bot_key_utils.d.ts +11 -0
- package/dist/analysis/bot_key_utils.d.ts.map +1 -0
- package/dist/analysis/bot_key_utils.js +153 -0
- package/dist/analysis/bot_key_utils.js.map +1 -0
- package/dist/analysis/bot_usage/discover_bot_accounts.d.ts +3 -0
- package/dist/analysis/bot_usage/discover_bot_accounts.d.ts.map +1 -0
- package/dist/analysis/bot_usage/discover_bot_accounts.js +499 -0
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +531 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.js +399 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -0
- package/dist/analysis/chart_css.d.ts +17 -0
- package/dist/analysis/chart_css.d.ts.map +1 -0
- package/dist/analysis/chart_css.js +49 -0
- package/dist/analysis/chart_css.js.map +1 -0
- package/dist/analysis/chart_ui.d.ts +45 -0
- package/dist/analysis/chart_ui.d.ts.map +1 -0
- package/dist/analysis/chart_ui.js +97 -0
- package/dist/analysis/chart_ui.js.map +1 -0
- package/dist/analysis/chart_utils.d.ts +27 -0
- package/dist/analysis/chart_utils.d.ts.map +1 -0
- package/dist/analysis/chart_utils.js +153 -0
- package/dist/analysis/chart_utils.js.map +1 -0
- package/dist/analysis/derivative_chart_generator.d.ts +12 -0
- package/dist/analysis/derivative_chart_generator.d.ts.map +1 -0
- package/dist/analysis/derivative_chart_generator.js +891 -0
- package/dist/analysis/derivative_chart_generator.js.map +1 -0
- package/dist/analysis/math_utils.d.ts +22 -0
- package/dist/analysis/math_utils.d.ts.map +1 -0
- package/dist/analysis/math_utils.js +43 -0
- package/dist/analysis/math_utils.js.map +1 -0
- package/dist/analysis/price_sources.d.ts +40 -0
- package/dist/analysis/price_sources.d.ts.map +1 -0
- package/dist/analysis/price_sources.js +94 -0
- package/dist/analysis/price_sources.js.map +1 -0
- package/dist/analysis/resolve_source.d.ts +25 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -0
- package/dist/analysis/resolve_source.js +69 -0
- package/dist/analysis/resolve_source.js.map +1 -0
- package/dist/analysis/trade_profitability.d.ts +115 -0
- package/dist/analysis/trade_profitability.d.ts.map +1 -0
- package/dist/analysis/trade_profitability.js +1240 -0
- package/dist/analysis/trade_profitability.js.map +1 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +31 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -0
- package/dist/analysis/tradingview/analyze_tradingview.js +146 -0
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -0
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -0
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -0
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1714 -0
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -0
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts +247 -0
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +1 -0
- package/dist/analysis/trend_detection/derivative_analyzer.js +901 -0
- package/dist/analysis/trend_detection/derivative_analyzer.js.map +1 -0
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts +3 -0
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -0
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +1361 -0
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -0
- package/dist/analysis/trend_detection/hurst_analyzer.d.ts +6 -0
- package/dist/analysis/trend_detection/hurst_analyzer.d.ts.map +1 -0
- package/dist/analysis/trend_detection/hurst_analyzer.js +7 -0
- package/dist/analysis/trend_detection/hurst_analyzer.js.map +1 -0
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts +3 -0
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -0
- package/dist/analysis/trend_detection/kalman_chart_generator.js +360 -0
- package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -0
- package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts +8 -0
- package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts.map +1 -0
- package/dist/analysis/trend_detection/kalman_trend_analyzer.js +8 -0
- package/dist/analysis/trend_detection/kalman_trend_analyzer.js.map +1 -0
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts +6 -0
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts.map +1 -0
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.js +7 -0
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.js.map +1 -0
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts +6 -0
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts.map +1 -0
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.js +7 -0
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.js.map +1 -0
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts +3 -0
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -0
- package/dist/analysis/trend_detection/regime_chart_generator.js +310 -0
- package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -0
- package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts +2 -0
- package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts.map +1 -0
- package/dist/analysis/trend_detection/tests/test_kalman_trend.js +128 -0
- package/dist/analysis/trend_detection/tests/test_kalman_trend.js.map +1 -0
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts +2 -0
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts.map +1 -0
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js +37 -0
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js.map +1 -0
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts +3 -0
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -0
- package/dist/analysis/trend_detection/volatility_chart_generator.js +646 -0
- package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -0
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +1 -1
- package/dist/market_adapter/ama_signal_runner.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/scripts/generate_lp_chart.js +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +5 -4
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
- package/docs/GRID_RECALCULATION.md +2 -2
- package/docs/README.md +1 -1
- package/market_adapter/README.md +23 -23
- package/package.json +17 -21
- package/scripts/README.md +12 -12
- package/scripts/bots +5 -7
- package/scripts/clear-market-adapter.sh +1 -1
- package/scripts/dexbot +5 -7
- package/scripts/keys +5 -7
- package/scripts/pm2 +5 -7
- package/scripts/unlock +5 -7
- package/scripts/update.js +6 -7
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
6
|
+
import { calculateAMA } from '../../market_adapter/core/strategies/ama.js';
|
|
7
|
+
import { toIntervalLabel } from '../../market_adapter/interval_utils.js';
|
|
8
|
+
import { generateHTML } from '../../market_adapter/lp_chart_core.js';
|
|
9
|
+
import { PATHS } from '../../modules/paths.js';
|
|
10
|
+
import { ensureDir } from '../../modules/order/utils/system.js';
|
|
11
|
+
import { range } from '../math_utils.js';
|
|
12
|
+
import { getStorage } from '../../modules/storage/index.js';
|
|
13
|
+
const { readJSON, writeJSON } = getStorage();
|
|
14
|
+
'use strict';
|
|
15
|
+
import { loadLpDataFile, } from '../../market_adapter/lp_chart_runner.js';
|
|
16
|
+
const AMA_PROFILES_FILE = PATHS.PROFILES.MARKET_PROFILES_JSON;
|
|
17
|
+
/**
|
|
18
|
+
* AMA GEOMETRIC OPTIMIZER
|
|
19
|
+
*
|
|
20
|
+
* Finds AMA parameters (ER, Fast, Slow) using geometric metrics only.
|
|
21
|
+
*
|
|
22
|
+
* Objective flow:
|
|
23
|
+
* 1. Compute a per-AMA max-distance cap from the max candle deviation percentile.
|
|
24
|
+
* 2. Keep only candidates at or below that cap.
|
|
25
|
+
* 3. Select the candidate with the lowest additive linear score.
|
|
26
|
+
*
|
|
27
|
+
* Distance to price is still tracked for reporting.
|
|
28
|
+
* Winner selection uses an additive tradeoff between AMA movement and
|
|
29
|
+
* AMA-to-price distance, so candidates must stay close enough while also
|
|
30
|
+
* being smooth.
|
|
31
|
+
*
|
|
32
|
+
* Usage (build first, then run compiled JS):
|
|
33
|
+
* npm run build && node dist/analysis/ama_fitting/optimizer_high_resolution.js --data <path-to-lp-candles.json>
|
|
34
|
+
* node dist/analysis/ama_fitting/optimizer_high_resolution.js --data <path-to-lp-candles.json> --write-profiles
|
|
35
|
+
* node dist/analysis/ama_fitting/optimizer_high_resolution.js --data <path-to-lp-candles.json> --erMax 400 --fastMax 20 --slowMax 200
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_SEARCH = {
|
|
39
|
+
er: { min: 500, max: 1000, count: 15, step: null, quantum: 1 },
|
|
40
|
+
fast: { min: 2, max: 8, count: 30, step: null, quantum: 0.01 },
|
|
41
|
+
slow: { min: 35, max: 140, count: 30, step: null, quantum: 0.1 },
|
|
42
|
+
};
|
|
43
|
+
// ── Default per-AMA distance weights ────────────────────────────────────────
|
|
44
|
+
// Each AMA has its own λ that controls the movement-vs-distance tradeoff.
|
|
45
|
+
// Override individually via --ama1Weight, --ama2Weight, etc. CLI flags.
|
|
46
|
+
const AMA_OBJECTIVES = [
|
|
47
|
+
{ key: 'AMA1', name: 'AMA1 (min move, cap 25%)', distanceCapQuantile: 0.25, distanceWeight: 0.0031 },
|
|
48
|
+
{ key: 'AMA2', name: 'AMA2 (min move, cap 30%)', distanceCapQuantile: 0.30, distanceWeight: 0.0025 },
|
|
49
|
+
{ key: 'AMA3', name: 'AMA3 (min move, cap 35%)', distanceCapQuantile: 0.35, distanceWeight: 0.00185 },
|
|
50
|
+
{ key: 'AMA4', name: 'AMA4 (min move, cap 40%)', distanceCapQuantile: 0.40, distanceWeight: 0.0013 },
|
|
51
|
+
];
|
|
52
|
+
function cloneObjectives() {
|
|
53
|
+
return AMA_OBJECTIVES.map((o) => ({ ...o }));
|
|
54
|
+
}
|
|
55
|
+
// ── Parameter ranges ──────────────────────────────────────────────────────────
|
|
56
|
+
function quantize(value, quantum) {
|
|
57
|
+
if (quantum == null || !Number.isFinite(quantum) || quantum <= 0)
|
|
58
|
+
return value;
|
|
59
|
+
return Math.round(value / quantum) * quantum;
|
|
60
|
+
}
|
|
61
|
+
function geometricRange(min, max, count, quantum = null) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const ratio = Math.pow(max / min, 1 / (count - 1));
|
|
64
|
+
for (let i = 0; i < count; i++) {
|
|
65
|
+
let v = min * Math.pow(ratio, i);
|
|
66
|
+
if (i === 0)
|
|
67
|
+
v = min;
|
|
68
|
+
if (i === count - 1)
|
|
69
|
+
v = max;
|
|
70
|
+
v = quantize(v, quantum);
|
|
71
|
+
v = Math.max(min, Math.min(max, v));
|
|
72
|
+
out.push(parseFloat(v.toFixed(6)));
|
|
73
|
+
}
|
|
74
|
+
return [...new Set(out)].sort((a, b) => a - b);
|
|
75
|
+
}
|
|
76
|
+
function buildDimension(_label, cfg) {
|
|
77
|
+
const min = Number(cfg.min);
|
|
78
|
+
const max = Number(cfg.max);
|
|
79
|
+
const step = Number(cfg.step);
|
|
80
|
+
const count = Number(cfg.count);
|
|
81
|
+
const quantum = Number(cfg.quantum);
|
|
82
|
+
if (Number.isFinite(step) && step > 0) {
|
|
83
|
+
const values = range(min, max, step, 2);
|
|
84
|
+
return {
|
|
85
|
+
values,
|
|
86
|
+
meta: {
|
|
87
|
+
mode: 'linear',
|
|
88
|
+
min: values[0],
|
|
89
|
+
max: values[values.length - 1],
|
|
90
|
+
step,
|
|
91
|
+
quantum: Number.isFinite(quantum) && quantum > 0 ? quantum : null,
|
|
92
|
+
count: values.length,
|
|
93
|
+
ratio: null,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const ratio = Math.pow(max / min, 1 / (count - 1));
|
|
98
|
+
const values = geometricRange(min, max, count, quantum);
|
|
99
|
+
return {
|
|
100
|
+
values,
|
|
101
|
+
meta: {
|
|
102
|
+
mode: 'geometric',
|
|
103
|
+
min: values[0],
|
|
104
|
+
max: values[values.length - 1],
|
|
105
|
+
step: null,
|
|
106
|
+
quantum: Number.isFinite(quantum) && quantum > 0 ? quantum : null,
|
|
107
|
+
count: values.length,
|
|
108
|
+
ratio,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
113
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
114
|
+
const out = {
|
|
115
|
+
dataFile: null,
|
|
116
|
+
er: { ...DEFAULT_SEARCH.er },
|
|
117
|
+
fast: { ...DEFAULT_SEARCH.fast },
|
|
118
|
+
slow: { ...DEFAULT_SEARCH.slow },
|
|
119
|
+
workers: null,
|
|
120
|
+
writeProfiles: false,
|
|
121
|
+
fixedEr: null,
|
|
122
|
+
fixedFast: null,
|
|
123
|
+
ama1Cap: null,
|
|
124
|
+
ama2Cap: null,
|
|
125
|
+
ama3Cap: null,
|
|
126
|
+
ama4Cap: null,
|
|
127
|
+
ama1Weight: null,
|
|
128
|
+
ama2Weight: null,
|
|
129
|
+
ama3Weight: null,
|
|
130
|
+
ama4Weight: null,
|
|
131
|
+
};
|
|
132
|
+
for (let i = 0; i < args.length; i++) {
|
|
133
|
+
const a = args[i];
|
|
134
|
+
const v = args[i + 1];
|
|
135
|
+
switch (a) {
|
|
136
|
+
case '--data':
|
|
137
|
+
out.dataFile = v || null;
|
|
138
|
+
i++;
|
|
139
|
+
break;
|
|
140
|
+
case '--fixEr': {
|
|
141
|
+
const n = Number(v);
|
|
142
|
+
if (!Number.isFinite(n))
|
|
143
|
+
throw new Error(`--fixEr requires a finite number, got "${v}"`);
|
|
144
|
+
out.fixedEr = n;
|
|
145
|
+
i++;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case '--fixFast': {
|
|
149
|
+
const n = Number(v);
|
|
150
|
+
if (!Number.isFinite(n))
|
|
151
|
+
throw new Error(`--fixFast requires a finite number, got "${v}"`);
|
|
152
|
+
out.fixedFast = n;
|
|
153
|
+
i++;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case '--erMin':
|
|
157
|
+
out.er.min = Number(v);
|
|
158
|
+
i++;
|
|
159
|
+
break;
|
|
160
|
+
case '--erMax':
|
|
161
|
+
out.er.max = Number(v);
|
|
162
|
+
i++;
|
|
163
|
+
break;
|
|
164
|
+
case '--erStep':
|
|
165
|
+
out.er.step = Number(v);
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
case '--erCount':
|
|
169
|
+
out.er.count = Number(v);
|
|
170
|
+
i++;
|
|
171
|
+
break;
|
|
172
|
+
case '--fastMin':
|
|
173
|
+
out.fast.min = Number(v);
|
|
174
|
+
i++;
|
|
175
|
+
break;
|
|
176
|
+
case '--fastMax':
|
|
177
|
+
out.fast.max = Number(v);
|
|
178
|
+
i++;
|
|
179
|
+
break;
|
|
180
|
+
case '--fastStep':
|
|
181
|
+
out.fast.step = Number(v);
|
|
182
|
+
i++;
|
|
183
|
+
break;
|
|
184
|
+
case '--fastCount':
|
|
185
|
+
out.fast.count = Number(v);
|
|
186
|
+
i++;
|
|
187
|
+
break;
|
|
188
|
+
case '--slowMin':
|
|
189
|
+
out.slow.min = Number(v);
|
|
190
|
+
i++;
|
|
191
|
+
break;
|
|
192
|
+
case '--slowMax':
|
|
193
|
+
out.slow.max = Number(v);
|
|
194
|
+
i++;
|
|
195
|
+
break;
|
|
196
|
+
case '--slowStep':
|
|
197
|
+
out.slow.step = Number(v);
|
|
198
|
+
i++;
|
|
199
|
+
break;
|
|
200
|
+
case '--slowCount':
|
|
201
|
+
out.slow.count = Number(v);
|
|
202
|
+
i++;
|
|
203
|
+
break;
|
|
204
|
+
case '--ama1Cap':
|
|
205
|
+
out.ama1Cap = Number(v);
|
|
206
|
+
i++;
|
|
207
|
+
break;
|
|
208
|
+
case '--ama2Cap':
|
|
209
|
+
out.ama2Cap = Number(v);
|
|
210
|
+
i++;
|
|
211
|
+
break;
|
|
212
|
+
case '--ama3Cap':
|
|
213
|
+
out.ama3Cap = Number(v);
|
|
214
|
+
i++;
|
|
215
|
+
break;
|
|
216
|
+
case '--ama4Cap':
|
|
217
|
+
out.ama4Cap = Number(v);
|
|
218
|
+
i++;
|
|
219
|
+
break;
|
|
220
|
+
case '--ama1Weight':
|
|
221
|
+
out.ama1Weight = Number(v);
|
|
222
|
+
i++;
|
|
223
|
+
break;
|
|
224
|
+
case '--ama2Weight':
|
|
225
|
+
out.ama2Weight = Number(v);
|
|
226
|
+
i++;
|
|
227
|
+
break;
|
|
228
|
+
case '--ama3Weight':
|
|
229
|
+
out.ama3Weight = Number(v);
|
|
230
|
+
i++;
|
|
231
|
+
break;
|
|
232
|
+
case '--ama4Weight':
|
|
233
|
+
out.ama4Weight = Number(v);
|
|
234
|
+
i++;
|
|
235
|
+
break;
|
|
236
|
+
case '--workers':
|
|
237
|
+
out.workers = Number(v);
|
|
238
|
+
i++;
|
|
239
|
+
break;
|
|
240
|
+
case '--write-profiles':
|
|
241
|
+
out.writeProfiles = true;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
function percentile(values, q) {
|
|
248
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
249
|
+
return null;
|
|
250
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
251
|
+
const qq = Math.max(0, Math.min(1, q));
|
|
252
|
+
const pos = (sorted.length - 1) * qq;
|
|
253
|
+
const lo = Math.floor(pos);
|
|
254
|
+
const hi = Math.ceil(pos);
|
|
255
|
+
if (lo === hi)
|
|
256
|
+
return sorted[lo];
|
|
257
|
+
const t = pos - lo;
|
|
258
|
+
return sorted[lo] * (1 - t) + sorted[hi] * t;
|
|
259
|
+
}
|
|
260
|
+
function getAmaObjectivesFromArgs(args) {
|
|
261
|
+
const objectives = cloneObjectives();
|
|
262
|
+
for (const o of objectives) {
|
|
263
|
+
const id = o.key.toLowerCase();
|
|
264
|
+
const cap = args[`${id}Cap`];
|
|
265
|
+
if (Number.isFinite(cap))
|
|
266
|
+
o.distanceCapQuantile = cap;
|
|
267
|
+
if (!Number.isFinite(o.distanceCapQuantile) || o.distanceCapQuantile <= 0 || o.distanceCapQuantile > 1) {
|
|
268
|
+
throw new Error(`Invalid cap for ${o.key}: ${o.distanceCapQuantile}. Use 0 < cap <= 1`);
|
|
269
|
+
}
|
|
270
|
+
const w = args[`${id}Weight`];
|
|
271
|
+
if (Number.isFinite(w)) {
|
|
272
|
+
if (w <= 0)
|
|
273
|
+
throw new Error(`${o.key} distanceWeight must be positive, got ${w}`);
|
|
274
|
+
o.distanceWeight = w;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return objectives;
|
|
278
|
+
}
|
|
279
|
+
function ensureValidRange(label, cfg) {
|
|
280
|
+
const { min, max, step, count } = cfg;
|
|
281
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) {
|
|
282
|
+
throw new Error(`Invalid ${label} range: min=${min}, max=${max}`);
|
|
283
|
+
}
|
|
284
|
+
if (Number.isFinite(step) && step > 0)
|
|
285
|
+
return;
|
|
286
|
+
if (!Number.isFinite(count) || count < 2) {
|
|
287
|
+
throw new Error(`Invalid ${label} geometric sampling count: ${count}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function boundaryFlags(winner, erValues, fastValues, slowValues, fixedEr = null, fixedFast = null) {
|
|
291
|
+
if (!winner)
|
|
292
|
+
return { er: null, fast: null, slow: null, any: false };
|
|
293
|
+
const eps = 1e-9;
|
|
294
|
+
const minEr = erValues[0], maxEr = erValues[erValues.length - 1];
|
|
295
|
+
const minFast = fastValues[0], maxFast = fastValues[fastValues.length - 1];
|
|
296
|
+
const minSlow = slowValues[0], maxSlow = slowValues[slowValues.length - 1];
|
|
297
|
+
const er = Number.isFinite(fixedEr) ? null : (Math.abs(winner.er - minEr) < eps ? 'min' : (Math.abs(winner.er - maxEr) < eps ? 'max' : null));
|
|
298
|
+
const fast = Number.isFinite(fixedFast) ? null : (Math.abs(winner.fast - minFast) < eps ? 'min' : (Math.abs(winner.fast - maxFast) < eps ? 'max' : null));
|
|
299
|
+
const slow = Math.abs(winner.slow - minSlow) < eps ? 'min' : (Math.abs(winner.slow - maxSlow) < eps ? 'max' : null);
|
|
300
|
+
return { er, fast, slow, any: !!(er || fast || slow) };
|
|
301
|
+
}
|
|
302
|
+
// ── Data loaders ──────────────────────────────────────────────────────────────
|
|
303
|
+
function normalizeSymbol(value) {
|
|
304
|
+
return String(value || '').trim().toUpperCase();
|
|
305
|
+
}
|
|
306
|
+
function inferIntervalLabel(dataFile, meta) {
|
|
307
|
+
const fromMeta = Number(meta?.intervalSeconds);
|
|
308
|
+
if (Number.isFinite(fromMeta) && fromMeta > 0) {
|
|
309
|
+
return toIntervalLabel(fromMeta);
|
|
310
|
+
}
|
|
311
|
+
const m = String(path.basename(dataFile || '')).match(/_(\d+)([mhd])\.json$/i);
|
|
312
|
+
return m ? `${m[1]}${m[2].toLowerCase()}` : '1h';
|
|
313
|
+
}
|
|
314
|
+
function updateAmaProfilesFile({ dataFile, meta, winners, sourceResultsFile }) {
|
|
315
|
+
const assetASymbol = normalizeSymbol(meta?.assetA?.symbol);
|
|
316
|
+
const assetBSymbol = normalizeSymbol(meta?.assetB?.symbol);
|
|
317
|
+
const assetAId = normalizeSymbol(meta?.assetA?.id);
|
|
318
|
+
const assetBId = normalizeSymbol(meta?.assetB?.id);
|
|
319
|
+
const assetA = assetASymbol || assetAId;
|
|
320
|
+
const assetB = assetBSymbol || assetBId;
|
|
321
|
+
if (!assetA || !assetB)
|
|
322
|
+
return;
|
|
323
|
+
const intervalSeconds = Number(meta?.intervalSeconds);
|
|
324
|
+
const intervalLabel = inferIntervalLabel(dataFile, meta);
|
|
325
|
+
const key = `${assetA}|${assetB}|${Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds : intervalLabel}`;
|
|
326
|
+
const payload = {
|
|
327
|
+
version: 1,
|
|
328
|
+
updatedAt: new Date().toISOString(),
|
|
329
|
+
profiles: [],
|
|
330
|
+
};
|
|
331
|
+
if (fs.existsSync(AMA_PROFILES_FILE)) {
|
|
332
|
+
try {
|
|
333
|
+
const current = readJSON(AMA_PROFILES_FILE);
|
|
334
|
+
if (current && typeof current === 'object') {
|
|
335
|
+
payload.version = Number(current.version) || 1;
|
|
336
|
+
payload.profiles = Array.isArray(current.profiles) ? current.profiles : [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (_) { }
|
|
340
|
+
}
|
|
341
|
+
const profile = {
|
|
342
|
+
key,
|
|
343
|
+
assetA,
|
|
344
|
+
assetB,
|
|
345
|
+
assetAId: assetAId || null,
|
|
346
|
+
assetBId: assetBId || null,
|
|
347
|
+
poolId: meta?.pool || null,
|
|
348
|
+
intervalSeconds: Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds : null,
|
|
349
|
+
intervalLabel,
|
|
350
|
+
defaultAma: 'AMA3',
|
|
351
|
+
sourceResultsFile,
|
|
352
|
+
updatedAt: payload.updatedAt,
|
|
353
|
+
amas: {
|
|
354
|
+
AMA1: {
|
|
355
|
+
name: winners.ama1.label,
|
|
356
|
+
erPeriod: winners.ama1.er,
|
|
357
|
+
fastPeriod: winners.ama1.fast,
|
|
358
|
+
slowPeriod: winners.ama1.slow,
|
|
359
|
+
},
|
|
360
|
+
AMA2: {
|
|
361
|
+
name: winners.ama2.label,
|
|
362
|
+
erPeriod: winners.ama2.er,
|
|
363
|
+
fastPeriod: winners.ama2.fast,
|
|
364
|
+
slowPeriod: winners.ama2.slow,
|
|
365
|
+
},
|
|
366
|
+
AMA3: {
|
|
367
|
+
name: winners.ama3.label,
|
|
368
|
+
erPeriod: winners.ama3.er,
|
|
369
|
+
fastPeriod: winners.ama3.fast,
|
|
370
|
+
slowPeriod: winners.ama3.slow,
|
|
371
|
+
},
|
|
372
|
+
AMA4: {
|
|
373
|
+
name: winners.ama4.label,
|
|
374
|
+
erPeriod: winners.ama4.er,
|
|
375
|
+
fastPeriod: winners.ama4.fast,
|
|
376
|
+
slowPeriod: winners.ama4.slow,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const idx = payload.profiles.findIndex((p) => String(p?.key || '') === key);
|
|
381
|
+
if (idx >= 0)
|
|
382
|
+
payload.profiles[idx] = profile;
|
|
383
|
+
else
|
|
384
|
+
payload.profiles.push(profile);
|
|
385
|
+
ensureDir(path.dirname(AMA_PROFILES_FILE));
|
|
386
|
+
writeJSON(AMA_PROFILES_FILE, payload);
|
|
387
|
+
}
|
|
388
|
+
function calcTotalAmaMovement(amaValues, erPeriod) {
|
|
389
|
+
const skip = erPeriod + 1;
|
|
390
|
+
let total = 0;
|
|
391
|
+
for (let i = skip + 1; i < amaValues.length; i++) {
|
|
392
|
+
total += Math.abs(amaValues[i] - amaValues[i - 1]) / amaValues[i - 1];
|
|
393
|
+
}
|
|
394
|
+
return total;
|
|
395
|
+
}
|
|
396
|
+
// ── Informational: area above/below AMA ──────────────────────────────────────
|
|
397
|
+
function calcArea(amaValues, candles, erPeriod) {
|
|
398
|
+
const skip = erPeriod + 1;
|
|
399
|
+
let above = 0, below = 0, maxUp = 0, maxDown = 0;
|
|
400
|
+
for (let i = skip; i < candles.length; i++) {
|
|
401
|
+
const ama = amaValues[i];
|
|
402
|
+
if (candles[i].high > ama) {
|
|
403
|
+
const d = (candles[i].high - ama) / ama;
|
|
404
|
+
above += d;
|
|
405
|
+
if (d > maxUp)
|
|
406
|
+
maxUp = d;
|
|
407
|
+
}
|
|
408
|
+
if (candles[i].low < ama) {
|
|
409
|
+
const d = (ama - candles[i].low) / ama;
|
|
410
|
+
below += d;
|
|
411
|
+
if (d > maxDown)
|
|
412
|
+
maxDown = d;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const total = above + below;
|
|
416
|
+
const maxDist = Math.max(maxUp, maxDown);
|
|
417
|
+
return { above, below, total, maxUp, maxDown, maxDist };
|
|
418
|
+
}
|
|
419
|
+
function calcTotalRelativeDistance(amaValues, candles, erPeriod) {
|
|
420
|
+
const skip = erPeriod + 1;
|
|
421
|
+
let total = 0;
|
|
422
|
+
for (let i = skip; i < candles.length; i++) {
|
|
423
|
+
const ama = amaValues[i];
|
|
424
|
+
const close = candles[i].close;
|
|
425
|
+
total += Math.abs(close - ama) / ama;
|
|
426
|
+
}
|
|
427
|
+
return total;
|
|
428
|
+
}
|
|
429
|
+
function runSearchShard(payload, onProgress = null) {
|
|
430
|
+
const { workerId, erShard, fastValues, slowValues, candles, closes, } = payload;
|
|
431
|
+
const totalCombos = erShard.length * fastValues.length * slowValues.length;
|
|
432
|
+
const progressStep = Math.max(2000, Math.floor(totalCombos / 20));
|
|
433
|
+
let checked = 0;
|
|
434
|
+
let valid = 0;
|
|
435
|
+
const entries = [];
|
|
436
|
+
const startMs = Date.now();
|
|
437
|
+
for (const er of erShard) {
|
|
438
|
+
for (const fast of fastValues) {
|
|
439
|
+
for (const slow of slowValues) {
|
|
440
|
+
checked++;
|
|
441
|
+
if (fast >= slow) {
|
|
442
|
+
if (onProgress && (checked % progressStep === 0 || checked === totalCombos)) {
|
|
443
|
+
const elapsedSec = (Date.now() - startMs) / 1000;
|
|
444
|
+
onProgress({ workerId, checked, total: totalCombos, elapsedSec });
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
valid++;
|
|
449
|
+
const ama = calculateAMA(closes, { erPeriod: er, fastPeriod: fast, slowPeriod: slow });
|
|
450
|
+
const area = calcArea(ama, candles, er);
|
|
451
|
+
const amaMovementTotal = calcTotalAmaMovement(ama, er);
|
|
452
|
+
const distanceTotal = calcTotalRelativeDistance(ama, candles, er);
|
|
453
|
+
const bandFactorPct = area.maxDist * 200;
|
|
454
|
+
const entry = {
|
|
455
|
+
er, fast, slow,
|
|
456
|
+
area,
|
|
457
|
+
amaMovementTotal,
|
|
458
|
+
bandFactorPct,
|
|
459
|
+
distanceTotal,
|
|
460
|
+
};
|
|
461
|
+
entries.push(entry);
|
|
462
|
+
if (onProgress && (checked % progressStep === 0 || checked === totalCombos)) {
|
|
463
|
+
const elapsedSec = (Date.now() - startMs) / 1000;
|
|
464
|
+
onProgress({ workerId, checked, total: totalCombos, elapsedSec });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return { workerId, entries, totalCombos, validCombos: valid };
|
|
470
|
+
}
|
|
471
|
+
function spawnShardWorker(payload, onProgress) {
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
const worker = new Worker(__filename, { workerData: { type: 'search_shard', payload } });
|
|
474
|
+
worker.on('message', (msg) => {
|
|
475
|
+
if (!msg || typeof msg !== 'object')
|
|
476
|
+
return;
|
|
477
|
+
if (msg.type === 'progress') {
|
|
478
|
+
if (onProgress)
|
|
479
|
+
onProgress(msg);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (msg.type === 'done')
|
|
483
|
+
resolve(msg.result);
|
|
484
|
+
});
|
|
485
|
+
worker.on('error', reject);
|
|
486
|
+
worker.on('exit', (code) => {
|
|
487
|
+
if (code !== 0)
|
|
488
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function splitIntoShards(values, shardCount) {
|
|
493
|
+
const out = [];
|
|
494
|
+
const size = Math.ceil(values.length / shardCount);
|
|
495
|
+
for (let i = 0; i < values.length; i += size) {
|
|
496
|
+
out.push(values.slice(i, i + size));
|
|
497
|
+
}
|
|
498
|
+
return out.filter((s) => s.length > 0);
|
|
499
|
+
}
|
|
500
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
501
|
+
async function run() {
|
|
502
|
+
const args = parseArgs();
|
|
503
|
+
let dataFile = args.dataFile;
|
|
504
|
+
const objectives = getAmaObjectivesFromArgs(args);
|
|
505
|
+
ensureValidRange('ER', args.er);
|
|
506
|
+
ensureValidRange('Fast', args.fast);
|
|
507
|
+
ensureValidRange('Slow', args.slow);
|
|
508
|
+
const erDim = buildDimension('ER', args.er);
|
|
509
|
+
const fastDim = buildDimension('Fast', args.fast);
|
|
510
|
+
const slowDim = buildDimension('Slow', args.slow);
|
|
511
|
+
const ER_VALUES = Number.isFinite(args.fixedEr) ? [args.fixedEr] : erDim.values;
|
|
512
|
+
const FAST_VALUES = Number.isFinite(args.fixedFast) ? [args.fixedFast] : fastDim.values;
|
|
513
|
+
const SLOW_VALUES_AREA = slowDim.values;
|
|
514
|
+
const totalCombos = ER_VALUES.length * FAST_VALUES.length * SLOW_VALUES_AREA.length;
|
|
515
|
+
const startMs = Date.now();
|
|
516
|
+
const erLabel = Number.isFinite(args.fixedEr) ? `fixed(${args.fixedEr})` : `${ER_VALUES[0]}–${ER_VALUES[ER_VALUES.length - 1]}`;
|
|
517
|
+
const fastLabel = Number.isFinite(args.fixedFast) ? `fixed(${args.fixedFast})` : `${FAST_VALUES[0]}–${FAST_VALUES[FAST_VALUES.length - 1]}`;
|
|
518
|
+
console.log('================================================================================');
|
|
519
|
+
console.log(' AMA GEOMETRIC OPTIMIZER');
|
|
520
|
+
console.log('================================================================================');
|
|
521
|
+
console.log(` 4 AMAs — pure geometric, no grid or bot settings`);
|
|
522
|
+
console.log(' Objective: minimise movement + λ·distance under per-AMA max distance caps');
|
|
523
|
+
for (const o of objectives) {
|
|
524
|
+
console.log(` ${o.key}: distance cap q=${o.distanceCapQuantile.toFixed(2)} λ=${o.distanceWeight.toFixed(6)}`);
|
|
525
|
+
}
|
|
526
|
+
console.log(` Ranges: ER ${erLabel} Fast ${fastLabel} Slow ${SLOW_VALUES_AREA[0]}–${SLOW_VALUES_AREA[SLOW_VALUES_AREA.length - 1]}`);
|
|
527
|
+
if (!Number.isFinite(args.fixedEr) && !Number.isFinite(args.fixedFast)) {
|
|
528
|
+
console.log(` Sampling: ER ${erDim.meta.mode}(${erDim.meta.count}${erDim.meta.ratio ? `,x${erDim.meta.ratio.toFixed(3)}` : ''}) Fast ${fastDim.meta.mode}(${fastDim.meta.count}${fastDim.meta.ratio ? `,x${fastDim.meta.ratio.toFixed(3)}` : ''}) Slow ${slowDim.meta.mode}(${slowDim.meta.count}${slowDim.meta.ratio ? `,x${slowDim.meta.ratio.toFixed(3)}` : ''})`);
|
|
529
|
+
}
|
|
530
|
+
console.log(` Combos: ${totalCombos}\n`);
|
|
531
|
+
// Load data
|
|
532
|
+
let candles, dataLabel, dataMeta = null;
|
|
533
|
+
if (!dataFile) {
|
|
534
|
+
throw new Error('Optimizer requires --data <lp_pool_*.json>. Use --write-profiles to also update profiles/market_profiles.json.');
|
|
535
|
+
}
|
|
536
|
+
const loaded = loadLpDataFile(path.resolve(dataFile));
|
|
537
|
+
candles = loaded.candleObjects;
|
|
538
|
+
const m = loaded.meta;
|
|
539
|
+
dataMeta = m;
|
|
540
|
+
dataLabel = m ? `LP Pool ${m.pool} (${m.assetA?.symbol}/${m.assetB?.symbol})` : path.basename(dataFile);
|
|
541
|
+
const closes = candles.map((c) => c.close);
|
|
542
|
+
console.log(` Data: ${dataLabel} (${candles.length} candles)\n`);
|
|
543
|
+
// ── Run: core-parallel full-grid scan split by ER shards ──────────────────
|
|
544
|
+
const cpuCount = Math.max(1, os.cpus().length);
|
|
545
|
+
const requestedWorkers = Number.isFinite(args.workers) && args.workers > 0 ? Math.floor(args.workers) : cpuCount;
|
|
546
|
+
const workerCount = Math.max(1, Math.min(requestedWorkers, ER_VALUES.length));
|
|
547
|
+
const erShards = splitIntoShards(ER_VALUES, workerCount);
|
|
548
|
+
const shardTotals = erShards.map((shard) => shard.length * FAST_VALUES.length * SLOW_VALUES_AREA.length);
|
|
549
|
+
const shardProgress = new Map();
|
|
550
|
+
console.log(` Parallel workers: ${erShards.length} shard workers (CPU cores available: ${cpuCount})`);
|
|
551
|
+
const shardResults = await Promise.all(erShards.map((erShard, idx) => {
|
|
552
|
+
const workerPayload = {
|
|
553
|
+
workerId: idx + 1,
|
|
554
|
+
erShard,
|
|
555
|
+
fastValues: FAST_VALUES,
|
|
556
|
+
slowValues: SLOW_VALUES_AREA,
|
|
557
|
+
candles,
|
|
558
|
+
closes,
|
|
559
|
+
};
|
|
560
|
+
return spawnShardWorker(workerPayload, (msg) => {
|
|
561
|
+
shardProgress.set(msg.workerId, msg.checked);
|
|
562
|
+
const done = shardTotals.reduce((acc, total, i) => acc + Math.min(total, shardProgress.get(i + 1) || 0), 0);
|
|
563
|
+
const pct = ((done / totalCombos) * 100).toFixed(1);
|
|
564
|
+
console.log(` [scan] ${pct}% (${done}/${totalCombos}) w${msg.workerId} ${msg.elapsedSec.toFixed(1)}s`);
|
|
565
|
+
});
|
|
566
|
+
}));
|
|
567
|
+
const entries = shardResults.flatMap((r) => r.entries);
|
|
568
|
+
const validCombos = shardResults.reduce((acc, r) => acc + r.validCombos, 0);
|
|
569
|
+
const maxDistVals = entries.map((e) => e.area.maxDist);
|
|
570
|
+
// When both ER and Fast are fixed, the search is 1D over Slow only. Slow
|
|
571
|
+
// variation rarely produces maxDist outliers, so the distance cap filter adds
|
|
572
|
+
// no value here — λ alone cleanly separates the candidates.
|
|
573
|
+
const skipCap = Number.isFinite(args.fixedEr) && Number.isFinite(args.fixedFast);
|
|
574
|
+
const objectiveResults = objectives.map((objective) => {
|
|
575
|
+
const distanceWeight = objective.distanceWeight;
|
|
576
|
+
const computedCap = skipCap ? null : percentile(maxDistVals, objective.distanceCapQuantile);
|
|
577
|
+
const cappedEntries = skipCap || computedCap == null ? entries : entries.filter((e) => e.area.maxDist <= computedCap);
|
|
578
|
+
let best = null;
|
|
579
|
+
for (const e of cappedEntries) {
|
|
580
|
+
const score = e.amaMovementTotal + (distanceWeight * e.distanceTotal);
|
|
581
|
+
const bestScore = best ? best.weightedScore : Infinity;
|
|
582
|
+
if (!best || score < bestScore || (score === bestScore && e.amaMovementTotal < best.amaMovementTotal) || (score === bestScore && e.amaMovementTotal === best.amaMovementTotal && e.distanceTotal < best.distanceTotal)) {
|
|
583
|
+
best = {
|
|
584
|
+
...e,
|
|
585
|
+
weightedScore: score,
|
|
586
|
+
normDistance: null,
|
|
587
|
+
normMovement: null,
|
|
588
|
+
key: objective.key,
|
|
589
|
+
label: objective.name,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
objective,
|
|
595
|
+
distanceWeight,
|
|
596
|
+
best,
|
|
597
|
+
totalCombos,
|
|
598
|
+
validCombos,
|
|
599
|
+
candidatesUnderCap: cappedEntries.length,
|
|
600
|
+
maxDistanceCap: computedCap,
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
const elapsedSec = (Date.now() - startMs) / 1000;
|
|
604
|
+
console.log(` Completed parallel search in ${elapsedSec.toFixed(1)}s (valid combos: ${validCombos})\n`);
|
|
605
|
+
const selected = objectiveResults.map((r) => r.best).filter(Boolean);
|
|
606
|
+
const failed = objectiveResults.filter((r) => !r.best).map((r) => r.objective?.key || '?');
|
|
607
|
+
if (failed.length > 0) {
|
|
608
|
+
throw new Error(`No candidate under distance cap for: ${failed.join(', ')}. Increase corresponding cap (e.g. --ama4Cap 0.35).`);
|
|
609
|
+
}
|
|
610
|
+
const ama1 = selected.find((s) => s.key === 'AMA1') || null;
|
|
611
|
+
const ama2 = selected.find((s) => s.key === 'AMA2') || null;
|
|
612
|
+
const ama3 = selected.find((s) => s.key === 'AMA3') || null;
|
|
613
|
+
const ama4 = selected.find((s) => s.key === 'AMA4') || null;
|
|
614
|
+
function detail(label, r, optimisedFor) {
|
|
615
|
+
if (!r) {
|
|
616
|
+
console.log(` ${label}`);
|
|
617
|
+
console.log(' └─ No valid candidate under constraint\n');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const asymmetry = Math.abs(r.area.above - r.area.below);
|
|
621
|
+
const bias = r.area.above > r.area.below ? 'AMA below price' : 'AMA above price';
|
|
622
|
+
console.log(` ${label}`);
|
|
623
|
+
console.log(` ├─ Optimised for: ${optimisedFor}`);
|
|
624
|
+
console.log(` ├─ Params: ER=${r.er} Fast=${r.fast} Slow=${r.slow}`);
|
|
625
|
+
console.log(` ├─ Area total: ${r.area.total.toFixed(2)} (above ${r.area.above.toFixed(2)} below ${r.area.below.toFixed(2)})`);
|
|
626
|
+
console.log(` ├─ Asymmetry: ${asymmetry.toFixed(2)} (${bias})`);
|
|
627
|
+
console.log(` └─ Movement: ${r.amaMovementTotal.toFixed(4)}\n`);
|
|
628
|
+
}
|
|
629
|
+
console.log('================================================================================');
|
|
630
|
+
console.log(` 4 AMAs — pure geometric (${ER_VALUES.length}×${FAST_VALUES.length}×${SLOW_VALUES_AREA.length} combinations)`);
|
|
631
|
+
console.log('================================================================================\n');
|
|
632
|
+
for (const w of selected) {
|
|
633
|
+
detail(w.label, w, `score=${w.weightedScore.toFixed(6)} λ=${objectiveResults.find((r) => r.objective?.key === w.key)?.distanceWeight?.toFixed(4) || 'n/a'} rawDist=${w.distanceTotal.toFixed(2)} rawMove=${w.amaMovementTotal.toFixed(2)}`);
|
|
634
|
+
}
|
|
635
|
+
for (const r of objectiveResults) {
|
|
636
|
+
const mdc = r.maxDistanceCap;
|
|
637
|
+
if (mdc != null && Number.isFinite(mdc)) {
|
|
638
|
+
console.log(` ${r.objective.key} cap value: ${mdc.toFixed(4)} (candidates under cap: ${r.candidatesUnderCap})`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
console.log();
|
|
642
|
+
// ── Side-by-side summary ───────────────────────────────────────────────────
|
|
643
|
+
console.log('================================================================================');
|
|
644
|
+
console.log(' SUMMARY');
|
|
645
|
+
console.log('================================================================================\n');
|
|
646
|
+
console.log(' | ER | Fast | Slow | Dist | Move | Area | MaxDist');
|
|
647
|
+
console.log('────────────────┼──────┼──────┼──────┼─────────┼─────────┼─────────┼─────────');
|
|
648
|
+
for (const r of selected) {
|
|
649
|
+
const name = r.key;
|
|
650
|
+
if (!r)
|
|
651
|
+
continue;
|
|
652
|
+
console.log(`${name.padEnd(15)} | ` +
|
|
653
|
+
`${r.er.toString().padStart(4)} | ` +
|
|
654
|
+
`${r.fast.toFixed(1).padStart(4)} | ` +
|
|
655
|
+
`${r.slow.toFixed(1).padStart(4)} | ` +
|
|
656
|
+
`${r.distanceTotal.toFixed(2).padStart(7)} | ` +
|
|
657
|
+
`${r.amaMovementTotal.toFixed(2).padStart(7)} | ` +
|
|
658
|
+
`${r.area.total.toFixed(2).padStart(7)} | ` +
|
|
659
|
+
`${(r.area.maxDist * 100).toFixed(1).padStart(7)}%`);
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
const boundarySummary = {
|
|
663
|
+
AMA1: boundaryFlags(ama1, ER_VALUES, FAST_VALUES, SLOW_VALUES_AREA, args.fixedEr, args.fixedFast),
|
|
664
|
+
AMA2: boundaryFlags(ama2, ER_VALUES, FAST_VALUES, SLOW_VALUES_AREA, args.fixedEr, args.fixedFast),
|
|
665
|
+
AMA3: boundaryFlags(ama3, ER_VALUES, FAST_VALUES, SLOW_VALUES_AREA, args.fixedEr, args.fixedFast),
|
|
666
|
+
AMA4: boundaryFlags(ama4, ER_VALUES, FAST_VALUES, SLOW_VALUES_AREA, args.fixedEr, args.fixedFast),
|
|
667
|
+
};
|
|
668
|
+
console.log(' Boundary Check');
|
|
669
|
+
console.log('────────────────────────────────────────────────────────────────────────────────');
|
|
670
|
+
for (const [name, b] of Object.entries(boundarySummary)) {
|
|
671
|
+
const txt = b.any ? `ER:${b.er || '-'} Fast:${b.fast || '-'} Slow:${b.slow || '-'}` : 'none';
|
|
672
|
+
console.log(`${name.padEnd(24)} ${txt}`);
|
|
673
|
+
}
|
|
674
|
+
console.log();
|
|
675
|
+
// ── Chart ─────────────────────────────────────────────────────────────────
|
|
676
|
+
const ws = (v) => String(v).replace('.', '_');
|
|
677
|
+
const lambdaSuffix = `_w${objectives.map((o) => ws(String(o.distanceWeight))).join('_')}`;
|
|
678
|
+
const COLOR_CYCLE = ['#26a69a', '#fb8c00', '#5c9ee6', '#ef5350'];
|
|
679
|
+
const DASH_CYCLE = ['dot', 'solid', 'dash', 'dashdot'];
|
|
680
|
+
const candleArrays = candles.map((c) => [c.timestamp, c.open, c.high, c.low, c.close, c.volume]);
|
|
681
|
+
const chartMeta = {
|
|
682
|
+
...(dataMeta || {}),
|
|
683
|
+
assetA: dataMeta?.assetA || { symbol: '?' },
|
|
684
|
+
assetB: dataMeta?.assetB || { symbol: '?' },
|
|
685
|
+
intervalSeconds: dataMeta?.intervalSeconds || 3600,
|
|
686
|
+
fetchedAt: dataMeta?.fetchedAt || new Date().toISOString(),
|
|
687
|
+
};
|
|
688
|
+
const amaResults = selected.map((w, i) => {
|
|
689
|
+
const ama = calculateAMA(closes, { erPeriod: w.er, fastPeriod: w.fast, slowPeriod: w.slow });
|
|
690
|
+
return {
|
|
691
|
+
name: w.label,
|
|
692
|
+
erPeriod: w.er,
|
|
693
|
+
fastPeriod: w.fast,
|
|
694
|
+
slowPeriod: w.slow,
|
|
695
|
+
color: COLOR_CYCLE[i % COLOR_CYCLE.length],
|
|
696
|
+
dash: DASH_CYCLE[i % DASH_CYCLE.length],
|
|
697
|
+
lineWidth: i === 0 ? 2 : 1.5,
|
|
698
|
+
values: ama,
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
const chartName = dataFile
|
|
702
|
+
? `optimization_chart_${path.basename(dataFile, '.json')}${lambdaSuffix}.html`
|
|
703
|
+
: `optimization_chart_high_resolution${lambdaSuffix}.html`;
|
|
704
|
+
const chartPath = path.join(PATHS.ANALYSIS.CHARTS_DIR, chartName);
|
|
705
|
+
ensureDir(path.dirname(chartPath));
|
|
706
|
+
const html = generateHTML(chartMeta, candleArrays, amaResults);
|
|
707
|
+
fs.writeFileSync(chartPath, html, 'utf8');
|
|
708
|
+
console.log(` Chart: ${path.relative(process.cwd(), chartPath)}\n`);
|
|
709
|
+
// ── Save ──────────────────────────────────────────────────────────────────
|
|
710
|
+
const outName = dataFile
|
|
711
|
+
? `optimization_results_${path.basename(dataFile, '.json')}${lambdaSuffix}.json`
|
|
712
|
+
: `optimization_results_high_resolution${lambdaSuffix}.json`;
|
|
713
|
+
const outPath = path.join(__dirname, outName);
|
|
714
|
+
writeJSON(outPath, {
|
|
715
|
+
meta: {
|
|
716
|
+
dataLabel,
|
|
717
|
+
candles: candles.length,
|
|
718
|
+
totalCombos,
|
|
719
|
+
ranges: {
|
|
720
|
+
er: { ...erDim.meta },
|
|
721
|
+
fast: { ...fastDim.meta },
|
|
722
|
+
slow: { ...slowDim.meta },
|
|
723
|
+
fixedEr: Number.isFinite(args.fixedEr) ? args.fixedEr : null,
|
|
724
|
+
fixedFast: Number.isFinite(args.fixedFast) ? args.fixedFast : null,
|
|
725
|
+
},
|
|
726
|
+
boundaryFlags: boundarySummary,
|
|
727
|
+
objective: {
|
|
728
|
+
type: 'per_ama_distance_weights',
|
|
729
|
+
distanceMetric: 'sum(abs(close-ama)/ama)',
|
|
730
|
+
movementMetric: 'sum(abs(ama_t-ama_t-1)/ama_t-1)',
|
|
731
|
+
distanceWeightByAma: Object.fromEntries(objectiveResults.map((r) => [r.objective.key, r.distanceWeight])),
|
|
732
|
+
weights: objectives,
|
|
733
|
+
maxDistanceCap: {
|
|
734
|
+
value: null,
|
|
735
|
+
quantileByAma: Object.fromEntries(objectives.map((o) => [o.key, o.distanceCapQuantile])),
|
|
736
|
+
appliedValueByAma: Object.fromEntries(objectiveResults.map((r) => [r.objective.key, r.maxDistanceCap])),
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
amas: {
|
|
740
|
+
AMA1: ama1,
|
|
741
|
+
AMA2: ama2,
|
|
742
|
+
AMA3: ama3,
|
|
743
|
+
AMA4: ama4,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
if (args.writeProfiles && dataFile && ama1 && ama2 && ama3 && ama4) {
|
|
748
|
+
updateAmaProfilesFile({
|
|
749
|
+
dataFile,
|
|
750
|
+
meta: dataMeta,
|
|
751
|
+
winners: { ama1, ama2, ama3, ama4 },
|
|
752
|
+
sourceResultsFile: outName,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
console.log(`================================================================================`);
|
|
756
|
+
console.log(` Saved: ${outName}\n`);
|
|
757
|
+
if (args.writeProfiles && dataFile) {
|
|
758
|
+
console.log(` Updated: ${path.relative(process.cwd(), AMA_PROFILES_FILE)}\n`);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.log(` Profiles unchanged. Add --write-profiles to update ${path.relative(process.cwd(), AMA_PROFILES_FILE)}.\n`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (!isMainThread) {
|
|
765
|
+
if (workerData?.type === 'search_shard') {
|
|
766
|
+
const result = runSearchShard(workerData.payload, (p) => {
|
|
767
|
+
parentPort.postMessage({ type: 'progress', ...p });
|
|
768
|
+
});
|
|
769
|
+
parentPort.postMessage({ type: 'done', result });
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
throw new Error('Unknown worker task type');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
776
|
+
run().catch((err) => {
|
|
777
|
+
console.error('Fatal:', err);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
export { parseArgs };
|
|
782
|
+
//# sourceMappingURL=optimizer_high_resolution.js.map
|