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,1714 @@
1
+ import fs from 'node:fs';
2
+ import { MARKET_ADAPTER } from '../../modules/constants.js';
3
+ import { escapeHtml, serializeJsonForScript } from '../chart_utils.js';
4
+ import { normalizeCandle } from '../math_utils.js';
5
+ import { PATHS } from '../../modules/paths.js';
6
+ import { getStorage } from '../../modules/storage/index.js';
7
+ const { readJSON } = getStorage();
8
+ import { getErrorMessage } from '../../modules/utils/errors.js';
9
+ 'use strict';
10
+ function inferBaseIntervalSeconds(candles, fallback = 3600) {
11
+ if (!Array.isArray(candles) || candles.length < 2)
12
+ return fallback;
13
+ const deltas = [];
14
+ for (let i = 1; i < candles.length; i++) {
15
+ const prev = Number(candles[i - 1]?.time);
16
+ const curr = Number(candles[i]?.time);
17
+ if (!Number.isFinite(prev) || !Number.isFinite(curr))
18
+ continue;
19
+ const d = curr - prev;
20
+ if (d > 0)
21
+ deltas.push(d);
22
+ }
23
+ if (deltas.length === 0)
24
+ return fallback;
25
+ deltas.sort((a, b) => a - b);
26
+ const mid = Math.floor(deltas.length / 2);
27
+ const med = deltas.length % 2 === 0 ? (deltas[mid - 1] + deltas[mid]) / 2 : deltas[mid];
28
+ return Math.max(60, Math.round(med));
29
+ }
30
+ function loadMarketProfiles(filePath = PATHS.PROFILES.MARKET_PROFILES_JSON) {
31
+ if (!filePath || !fs.existsSync(filePath))
32
+ return null;
33
+ try {
34
+ return readJSON(filePath);
35
+ }
36
+ catch (err) {
37
+ console.warn(`[WARN] Failed to parse ${filePath}: ${getErrorMessage(err)}. Falling back to built-in AMA defaults.`);
38
+ return null;
39
+ }
40
+ }
41
+ function findMarketProfile(profiles, meta = {}) {
42
+ const entries = Array.isArray(profiles?.profiles) ? profiles.profiles : [];
43
+ if (!entries.length)
44
+ return null;
45
+ const assetA = meta.assetA?.symbol || meta.assetA?.id || meta.assetA;
46
+ const assetB = meta.assetB?.symbol || meta.assetB?.id || meta.assetB;
47
+ const intervalSeconds = Number(meta.intervalSeconds);
48
+ return entries.find((entry) => {
49
+ if (!entry || typeof entry !== 'object')
50
+ return false;
51
+ if (assetA && assetB) {
52
+ const matchesPair = (String(entry.assetA) === String(assetA) || String(entry.assetAId) === String(assetA))
53
+ && (String(entry.assetB) === String(assetB) || String(entry.assetBId) === String(assetB));
54
+ if (!matchesPair)
55
+ return false;
56
+ }
57
+ else {
58
+ return false;
59
+ }
60
+ if (Number.isFinite(intervalSeconds) && intervalSeconds > 0 && Number(entry.intervalSeconds) !== intervalSeconds) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }) || null;
65
+ }
66
+ function resolveAmaDefaults({ meta, data, marketProfiles } = {}) {
67
+ const amaDefaultsSource = MARKET_ADAPTER.AMAS.AMA3;
68
+ const profile = findMarketProfile(marketProfiles, meta);
69
+ const profileAmaKey = profile?.defaultAma && profile.amas && profile.amas[profile.defaultAma]
70
+ ? profile.defaultAma
71
+ : null;
72
+ const profileAma = profileAmaKey ? profile.amas[profileAmaKey] : null;
73
+ const source = data?.amaDefaults || profileAma || amaDefaultsSource;
74
+ return {
75
+ erPeriod: Math.max(1, Math.round(Number(data?.amaErPeriod ?? source.erPeriod))),
76
+ fastPeriod: Number.isFinite(Number(data?.amaFastPeriod))
77
+ ? Number(data.amaFastPeriod)
78
+ : Number(source.fastPeriod),
79
+ slowPeriod: Number.isFinite(Number(data?.amaSlowPeriod))
80
+ ? Number(data.amaSlowPeriod)
81
+ : Number(source.slowPeriod),
82
+ };
83
+ }
84
+ function generateHTML(data, title = 'TradingView Style Research') {
85
+ const rawCandles = Array.isArray(data.candles) ? data.candles : [];
86
+ const candles = rawCandles.map(normalizeCandle).filter(Boolean);
87
+ if (candles.length === 0)
88
+ throw new Error('No candle data in input');
89
+ const meta = data.meta || {};
90
+ const baseIntervalSeconds = Number(meta.intervalSeconds) > 0
91
+ ? Number(meta.intervalSeconds)
92
+ : inferBaseIntervalSeconds(candles, 3600);
93
+ const timeframes = [
94
+ { label: '1h', seconds: 3600 },
95
+ { label: '4h', seconds: 14400 },
96
+ { label: '1d', seconds: 86400 },
97
+ { label: '1w', seconds: 604800 },
98
+ ].map((item) => ({ ...item, enabled: item.seconds >= baseIntervalSeconds }));
99
+ const defaultTimeframe = timeframes.find((item) => item.label === data.defaultTimeframe && item.enabled)
100
+ || timeframes.find((item) => item.enabled)
101
+ || timeframes[0];
102
+ const marketProfiles = data.marketProfiles || loadMarketProfiles();
103
+ const defaultAmaConfig = resolveAmaDefaults({ meta, data, marketProfiles });
104
+ const defaults = {
105
+ smaPeriod: Math.max(1, Math.round(data.smaPeriod ?? 500)),
106
+ amaDefaults: defaultAmaConfig,
107
+ smaEnabled: data.smaEnabled === true,
108
+ amaEnabled: data.amaEnabled === true,
109
+ vwapEnabled: data.vwapEnabled === true,
110
+ vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
111
+ priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
112
+ };
113
+ const assetLabelA = meta.assetA?.symbol || meta.assetA?.id || 'Asset A';
114
+ const assetLabelB = meta.assetB?.symbol || meta.assetB?.id || 'Asset B';
115
+ const pairLabelNormal = `${assetLabelA}/${assetLabelB}`;
116
+ const pairLabelInverse = `${assetLabelB}/${assetLabelA}`;
117
+ const poolLabel = meta.pool ? `Pool ${String(meta.pool).replace(/^1\.19\./, '')}` : 'Pair';
118
+ const intervalLabel = baseIntervalSeconds >= 86400
119
+ ? `${Math.round(baseIntervalSeconds / 86400)}d`
120
+ : `${Math.round(baseIntervalSeconds / 3600)}h`;
121
+ const defaultPairMode = data.defaultPairMode === 'inverse' ? 'inverse' : 'normal';
122
+ const payload = {
123
+ candles,
124
+ timeframes,
125
+ defaultTimeframe: defaultTimeframe.label,
126
+ smaPeriod: defaults.smaPeriod,
127
+ amaDefaults: defaultAmaConfig,
128
+ smaEnabled: defaults.smaEnabled,
129
+ amaEnabled: defaults.amaEnabled,
130
+ vwapEnabled: defaults.vwapEnabled,
131
+ vwapBars: defaults.vwapBars,
132
+ priceScale: defaults.priceScale,
133
+ defaultPairMode,
134
+ assetLabelA,
135
+ assetLabelB,
136
+ pairLabelNormal,
137
+ pairLabelInverse,
138
+ poolLabel,
139
+ intervalLabel,
140
+ amaDefaultsSource: marketProfiles ? 'market_profiles' : 'constants',
141
+ };
142
+ return `<!doctype html>
143
+ <html lang="en">
144
+ <head>
145
+ <meta charset="utf-8">
146
+ <meta name="color-scheme" content="dark">
147
+ <meta name="darkreader-lock">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1">
149
+ <title>${escapeHtml(title)}</title>
150
+ <link rel="stylesheet" href="../uplot/uPlot.min.css">
151
+ <script src="../uplot/uPlot.iife.min.js"></script>
152
+ <style>
153
+ * { box-sizing: border-box; }
154
+ :root { color-scheme: dark; }
155
+ body {
156
+ margin: 0;
157
+ overflow: hidden;
158
+ background: linear-gradient(180deg, #090d12 0%, #0b0f14 40%, #0a0c11 100%);
159
+ color: #d7e0ea;
160
+ font-family: Inter, "Segoe UI", system-ui, sans-serif;
161
+ }
162
+ #app { height: 100vh; display: flex; flex-direction: column; }
163
+ #topbar {
164
+ display: flex;
165
+ justify-content: space-between;
166
+ gap: 16px;
167
+ padding: 12px 16px 10px;
168
+ border-bottom: 1px solid #263241;
169
+ background: rgba(15,19,26,0.94);
170
+ backdrop-filter: blur(10px);
171
+ }
172
+ #brand { display: flex; flex-direction: column; gap: 4px; min-width: 240px; }
173
+ #title { font-size: 15px; font-weight: 700; color: #f5f8fb; }
174
+ #subtitle { font-size: 11px; color: #b9c6d4; text-transform: uppercase; letter-spacing: 0.6px; }
175
+ #toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
176
+ .group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
177
+ .group:first-child { padding-left: 0; border-left: 0; }
178
+ :root {
179
+ --control-height: 36px;
180
+ }
181
+ .time-btn {
182
+ appearance: none;
183
+ border: 1px solid #263241;
184
+ background: rgba(24,30,39,0.85);
185
+ color: #8290a2;
186
+ font-size: 11px;
187
+ font-weight: 700;
188
+ letter-spacing: 0.5px;
189
+ text-transform: uppercase;
190
+ padding: 7px 10px;
191
+ border-radius: 8px;
192
+ cursor: pointer;
193
+ }
194
+ .time-btn.active {
195
+ color: #fff;
196
+ border-color: rgba(94,161,255,0.85);
197
+ background: linear-gradient(180deg, rgba(35,53,79,0.95), rgba(22,32,47,0.98));
198
+ }
199
+ .time-btn:disabled { opacity: 0.35; cursor: not-allowed; text-decoration: line-through; }
200
+ .indicator {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ min-height: var(--control-height);
205
+ padding: 0 13px;
206
+ border: 1px solid #263241;
207
+ border-radius: 10px;
208
+ background: rgba(20,25,33,0.86);
209
+ }
210
+ .indicator label {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ gap: 6px;
214
+ min-height: var(--control-height);
215
+ font-size: 11px;
216
+ font-weight: 700;
217
+ color: #d7e0ea;
218
+ text-transform: uppercase;
219
+ white-space: nowrap;
220
+ }
221
+ .indicator input[type="checkbox"] { width: 14px; height: 14px; accent-color: #5ea1ff; }
222
+ .indicator input[type="number"], .indicator select {
223
+ appearance: none;
224
+ border: 1px solid #263241;
225
+ background: rgba(10,14,19,0.92);
226
+ color: #d7e0ea;
227
+ border-radius: 8px;
228
+ padding: 7px 10px;
229
+ font-size: 11px;
230
+ outline: none;
231
+ width: 79px;
232
+ min-width: 79px;
233
+ height: calc(var(--control-height) - 8px);
234
+ }
235
+ .pair-toggle {
236
+ appearance: none;
237
+ width: 32px;
238
+ height: 32px;
239
+ display: inline-flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ border: 1px solid #263241;
243
+ border-radius: 999px;
244
+ background: radial-gradient(circle at 30% 30%, rgba(94,161,255,0.18), rgba(10,14,19,0.96));
245
+ color: #d7e0ea;
246
+ cursor: pointer;
247
+ padding: 0;
248
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03);
249
+ transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, color 120ms ease;
250
+ }
251
+ .pair-toggle:hover {
252
+ transform: translateY(-1px);
253
+ border-color: rgba(94,161,255,0.85);
254
+ color: #fff;
255
+ }
256
+ .pair-toggle:active {
257
+ transform: translateY(0);
258
+ }
259
+ .pair-toggle svg {
260
+ width: 17px;
261
+ height: 17px;
262
+ fill: currentColor;
263
+ display: block;
264
+ }
265
+ .pair-toggle.is-inverse {
266
+ color: #5ea1ff;
267
+ border-color: rgba(94,161,255,0.45);
268
+ }
269
+ .indicator .step-stack {
270
+ display: inline-flex;
271
+ flex-direction: column;
272
+ vertical-align: middle;
273
+ margin-left: 2px;
274
+ }
275
+ .indicator .step-btn {
276
+ appearance: none;
277
+ display: block;
278
+ width: 24px;
279
+ height: 13px;
280
+ border: 1px solid #3d4a5a;
281
+ background: #1a2332;
282
+ color: #8b949e;
283
+ font-size: 9px;
284
+ line-height: 1;
285
+ cursor: pointer;
286
+ padding: 0;
287
+ margin: 0;
288
+ }
289
+ .indicator .step-btn:first-child {
290
+ border-radius: 3px 3px 0 0;
291
+ border-bottom: none;
292
+ }
293
+ .indicator .step-btn:last-child {
294
+ border-radius: 0 0 3px 3px;
295
+ }
296
+ .indicator .step-btn:hover {
297
+ background: #263241;
298
+ }
299
+ .indicator .step-btn:active {
300
+ background: #3d4a5a;
301
+ }
302
+ .indicator input[type="text"] {
303
+ appearance: none;
304
+ border: 1px solid #263241;
305
+ background: rgba(6,8,12,0.95);
306
+ color: #d7e0ea;
307
+ border-radius: 8px;
308
+ padding: 7px 4px;
309
+ font-size: 12px;
310
+ outline: none;
311
+ width: 44px;
312
+ text-align: center;
313
+ }
314
+ .indicator input[type="text"]:focus {
315
+ border-color: #5ea1ff;
316
+ }
317
+ #ama-fast {
318
+ width: 38px;
319
+ }
320
+ .scale-toggle {
321
+ appearance: none;
322
+ min-width: 76px;
323
+ height: 32px;
324
+ display: inline-flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ border: 1px solid #263241;
328
+ border-radius: 999px;
329
+ background: rgba(10,14,19,0.92);
330
+ color: #d7e0ea;
331
+ font-size: 11px;
332
+ font-weight: 700;
333
+ letter-spacing: 0.45px;
334
+ text-transform: uppercase;
335
+ cursor: pointer;
336
+ padding: 0 11px;
337
+ transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, color 120ms ease;
338
+ }
339
+ .scale-toggle:hover {
340
+ transform: translateY(-1px);
341
+ border-color: rgba(94,161,255,0.85);
342
+ color: #fff;
343
+ }
344
+ .scale-toggle:active {
345
+ transform: translateY(0);
346
+ }
347
+ .scale-toggle.is-linear {
348
+ color: #5ea1ff;
349
+ border-color: rgba(94,161,255,0.45);
350
+ }
351
+ .reset-btn {
352
+ appearance: none;
353
+ border: 1px solid #263241;
354
+ background: rgba(24,30,39,0.85);
355
+ color: #d7e0ea;
356
+ font-size: 11px;
357
+ font-weight: 700;
358
+ letter-spacing: 0.45px;
359
+ text-transform: uppercase;
360
+ padding: 6px 11px;
361
+ border-radius: 8px;
362
+ cursor: pointer;
363
+ }
364
+ .reset-btn:hover {
365
+ border-color: rgba(94,161,255,0.85);
366
+ color: #fff;
367
+ }
368
+ .indicator .tag { font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
369
+ #chart-shell { position: relative; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
370
+ #price-chart { position: relative; flex: 1 1 auto; min-height: 0; }
371
+ #volume-chart { position: relative; flex: 0 0 19%; min-height: 0; border-top: 1px solid #263241; }
372
+ #legend {
373
+ position: absolute;
374
+ top: 14px;
375
+ left: 14px;
376
+ z-index: 4;
377
+ padding: 10px 12px;
378
+ min-width: 280px;
379
+ max-width: calc(100vw - 28px);
380
+ border: 1px solid #263241;
381
+ border-radius: 12px;
382
+ background: rgba(10,14,20,0.72);
383
+ backdrop-filter: blur(10px);
384
+ }
385
+ .legend-line { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size: 11px; }
386
+ .legend-line + .legend-line { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.06); }
387
+ .legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
388
+ .legend-dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
389
+ .legend-label { color: #8290a2; text-transform: uppercase; letter-spacing: 0.45px; font-size: 10px; }
390
+ .legend-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #eef4fb; font-weight: 700; }
391
+ .status { display: inline-flex; align-items: center; gap: 8px; font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
392
+ .pill { border: 1px solid rgba(255,255,255,0.06); border-radius: 999px; padding: 4px 8px; background: rgba(255,255,255,0.03); }
393
+ .uplot { background: #0b0f14; }
394
+ .u-cursor-x { border-left: 1px dashed rgba(255,255,255,0.3) !important; }
395
+ .u-cursor-y { border-top: 1px dashed rgba(255,255,255,0.3) !important; display: none; }
396
+ .is-hovered .u-cursor-y { display: block; }
397
+ @media (max-width: 980px) {
398
+ #topbar { flex-direction: column; align-items: flex-start; }
399
+ #toolbar { justify-content: flex-start; }
400
+ .group { padding-left: 0; border-left: 0; flex-wrap: wrap; }
401
+ #legend { min-width: 0; max-width: calc(100vw - 24px); }
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div id="app">
407
+ <div id="topbar">
408
+ <div id="brand">
409
+ <div id="title">${escapeHtml(title)}</div>
410
+ <div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
411
+ </div>
412
+ <div id="toolbar">
413
+ <div class="group" id="tf-group">
414
+ ${timeframes.map((item) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
415
+ </div>
416
+ <div class="group">
417
+ <div class="indicator">
418
+ <span class="tag">pair</span>
419
+ <button type="button" class="pair-toggle${defaultPairMode === 'inverse' ? ' is-inverse' : ''}" id="pair-toggle" aria-label="Swap pair orientation" title="Swap pair orientation" data-pair-mode="${escapeHtml(defaultPairMode)}">
420
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
421
+ <path d="M6.5 7H19v2H6.5v3L2 8.5 6.5 5v2Zm11 10H5v-2h12.5v-3L22 15.5 17.5 19v-2Z"/>
422
+ </svg>
423
+ </button>
424
+ </div>
425
+ <div class="indicator">
426
+ <span class="tag">scale</span>
427
+ <button type="button" class="scale-toggle${defaults.priceScale === 'linear' ? ' is-linear' : ''}" id="scale-toggle" data-scale="${escapeHtml(defaults.priceScale)}" aria-label="Toggle price scale" title="Toggle price scale">${defaults.priceScale === 'linear' ? 'Linear' : 'Log'}</button>
428
+ </div>
429
+ <div class="indicator">
430
+ <label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
431
+ <span class="tag">period</span>
432
+ <input type="text" inputmode="numeric" id="sma-period" value="${defaults.smaPeriod}">
433
+ <span class="step-stack">
434
+ <button type="button" class="step-btn" id="sma-period-inc">▲</button>
435
+ <button type="button" class="step-btn" id="sma-period-dec">▼</button>
436
+ </span>
437
+ </div>
438
+ <div class="indicator">
439
+ <label><input type="checkbox" id="vwap-toggle"${defaults.vwapEnabled ? ' checked' : ''}> VWMA</label>
440
+ <span class="tag">bars</span>
441
+ <input type="text" inputmode="numeric" id="vwap-bars" value="${defaults.vwapBars}">
442
+ <span class="step-stack">
443
+ <button type="button" class="step-btn" id="vwap-bars-inc">▲</button>
444
+ <button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
445
+ </span>
446
+ </div>
447
+ <div class="indicator">
448
+ <label><input type="checkbox" id="ama-toggle"${defaults.amaEnabled ? ' checked' : ''}> AMA</label>
449
+ <span class="tag">er</span>
450
+ <input type="text" inputmode="numeric" id="ama-er" value="${defaults.amaDefaults.erPeriod}">
451
+ <span class="step-stack">
452
+ <button type="button" class="step-btn" id="ama-er-inc">▲</button>
453
+ <button type="button" class="step-btn" id="ama-er-dec">▼</button>
454
+ </span>
455
+ <span class="tag">fast</span>
456
+ <input type="text" inputmode="decimal" id="ama-fast" value="${defaults.amaDefaults.fastPeriod}">
457
+ <span class="step-stack">
458
+ <button type="button" class="step-btn" id="ama-fast-inc">▲</button>
459
+ <button type="button" class="step-btn" id="ama-fast-dec">▼</button>
460
+ </span>
461
+ <span class="tag">slow</span>
462
+ <input type="text" inputmode="decimal" id="ama-slow" value="${defaults.amaDefaults.slowPeriod}">
463
+ <span class="step-stack">
464
+ <button type="button" class="step-btn" id="ama-slow-inc">▲</button>
465
+ <button type="button" class="step-btn" id="ama-slow-dec">▼</button>
466
+ </span>
467
+ <button type="button" class="reset-btn" id="ama-reset">Reset</button>
468
+ </div>
469
+ <div class="indicator" style="margin-top:4px">
470
+ <label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
471
+ <input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:120px;vertical-align:middle" disabled>
472
+ <span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
473
+ </div>
474
+ </div>
475
+ <div class="group"></div>
476
+ </div>
477
+ </div>
478
+ <div id="chart-shell">
479
+ <div id="legend">
480
+ <div class="legend-line">
481
+ <span class="legend-item"><span class="legend-label">Time</span> <span class="legend-value" id="legend-time">-</span></span>
482
+ <span class="legend-item"><span class="legend-label">C</span> <span class="legend-value" id="legend-close">-</span></span>
483
+ <span class="legend-item"><span class="legend-label">Delta</span> <span class="legend-value" id="legend-delta">-</span></span>
484
+ <span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume">-</span></span>
485
+ <span class="legend-item"><span class="legend-label">Scale</span> <span class="legend-value" id="legend-scale">-</span></span>
486
+ </div>
487
+ <div class="legend-line">
488
+ <span class="legend-item"><span class="legend-dot" style="background:#f59e0b"></span><span class="legend-label">SMA</span> <span class="legend-value" id="legend-sma">-</span></span>
489
+ <span class="legend-item"><span class="legend-dot" style="background:#2dd4bf"></span><span class="legend-label">AMA</span> <span class="legend-value" id="legend-ama">-</span></span>
490
+ <span class="legend-item"><span class="legend-dot" style="background:#22c55e"></span><span class="legend-label">AMA Init</span> <span class="legend-value" id="legend-sma-init">-</span></span>
491
+ <span class="legend-item"><span class="legend-dot" style="background:#a855f7"></span><span class="legend-label">Off Init</span> <span class="legend-value" id="legend-off-init">-</span></span>
492
+ <span class="legend-item"><span class="legend-dot" style="background:#a855f7"></span><span class="legend-label">AMA Off</span> <span class="legend-value" id="legend-ama-off">-</span></span>
493
+
494
+ <span class="legend-item"><span class="legend-dot" style="background:#93c5fd"></span><span class="legend-label">VWMA</span> <span class="legend-value" id="legend-vwap">-</span></span>
495
+ </div>
496
+ </div>
497
+ <div id="price-chart"></div>
498
+ <div id="volume-chart"></div>
499
+ </div>
500
+ </div>
501
+
502
+ <script id="payload" type="application/json">${serializeJsonForScript(payload)}</script>
503
+ <script>
504
+ (function () {
505
+ const payload = JSON.parse(document.getElementById('payload').textContent);
506
+ const priceEl = document.getElementById('price-chart');
507
+ const volumeEl = document.getElementById('volume-chart');
508
+ const baseCandles = Array.isArray(payload.candles) ? payload.candles.slice() : [];
509
+ const baseCloseValues = baseCandles.map((c) => c.close);
510
+ const timeframes = Array.isArray(payload.timeframes) ? payload.timeframes.slice() : [];
511
+ const timeframeMap = new Map(timeframes.map((item) => [item.label, item]));
512
+ const STORAGE_KEY = 'dexbot2-tradingview-uplot-v2';
513
+
514
+ const state = loadState();
515
+ let currentTimeframe = state.timeframe || payload.defaultTimeframe || '1h';
516
+ let currentSmaEnabled = state.smaEnabled ?? !!payload.smaEnabled;
517
+ let currentSmaPeriod = Number.isFinite(state.smaPeriod) ? state.smaPeriod : Number(payload.smaPeriod || 500);
518
+ let currentAmaEnabled = state.amaEnabled ?? !!payload.amaEnabled;
519
+ let currentAmaErPeriod = Number.isFinite(state.amaErPeriod)
520
+ ? Number(state.amaErPeriod)
521
+ : Number(payload.amaDefaults?.erPeriod || ${MARKET_ADAPTER.AMAS.AMA3.erPeriod});
522
+ let currentAmaFastPeriod = Number.isFinite(state.amaFastPeriod)
523
+ ? Number(state.amaFastPeriod)
524
+ : Number(payload.amaDefaults?.fastPeriod || ${MARKET_ADAPTER.AMAS.AMA3.fastPeriod});
525
+ let currentAmaSlowPeriod = Number.isFinite(state.amaSlowPeriod)
526
+ ? Number(state.amaSlowPeriod)
527
+ : Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod});
528
+ let currentVwapEnabled = state.vwapEnabled ?? !!payload.vwapEnabled;
529
+ let currentVwapBars = Number.isFinite(state.vwapBars) ? state.vwapBars : Number(payload.vwapBars || 500);
530
+ let currentAmaInitOffsetEnabled = false;
531
+ let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
532
+ let currentPriceScale = state.priceScale || payload.priceScale || 'log';
533
+ let currentPairMode = state.pairMode === 'inverse' ? 'inverse' : (payload.defaultPairMode === 'inverse' ? 'inverse' : 'normal');
534
+ let currentCandles = [];
535
+ let currentSeriesState = null;
536
+ let currentSeriesCandles = baseCandles;
537
+ let currentSeriesCloseValues = baseCloseValues;
538
+ let currentOpen = [];
539
+ let currentHigh = [];
540
+ let currentLow = [];
541
+ let currentClose = [];
542
+ let currentDisplayCandles = [];
543
+ let currentPriceData = [];
544
+ let currentVolumeData = [];
545
+ let currentSma = [];
546
+ let currentAma = [];
547
+ let currentVwap = [];
548
+ let currentSmaInit = [];
549
+ let currentSmaInitOff = [];
550
+ let currentAmaOff = [];
551
+ let priceChart = null;
552
+ let volumeChart = null;
553
+ let lastRenderedPriceScale = null;
554
+ let charts = [];
555
+ let chartEventsBound = false;
556
+ let pendingRange = null;
557
+ let pendingRangeRaf = 0;
558
+ let smaWorker = null;
559
+ let smaWorkerJob = null;
560
+ let smaWorkerStartRaf = 0;
561
+ let smaWorkerPaintRaf = 0;
562
+ let smaWorkerSeq = 0;
563
+ const aggregateCache = new Map();
564
+ const smaCache = new Map();
565
+ const amaCache = new Map();
566
+ const vwapCache = new Map();
567
+ const seriesCache = new Map();
568
+
569
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
570
+ function fmtPrice(v) {
571
+ if (v == null || !Number.isFinite(v)) return '-';
572
+ const abs = Math.abs(v);
573
+ if (abs >= 1000) return v.toFixed(2);
574
+ if (abs >= 100) return v.toFixed(3);
575
+ if (abs >= 1) return v.toFixed(4);
576
+ return v.toPrecision(6);
577
+ }
578
+ function fmtVolume(v) {
579
+ if (v == null || !Number.isFinite(v)) return '-';
580
+ const abs = Math.abs(v);
581
+ if (abs >= 1e9) return (v / 1e9).toFixed(2) + 'B';
582
+ if (abs >= 1e6) return (v / 1e6).toFixed(2) + 'M';
583
+ if (abs >= 1e3) return (v / 1e3).toFixed(2) + 'K';
584
+ return v.toFixed(2);
585
+ }
586
+ function candleDirection(open, close) {
587
+ if (close > open) return 'up';
588
+ if (close < open) return 'down';
589
+ return 'flat';
590
+ }
591
+ function fmtTime(ts) {
592
+ if (!Number.isFinite(ts)) return '-';
593
+ const d = new Date(ts * 1000);
594
+ return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
595
+ }
596
+ function pad2(n) {
597
+ return String(n).padStart(2, '0');
598
+ }
599
+ function formatTimeLabel(tsSec, spanSec) {
600
+ const d = new Date(tsSec * 1000);
601
+ if (!Number.isFinite(spanSec)) spanSec = 0;
602
+ if (spanSec >= 365 * 24 * 3600 * 2) {
603
+ return String(d.getUTCFullYear());
604
+ }
605
+ if (spanSec >= 90 * 24 * 3600) {
606
+ return d.toLocaleString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' });
607
+ }
608
+ if (spanSec >= 14 * 24 * 3600) {
609
+ return d.toLocaleString(undefined, { month: 'short', day: '2-digit', timeZone: 'UTC' });
610
+ }
611
+ return d.toLocaleString(undefined, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' });
612
+ }
613
+ function makeTimeAxis(showLabels) {
614
+ return {
615
+ show: true,
616
+ size: showLabels ? 24 : 14,
617
+ stroke: '#ffffff',
618
+ grid: { stroke: '#1c2128' },
619
+ ticks: { stroke: '#30363d', width: 1 },
620
+ font: '11px Segoe UI, sans-serif',
621
+ values: showLabels ? (u, vals) => {
622
+ const xScale = u.scales.x || {};
623
+ const spanSec = Number.isFinite(xScale.min) && Number.isFinite(xScale.max)
624
+ ? Math.max(0, xScale.max - xScale.min)
625
+ : 0;
626
+ return vals.map((ts) => formatTimeLabel(ts, spanSec));
627
+ } : () => [],
628
+ };
629
+ }
630
+ function loadState() {
631
+ try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch (e) { return {}; }
632
+ }
633
+ function saveState() {
634
+ try {
635
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
636
+ timeframe: currentTimeframe,
637
+ smaEnabled: currentSmaEnabled,
638
+ smaPeriod: currentSmaPeriod,
639
+ amaEnabled: currentAmaEnabled,
640
+ amaErPeriod: currentAmaErPeriod,
641
+ amaFastPeriod: currentAmaFastPeriod,
642
+ amaSlowPeriod: currentAmaSlowPeriod,
643
+ vwapEnabled: currentVwapEnabled,
644
+ vwapBars: currentVwapBars,
645
+ amaInitOffset: currentAmaInitOffset,
646
+ priceScale: currentPriceScale,
647
+ pairMode: currentPairMode,
648
+ }));
649
+ } catch (e) {}
650
+ }
651
+ function normalizePairMode(mode) {
652
+ return String(mode).toLowerCase() === 'inverse' ? 'inverse' : 'normal';
653
+ }
654
+ function getPairLabel(mode = currentPairMode) {
655
+ return normalizePairMode(mode) === 'inverse'
656
+ ? (payload.pairLabelInverse || (payload.assetLabelB + '/' + payload.assetLabelA))
657
+ : (payload.pairLabelNormal || (payload.assetLabelA + '/' + payload.assetLabelB));
658
+ }
659
+ function refreshSubtitle() {
660
+ const subtitleEl = document.getElementById('subtitle');
661
+ if (!subtitleEl) return;
662
+ subtitleEl.textContent = payload.poolLabel + ' · ' + getPairLabel() + ' · ' + payload.intervalLabel + ' base · indicators from 1h · volume · uPlot';
663
+ }
664
+ function snapInitOffset(value) {
665
+ const next = Math.round(Number(value) || 0);
666
+ return Math.abs(next) <= 2 ? 0 : next;
667
+ }
668
+ function setActivePairMode(mode) {
669
+ const label = normalizePairMode(mode);
670
+ const btn = document.getElementById('pair-toggle');
671
+ if (!btn) return;
672
+ btn.dataset.pairMode = label;
673
+ btn.classList.toggle('is-inverse', label === 'inverse');
674
+ btn.title = label === 'inverse' ? 'Swap to A/B' : 'Swap to B/A';
675
+ btn.setAttribute('aria-label', label === 'inverse' ? 'Swap to A/B' : 'Swap to B/A');
676
+ }
677
+ function invertCandle(candle) {
678
+ const open = Number(candle?.open);
679
+ const high = Number(candle?.high);
680
+ const low = Number(candle?.low);
681
+ const close = Number(candle?.close);
682
+ if (![open, high, low, close].every(Number.isFinite) || open <= 0 || high <= 0 || low <= 0 || close <= 0) return null;
683
+ const volume = Number(candle?.volume);
684
+ return {
685
+ time: candle.time,
686
+ open: 1 / open,
687
+ high: 1 / low,
688
+ low: 1 / high,
689
+ close: 1 / close,
690
+ // Convert base-asset volume into the inverse pair's base units approximately.
691
+ volume: Number.isFinite(volume) ? volume * close : 0,
692
+ };
693
+ }
694
+ function getSeriesState(mode = currentPairMode) {
695
+ const key = normalizePairMode(mode);
696
+ if (seriesCache.has(key)) return seriesCache.get(key);
697
+ const candles = key === 'inverse'
698
+ ? baseCandles.map(invertCandle).filter(Boolean)
699
+ : baseCandles.slice();
700
+ const state = {
701
+ candles,
702
+ closeValues: candles.map((c) => c.close),
703
+ };
704
+ seriesCache.set(key, state);
705
+ return state;
706
+ }
707
+ function aggregateCandles(rows, seconds) {
708
+ const bucketSec = Math.max(1, Math.round(seconds || 3600));
709
+ const cacheKey = currentPairMode + '|' + bucketSec;
710
+ if (aggregateCache.has(cacheKey)) return aggregateCache.get(cacheKey);
711
+ const out = [];
712
+ const idxs = [];
713
+ let cur = null;
714
+ let curBucket = null;
715
+ let curIdx = -1;
716
+ for (let i = 0; i < rows.length; i++) {
717
+ const row = rows[i];
718
+ const ts = Number(row.time);
719
+ if (!Number.isFinite(ts)) continue;
720
+ const bucket = Math.floor(ts / bucketSec) * bucketSec;
721
+ if (!cur || bucket !== curBucket) {
722
+ if (cur) {
723
+ out.push(cur);
724
+ idxs.push(curIdx);
725
+ }
726
+ curBucket = bucket;
727
+ curIdx = i;
728
+ cur = { time: bucket, open: row.open, high: row.high, low: row.low, close: row.close, volume: row.volume || 0 };
729
+ } else {
730
+ cur.high = Math.max(cur.high, row.high);
731
+ cur.low = Math.min(cur.low, row.low);
732
+ cur.close = row.close;
733
+ cur.volume += row.volume || 0;
734
+ curIdx = i;
735
+ }
736
+ }
737
+ if (cur) {
738
+ out.push(cur);
739
+ idxs.push(curIdx);
740
+ }
741
+ const result = { candles: out, idxs };
742
+ aggregateCache.set(cacheKey, result);
743
+ return result;
744
+ }
745
+ function sampleSeriesByIndex(series, idxs) {
746
+ if (!Array.isArray(series) || !Array.isArray(idxs)) return [];
747
+ return idxs.map((idx) => (Number.isFinite(idx) && series[idx] != null ? series[idx] : null));
748
+ }
749
+ function deriveDisplayCandles(candles) {
750
+ if (!Array.isArray(candles) || candles.length === 0) return [];
751
+ return candles.map((c, i) => {
752
+ const prevClose = i > 0 ? candles[i - 1].close : c.close;
753
+ const open = Number.isFinite(prevClose) ? prevClose : c.close;
754
+ const close = c.close;
755
+ return {
756
+ time: c.time,
757
+ open,
758
+ close,
759
+ volume: c.volume,
760
+ };
761
+ });
762
+ }
763
+ function computeSMA(candles, period) {
764
+ const safePeriod = Math.max(1, Math.round(period));
765
+ const cacheKey = currentPairMode + '|' + safePeriod + '|' + candles.length;
766
+ if (smaCache.has(cacheKey)) return smaCache.get(cacheKey);
767
+ const out = [];
768
+ let sum = 0;
769
+ const window = new Array(safePeriod);
770
+ let head = 0;
771
+ let count = 0;
772
+ for (const candle of candles) {
773
+ const price = candle.close;
774
+ if (count < safePeriod) {
775
+ window[count] = price;
776
+ sum += price;
777
+ count++;
778
+ out.push(count === safePeriod ? (sum / safePeriod) : null);
779
+ continue;
780
+ }
781
+ sum -= window[head];
782
+ window[head] = price;
783
+ sum += price;
784
+ head = (head + 1) % safePeriod;
785
+ out.push(sum / safePeriod);
786
+ }
787
+ smaCache.set(cacheKey, out);
788
+ return out;
789
+ }
790
+ function getSmaCacheKey(period) {
791
+ return currentPairMode + '|' + Math.max(1, Math.round(period)) + '|' + currentSeriesCloseValues.length;
792
+ }
793
+ function createSMAWorker() {
794
+ if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined') return null;
795
+ const workerSource = [
796
+ 'self.onmessage = function(e) {',
797
+ ' const data = e.data || {};',
798
+ ' const id = data.id;',
799
+ ' const key = data.key;',
800
+ ' const closes = Array.isArray(data.closes) ? data.closes : [];',
801
+ ' const period = Math.max(1, Math.round(Number(data.period) || 1));',
802
+ ' const out = new Array(closes.length);',
803
+ ' const window = new Array(period);',
804
+ ' let sum = 0;',
805
+ ' let head = 0;',
806
+ ' let count = 0;',
807
+ ' for (let i = 0; i < closes.length; i++) {',
808
+ ' const price = Number(closes[i]);',
809
+ ' if (count < period) {',
810
+ ' window[count] = price;',
811
+ ' sum += price;',
812
+ ' count++;',
813
+ ' out[i] = count === period ? (sum / period) : null;',
814
+ ' continue;',
815
+ ' }',
816
+ ' sum -= window[head];',
817
+ ' window[head] = price;',
818
+ ' sum += price;',
819
+ ' head = (head + 1) % period;',
820
+ ' out[i] = sum / period;',
821
+ ' }',
822
+ ' self.postMessage({ id, key, series: out });',
823
+ '};',
824
+ ].join('\\n');
825
+ const blob = new Blob([workerSource], { type: 'text/javascript' });
826
+ const worker = new Worker(URL.createObjectURL(blob));
827
+ worker.onmessage = (e) => {
828
+ const msg = e && e.data ? e.data : {};
829
+ if (!smaWorkerJob || msg.id !== smaWorkerJob.id || msg.key !== smaWorkerJob.key) return;
830
+ smaCache.set(msg.key, Array.isArray(msg.series) ? msg.series : []);
831
+ smaWorkerJob = null;
832
+ if (currentSmaEnabled && msg.key === getSmaCacheKey(currentSmaPeriod)) {
833
+ rerender(false);
834
+ }
835
+ };
836
+ worker.onerror = () => {
837
+ try { worker.terminate(); } catch (e) {}
838
+ smaWorker = null;
839
+ smaWorkerJob = null;
840
+ };
841
+ return worker;
842
+ }
843
+ function clearSMAWorker() {
844
+ if (smaWorkerStartRaf) {
845
+ cancelAnimationFrame(smaWorkerStartRaf);
846
+ smaWorkerStartRaf = 0;
847
+ }
848
+ if (smaWorkerPaintRaf) {
849
+ cancelAnimationFrame(smaWorkerPaintRaf);
850
+ smaWorkerPaintRaf = 0;
851
+ }
852
+ if (!smaWorker) return;
853
+ try { smaWorker.terminate(); } catch (e) {}
854
+ smaWorker = null;
855
+ smaWorkerJob = null;
856
+ }
857
+ function requestSMA(period) {
858
+ const safePeriod = Math.max(1, Math.round(period));
859
+ const key = getSmaCacheKey(safePeriod);
860
+ if (smaCache.has(key)) return;
861
+ if (smaWorkerJob && smaWorkerJob.key === key) return;
862
+ if (smaWorkerJob && smaWorkerJob.key !== key) clearSMAWorker();
863
+ if (!smaWorker) smaWorker = createSMAWorker();
864
+ if (!smaWorker) {
865
+ smaCache.set(key, computeSMA(currentSeriesCandles, safePeriod));
866
+ if (currentSmaEnabled && key === getSmaCacheKey(currentSmaPeriod)) rerender(false);
867
+ return;
868
+ }
869
+ smaWorkerJob = { id: ++smaWorkerSeq, key };
870
+ if (smaWorkerStartRaf) cancelAnimationFrame(smaWorkerStartRaf);
871
+ if (smaWorkerPaintRaf) cancelAnimationFrame(smaWorkerPaintRaf);
872
+ smaWorkerStartRaf = requestAnimationFrame(() => {
873
+ smaWorkerStartRaf = 0;
874
+ smaWorkerPaintRaf = requestAnimationFrame(() => {
875
+ smaWorkerPaintRaf = 0;
876
+ if (!smaWorker || !smaWorkerJob || smaWorkerJob.key !== key || !currentSmaEnabled) return;
877
+ smaWorker.postMessage({
878
+ id: smaWorkerJob.id,
879
+ key,
880
+ period: safePeriod,
881
+ closes: currentSeriesCloseValues,
882
+ });
883
+ });
884
+ });
885
+ }
886
+ function currentAmaConfig() {
887
+ return {
888
+ erPeriod: Math.max(1, Math.round(currentAmaErPeriod)),
889
+ fastPeriod: Math.max(0.1, Number(currentAmaFastPeriod) || 0.1),
890
+ slowPeriod: Math.max(0.1, Number(currentAmaSlowPeriod) || 0.1),
891
+ };
892
+ }
893
+ function computeAMA(candles, cfg, initOffset = 0) {
894
+ const params = cfg || currentAmaConfig();
895
+ const erPeriod = Math.max(1, Math.round(params.erPeriod ?? 10));
896
+ const fastPeriod = Math.max(0.1, Number(params.fastPeriod ?? 2));
897
+ const slowPeriod = Math.max(0.1, Number(params.slowPeriod ?? 30));
898
+ const cacheKey = currentPairMode + '|' + erPeriod + '|' + fastPeriod + '|' + slowPeriod + '|' + candles.length + '|' + initOffset;
899
+ if (amaCache.has(cacheKey)) return amaCache.get(cacheKey);
900
+ const fastSC = 2 / (fastPeriod + 1);
901
+ const slowSC = 2 / (slowPeriod + 1);
902
+ const out = [];
903
+ const windowSize = erPeriod + 1;
904
+ const history = new Array(windowSize);
905
+ let head = 0;
906
+ let len = 0;
907
+ let volatility = 0;
908
+ let prev = null;
909
+ let smaSum = 0;
910
+ for (const candle of candles) {
911
+ const price = candle.close;
912
+ if (len < windowSize) {
913
+ history[(head + len) % windowSize] = price;
914
+ smaSum += price;
915
+ if (len > 0) {
916
+ volatility += Math.abs(price - history[(head + len - 1) % windowSize]);
917
+ }
918
+ len++;
919
+ if (len < windowSize) {
920
+ out.push(null);
921
+ continue;
922
+ }
923
+ prev = initOffset !== 0 ? (smaSum / windowSize) * (1 + initOffset / 100) : smaSum / windowSize;
924
+ out.push(prev);
925
+ continue;
926
+ }
927
+ const oldest = history[head];
928
+ const second = history[(head + 1) % windowSize];
929
+ const last = history[(head + windowSize - 1) % windowSize];
930
+ volatility += Math.abs(price - last) - Math.abs(second - oldest);
931
+ history[head] = price;
932
+ head = (head + 1) % windowSize;
933
+ const first = history[head];
934
+ const direction = Math.abs(price - first);
935
+ const er = volatility === 0 ? 0 : (direction / volatility);
936
+ const smoothing = Math.pow(er * (fastSC - slowSC) + slowSC, 2);
937
+ const ama = prev + smoothing * (price - prev);
938
+ prev = ama;
939
+ out.push(ama);
940
+ }
941
+ amaCache.set(cacheKey, out);
942
+ return out;
943
+ }
944
+ function computeVWMA(candles, bars) {
945
+ const safeBars = Math.max(1, Math.round(bars || 500));
946
+ const cacheKey = currentPairMode + '|' + safeBars + '|' + candles.length;
947
+ if (vwapCache.has(cacheKey)) return vwapCache.get(cacheKey);
948
+ const out = [];
949
+ let cumPV = 0;
950
+ let cumVol = 0;
951
+ const window = new Array(safeBars);
952
+ let head = 0;
953
+ let count = 0;
954
+ for (const candle of candles) {
955
+ const typical = (candle.high + candle.low + candle.close) / 3;
956
+ const volume = Number.isFinite(candle.volume) ? candle.volume : 0;
957
+ cumPV += typical * volume;
958
+ cumVol += volume;
959
+ if (count < safeBars) {
960
+ window[count] = { typical, volume };
961
+ count++;
962
+ } else {
963
+ const old = window[head];
964
+ cumPV -= old.typical * old.volume;
965
+ cumVol -= old.volume;
966
+ window[head] = { typical, volume };
967
+ head = (head + 1) % safeBars;
968
+ }
969
+ out.push(cumVol > 0 ? (cumPV / cumVol) : null);
970
+ }
971
+ vwapCache.set(cacheKey, out);
972
+ return out;
973
+ }
974
+ function lowerBound(arr, value) {
975
+ let lo = 0;
976
+ let hi = arr.length;
977
+ while (lo < hi) {
978
+ const mid = (lo + hi) >> 1;
979
+ if (arr[mid] < value) lo = mid + 1;
980
+ else hi = mid;
981
+ }
982
+ return lo;
983
+ }
984
+ function visiblePriceRange(u) {
985
+ if (!currentCandles.length) return null;
986
+ const xs = u.data[0];
987
+ const xScale = u.scales.x || {};
988
+ const minX = Number.isFinite(xScale.min) ? xScale.min : xs[0];
989
+ const maxX = Number.isFinite(xScale.max) ? xScale.max : xs[xs.length - 1];
990
+ let start = Math.max(0, lowerBound(xs, minX) - 1);
991
+ let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
992
+ let min = Infinity;
993
+ let max = -Infinity;
994
+ for (let i = start; i < end; i++) {
995
+ const lo = currentLow[i];
996
+ const hi = currentHigh[i];
997
+ if (Number.isFinite(lo) && lo > 0 && lo < min) min = lo;
998
+ if (Number.isFinite(hi) && hi > max) max = hi;
999
+ const s = currentSma[i];
1000
+ const a = currentAma[i];
1001
+ const v = currentVwap[i];
1002
+ if (Number.isFinite(s)) { if (s < min) min = s; if (s > max) max = s; }
1003
+ if (Number.isFinite(a)) { if (a < min) min = a; if (a > max) max = a; }
1004
+ if (Number.isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
1005
+ }
1006
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max <= 0) return null;
1007
+ if (min === max) return [min * 0.98, max * 1.02];
1008
+ return [min * 0.97, max * 1.03];
1009
+ }
1010
+ function visibleVolumeRange(u) {
1011
+ if (!currentCandles.length) return null;
1012
+ const xs = u.data[0];
1013
+ const xScale = u.scales.x || {};
1014
+ const minX = Number.isFinite(xScale.min) ? xScale.min : xs[0];
1015
+ const maxX = Number.isFinite(xScale.max) ? xScale.max : xs[xs.length - 1];
1016
+ let start = Math.max(0, lowerBound(xs, minX) - 1);
1017
+ let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
1018
+ let max = 0;
1019
+ for (let i = start; i < end; i++) {
1020
+ const v = currentCandles[i]?.volume;
1021
+ if (Number.isFinite(v) && v > max) max = v;
1022
+ }
1023
+ if (!Number.isFinite(max) || max <= 0) return [0, 1];
1024
+ return [0, max * 1.15];
1025
+ }
1026
+ function setActiveTimeframe(label) {
1027
+ document.querySelectorAll('.time-btn').forEach((btn) => btn.classList.toggle('active', btn.dataset.timeframe === label));
1028
+ }
1029
+ function setActivePriceScale(label) {
1030
+ const btn = document.getElementById('scale-toggle');
1031
+ if (!btn) return;
1032
+ const next = label === 'linear' ? 'linear' : 'log';
1033
+ btn.dataset.scale = next;
1034
+ btn.classList.toggle('is-linear', next === 'linear');
1035
+ btn.textContent = next === 'linear' ? 'Linear' : 'Log';
1036
+ btn.title = next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale';
1037
+ btn.setAttribute('aria-label', next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale');
1038
+ }
1039
+ function resetAmaDefaults() {
1040
+ currentAmaErPeriod = Number(payload.amaDefaults?.erPeriod || ${MARKET_ADAPTER.AMAS.AMA3.erPeriod});
1041
+ currentAmaFastPeriod = Number(payload.amaDefaults?.fastPeriod || ${MARKET_ADAPTER.AMAS.AMA3.fastPeriod});
1042
+ currentAmaSlowPeriod = Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod});
1043
+ setControls();
1044
+ rerender(false);
1045
+ }
1046
+ function setIndicatorSeriesVisible(seriesIndex, visible) {
1047
+ if (!priceChart) return;
1048
+ priceChart.setSeries(seriesIndex, { show: visible }, false, false);
1049
+ }
1050
+ function syncIndicatorSeriesVisibility() {
1051
+ if (!priceChart) return;
1052
+ setIndicatorSeriesVisible(5, currentSmaEnabled);
1053
+ setIndicatorSeriesVisible(6, currentAmaEnabled);
1054
+ setIndicatorSeriesVisible(7, currentVwapEnabled);
1055
+ setIndicatorSeriesVisible(8, currentAmaEnabled);
1056
+ setIndicatorSeriesVisible(9, currentAmaEnabled && currentAmaInitOffset !== 0);
1057
+ setIndicatorSeriesVisible(10, currentAmaEnabled && currentAmaInitOffset !== 0);
1058
+ }
1059
+ function hideSmaSeriesImmediate() {
1060
+ clearSMAWorker();
1061
+ currentSma = new Array(currentCandles.length).fill(null);
1062
+ if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
1063
+ currentPriceData[5] = currentSma;
1064
+ }
1065
+ setIndicatorSeriesVisible(5, false);
1066
+ refreshLegend();
1067
+ saveState();
1068
+ }
1069
+ function hideAmaSeriesImmediate() {
1070
+ currentAma = new Array(currentCandles.length).fill(null);
1071
+ currentAmaOff = new Array(currentCandles.length).fill(null);
1072
+ currentSmaInit = new Array(currentCandles.length).fill(null);
1073
+ currentSmaInitOff = new Array(currentCandles.length).fill(null);
1074
+ if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
1075
+ currentPriceData[6] = currentAma;
1076
+ currentPriceData[8] = currentSmaInit;
1077
+ currentPriceData[9] = currentSmaInitOff;
1078
+ currentPriceData[10] = currentAmaOff;
1079
+ }
1080
+ setIndicatorSeriesVisible(6, false);
1081
+ setIndicatorSeriesVisible(8, false);
1082
+ setIndicatorSeriesVisible(9, false);
1083
+ setIndicatorSeriesVisible(10, false);
1084
+ refreshLegend();
1085
+ saveState();
1086
+ }
1087
+ function hideVwapSeriesImmediate() {
1088
+ currentVwap = new Array(currentCandles.length).fill(null);
1089
+ if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
1090
+ currentPriceData[7] = currentVwap;
1091
+ }
1092
+ setIndicatorSeriesVisible(7, false);
1093
+ refreshLegend();
1094
+ saveState();
1095
+ }
1096
+ function updateLegend(idx) {
1097
+ const c = currentCandles[idx];
1098
+ if (!c) return;
1099
+ const smaKey = getSmaCacheKey(currentSmaPeriod);
1100
+ const smaPending = currentSmaEnabled && smaWorkerJob && smaWorkerJob.key === smaKey && !smaCache.has(smaKey);
1101
+ document.getElementById('legend-time').textContent = fmtTime(c.time);
1102
+ document.getElementById('legend-close').textContent = fmtPrice(c.close);
1103
+ const delta = Number.isFinite(c.open) && Number.isFinite(c.close) ? (c.close - c.open) : null;
1104
+ document.getElementById('legend-delta').textContent = Number.isFinite(delta)
1105
+ ? ((delta >= 0 ? '+' : '') + fmtPrice(delta))
1106
+ : '-';
1107
+ document.getElementById('legend-volume').textContent = fmtVolume(c.volume);
1108
+ document.getElementById('legend-scale').textContent = currentPriceScale === 'linear' ? 'Linear' : 'Log';
1109
+ document.getElementById('legend-sma').textContent = Number.isFinite(currentSma[idx]) ? fmtPrice(currentSma[idx]) : (smaPending ? '...' : '-');
1110
+ document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
1111
+ document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
1112
+ document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
1113
+ document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
1114
+ document.getElementById('legend-vwap').textContent = Number.isFinite(currentVwap[idx]) ? fmtPrice(currentVwap[idx]) : '-';
1115
+ }
1116
+ function candlePlugin() {
1117
+ function drawCandles(u) {
1118
+ u.ctx.save();
1119
+
1120
+ const offset = 0.5;
1121
+ u.ctx.translate(offset, offset);
1122
+
1123
+ const [iMin, iMax] = u.series[0].idxs;
1124
+
1125
+ for (let i = iMin; i <= iMax; i++) {
1126
+ const xVal = u.scales.x.distr == 2 ? i : u.data[0][i];
1127
+ const open = u.data[1][i];
1128
+ const high = u.data[2][i];
1129
+ const low = u.data[3][i];
1130
+ const close = u.data[4][i];
1131
+
1132
+ const x = u.valToPos(xVal, 'x', true);
1133
+ const openY = u.valToPos(open, 'y', true);
1134
+ const highY = u.valToPos(high, 'y', true);
1135
+ const lowY = u.valToPos(low, 'y', true);
1136
+ const closeY = u.valToPos(close, 'y', true);
1137
+ if (![x, openY, highY, lowY, closeY].every(Number.isFinite)) continue;
1138
+
1139
+ const nextX = i + 1 <= iMax ? u.valToPos(u.scales.x.distr == 2 ? (i + 1) : u.data[0][i + 1], 'x', true) : null;
1140
+ const prevX = i - 1 >= iMin ? u.valToPos(u.scales.x.distr == 2 ? (i - 1) : u.data[0][i - 1], 'x', true) : null;
1141
+ const spacing = Number.isFinite(nextX) ? (nextX - x) : (Number.isFinite(prevX) ? (x - prevX) : 12);
1142
+ // Wider bodies read better when zoomed in and reduce empty space between candles.
1143
+ const bodyW = Math.max(
1144
+ 4,
1145
+ Math.min(
1146
+ Math.abs(spacing) * 0.86,
1147
+ Math.max(18, Math.abs(spacing) * 0.94),
1148
+ ),
1149
+ );
1150
+ const dir = candleDirection(open, close);
1151
+ const bodyColor = dir === 'up' ? '#26a69a' : (dir === 'down' ? '#ef5350' : '#64748b');
1152
+ const wickColor = dir === 'up' ? '#b3fff3' : (dir === 'down' ? '#ffb3b3' : '#94a3b8');
1153
+ const bodyTop = Math.min(openY, closeY);
1154
+ const bodyBottom = Math.max(openY, closeY);
1155
+ const bodyH = Math.max(dir === 'flat' ? 2 : 3, Math.round(bodyBottom - bodyTop));
1156
+ const bodyX = Math.round(x - bodyW / 2);
1157
+ const bodyY = Math.round(bodyTop);
1158
+
1159
+ const wickW = Math.max(1, Math.min(5, Math.round(Math.abs(spacing) * 0.14)));
1160
+ u.ctx.strokeStyle = wickColor;
1161
+ u.ctx.lineWidth = wickW;
1162
+ u.ctx.beginPath();
1163
+ u.ctx.moveTo(x, highY);
1164
+ u.ctx.lineTo(x, lowY);
1165
+ u.ctx.stroke();
1166
+
1167
+ u.ctx.fillStyle = bodyColor;
1168
+ u.ctx.fillRect(bodyX, bodyY, Math.round(bodyW), bodyH);
1169
+ if (bodyW > 2 && bodyH > 2) {
1170
+ u.ctx.strokeStyle = dir === 'up' ? '#0f7f77' : (dir === 'down' ? '#a92d35' : '#475569');
1171
+ u.ctx.lineWidth = 1;
1172
+ u.ctx.strokeRect(bodyX + 0.5, bodyY + 0.5, Math.round(bodyW) - 1, bodyH - 1);
1173
+ }
1174
+ }
1175
+
1176
+ u.ctx.translate(-offset, -offset);
1177
+ u.ctx.restore();
1178
+ }
1179
+
1180
+ return {
1181
+ opts: (u, opts) => {
1182
+ uPlot.assign(opts, {
1183
+ cursor: { drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
1184
+ });
1185
+ opts.series.forEach((s, i) => {
1186
+ if (i > 0 && i <= 4) {
1187
+ s.paths = () => null;
1188
+ s.points = { show: false };
1189
+ s.stroke = 'transparent';
1190
+ }
1191
+ });
1192
+ },
1193
+ hooks: { drawAxes: [drawCandles] },
1194
+ };
1195
+ }
1196
+ function volumePlugin() {
1197
+ function drawVolumeBars(u) {
1198
+ u.ctx.save();
1199
+
1200
+ const offset = 0.5;
1201
+ u.ctx.translate(offset, offset);
1202
+
1203
+ const [iMin, iMax] = u.series[0].idxs;
1204
+ const xVals = u.data[0];
1205
+ const volVals = u.data[1];
1206
+
1207
+ for (let i = iMin; i <= iMax; i++) {
1208
+ const vol = volVals[i];
1209
+ if (!Number.isFinite(vol) || vol <= 0) continue;
1210
+
1211
+ const xVal = u.scales.x.distr == 2 ? i : xVals[i];
1212
+ const x = u.valToPos(xVal, 'x', true);
1213
+ const top = u.valToPos(vol, 'y', true);
1214
+ const bottom = u.valToPos(0, 'y', true);
1215
+ if (![x, top, bottom].every(Number.isFinite)) continue;
1216
+
1217
+ const nextX = i + 1 <= iMax ? u.valToPos(u.scales.x.distr == 2 ? (i + 1) : xVals[i + 1], 'x', true) : null;
1218
+ const prevX = i - 1 >= iMin ? u.valToPos(u.scales.x.distr == 2 ? (i - 1) : xVals[i - 1], 'x', true) : null;
1219
+ const spacing = Number.isFinite(nextX) ? (nextX - x) : (Number.isFinite(prevX) ? (x - prevX) : 12);
1220
+ const barW = Math.max(
1221
+ 4,
1222
+ Math.min(
1223
+ Math.abs(spacing) * 0.86,
1224
+ Math.max(18, Math.abs(spacing) * 0.94),
1225
+ ),
1226
+ );
1227
+
1228
+ const left = Math.round(x - barW / 2);
1229
+ const topY = Math.round(Math.min(top, bottom));
1230
+ const h = Math.max(1, Math.round(Math.abs(bottom - top)));
1231
+
1232
+ u.ctx.fillStyle = '#ffffff';
1233
+ u.ctx.fillRect(left, topY, Math.round(barW), h);
1234
+ }
1235
+
1236
+ u.ctx.translate(-offset, -offset);
1237
+ u.ctx.restore();
1238
+ }
1239
+
1240
+ return {
1241
+ opts: (u, opts) => {
1242
+ uPlot.assign(opts, {
1243
+ cursor: { drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
1244
+ });
1245
+ opts.series.forEach((s, i) => {
1246
+ if (i === 1) {
1247
+ s.paths = () => null;
1248
+ s.points = { show: false };
1249
+ s.stroke = 'transparent';
1250
+ s.fill = 'transparent';
1251
+ }
1252
+ });
1253
+ },
1254
+ hooks: { drawAxes: [drawVolumeBars] },
1255
+ };
1256
+ }
1257
+ function buildData() {
1258
+ const tf = timeframeMap.get(currentTimeframe) || timeframeMap.get(defaultTimeframe.label) || timeframes[0];
1259
+ currentSeriesState = getSeriesState(currentPairMode);
1260
+ currentSeriesCandles = currentSeriesState.candles;
1261
+ currentSeriesCloseValues = currentSeriesState.closeValues;
1262
+ const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600);
1263
+ currentCandles = aggregated.candles;
1264
+ currentDisplayCandles = deriveDisplayCandles(currentCandles);
1265
+ currentOpen = currentDisplayCandles.map((c) => c.open);
1266
+ currentHigh = currentCandles.map((c) => c.high);
1267
+ currentLow = currentCandles.map((c) => c.low);
1268
+ currentClose = currentDisplayCandles.map((c) => c.close);
1269
+ const baseAma = currentAmaEnabled ? computeAMA(currentSeriesCandles, currentAmaConfig()) : [];
1270
+ const baseAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0
1271
+ ? computeAMA(currentSeriesCandles, currentAmaConfig(), currentAmaInitOffset)
1272
+ : [];
1273
+ const baseVwap = currentVwapEnabled ? computeVWMA(currentSeriesCandles, currentVwapBars) : [];
1274
+ if (currentSmaEnabled) {
1275
+ const smaKey = getSmaCacheKey(currentSmaPeriod);
1276
+ const baseSma = smaCache.get(smaKey);
1277
+ if (baseSma) {
1278
+ currentSma = sampleSeriesByIndex(baseSma, aggregated.idxs);
1279
+ } else {
1280
+ currentSma = new Array(currentCandles.length).fill(null);
1281
+ requestSMA(currentSmaPeriod);
1282
+ }
1283
+ } else {
1284
+ currentSma = new Array(currentCandles.length).fill(null);
1285
+ }
1286
+ currentAma = currentAmaEnabled ? sampleSeriesByIndex(baseAma, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1287
+ currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1288
+ currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1289
+ const amaCfg = currentAmaConfig();
1290
+ const warmupInit = currentAmaEnabled && baseAma.length > amaCfg.erPeriod ? baseAma[amaCfg.erPeriod] : null;
1291
+ const baseSmaInit = warmupInit !== null
1292
+ ? currentSeriesCandles.map((c, i) => i <= amaCfg.erPeriod ? warmupInit : null)
1293
+ : new Array(currentSeriesCandles.length).fill(null);
1294
+ currentSmaInit = sampleSeriesByIndex(baseSmaInit, aggregated.idxs);
1295
+ const offsetInit = currentAmaEnabled && currentAmaInitOffset !== 0 && baseAmaOff.length > amaCfg.erPeriod
1296
+ ? baseAmaOff[amaCfg.erPeriod]
1297
+ : null;
1298
+ const baseSmaInitOff = offsetInit !== null
1299
+ ? currentSeriesCandles.map((c, i) => i <= amaCfg.erPeriod ? offsetInit : null)
1300
+ : new Array(currentSeriesCandles.length).fill(null);
1301
+ currentSmaInitOff = sampleSeriesByIndex(baseSmaInitOff, aggregated.idxs);
1302
+ currentPriceData = [
1303
+ currentCandles.map((c) => c.time),
1304
+ currentOpen,
1305
+ currentHigh,
1306
+ currentLow,
1307
+ currentClose,
1308
+ currentSma,
1309
+ currentAma,
1310
+ currentVwap,
1311
+ currentSmaInit,
1312
+ currentSmaInitOff,
1313
+ currentAmaOff,
1314
+ ];
1315
+ currentVolumeData = [
1316
+ currentCandles.map((c) => c.time),
1317
+ currentCandles.map((c) => c.volume),
1318
+ ];
1319
+ return {
1320
+ priceData: currentPriceData,
1321
+ volumeData: currentVolumeData,
1322
+ };
1323
+ }
1324
+ function makeChart() {
1325
+ const data = buildData();
1326
+ const isLogScale = currentPriceScale !== 'linear';
1327
+
1328
+ const priceOpts = {
1329
+ width: priceEl.clientWidth,
1330
+ height: priceEl.clientHeight,
1331
+ padding: [14, 8, 8, 8],
1332
+ legend: { show: false },
1333
+ select: { show: false },
1334
+ cursor: { sync: { key: STORAGE_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
1335
+ scales: {
1336
+ x: { time: true },
1337
+ y: {
1338
+ auto: true,
1339
+ distr: isLogScale ? 3 : 1,
1340
+ log: isLogScale ? 10 : undefined,
1341
+ range: (u, min, max) => {
1342
+ const vis = visiblePriceRange(u);
1343
+ if (vis) return vis;
1344
+ if (isLogScale) {
1345
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max <= 0) return [1, 10];
1346
+ } else if (!Number.isFinite(min) || !Number.isFinite(max)) {
1347
+ return [0, 1];
1348
+ }
1349
+ return [min * 0.97, max * 1.03];
1350
+ },
1351
+ },
1352
+ },
1353
+ series: [
1354
+ { label: 'Time' },
1355
+ { label: 'Open' },
1356
+ { label: 'High' },
1357
+ { label: 'Low' },
1358
+ { label: 'Close' },
1359
+ { label: 'SMA', stroke: '#ffffff', width: 2, points: { show: false }, spanGaps: true },
1360
+ { label: 'AMA', stroke: '#facc15', width: 2, points: { show: false }, spanGaps: true },
1361
+ { label: 'VWMA', stroke: '#93c5fd', width: 2, points: { show: false }, spanGaps: true },
1362
+ { label: 'AMA Init', stroke: '#22c55e', width: 1.5, dash: [6, 4], points: { show: false }, spanGaps: false },
1363
+ { label: 'Off Init', stroke: '#a855f7', width: 1.5, dash: [6, 4], points: { show: false }, spanGaps: false },
1364
+ { label: 'AMA Off', stroke: '#a855f7', width: 1.5, dash: [4, 4], points: { show: false }, spanGaps: false },
1365
+ ],
1366
+ axes: [
1367
+ makeTimeAxis(false),
1368
+ { scale: 'y', size: 62, stroke: '#ffffff', grid: { stroke: '#1c2128' }, ticks: { stroke: '#30363d', width: 1 }, values: (u, vals) => vals.map((v) => (v == null ? '' : fmtPrice(v))) },
1369
+ ],
1370
+ };
1371
+
1372
+ const plugin = candlePlugin();
1373
+ plugin.opts(null, priceOpts);
1374
+ priceOpts.plugins = [plugin];
1375
+ if (!priceChart) priceChart = new uPlot(priceOpts, data.priceData, priceEl);
1376
+ else priceChart.setData(data.priceData, false);
1377
+
1378
+ const volumeOpts = {
1379
+ width: volumeEl.clientWidth,
1380
+ height: volumeEl.clientHeight,
1381
+ padding: [6, 8, 8, 8],
1382
+ legend: { show: false },
1383
+ select: { show: false },
1384
+ cursor: { sync: { key: STORAGE_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
1385
+ scales: {
1386
+ x: { time: true },
1387
+ y: {
1388
+ auto: true,
1389
+ range: (u, min, max) => {
1390
+ const vis = visibleVolumeRange(u);
1391
+ if (vis) return vis;
1392
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= 0) return [0, 1];
1393
+ return [0, max * 1.15];
1394
+ },
1395
+ },
1396
+ },
1397
+ series: [
1398
+ { label: 'Time' },
1399
+ { label: 'Volume', stroke: '#ffffff', fill: '#ffffff', width: 1, points: { show: false } },
1400
+ ],
1401
+ axes: [
1402
+ makeTimeAxis(true),
1403
+ { scale: 'y', size: 62, stroke: '#ffffff', grid: { stroke: '#1c2128' }, ticks: { stroke: '#30363d', width: 1 }, values: (u, vals) => vals.map((v) => (v == null ? '' : fmtVolume(v))) },
1404
+ ],
1405
+ };
1406
+
1407
+ const volumePluginInst = volumePlugin();
1408
+ volumePluginInst.opts(null, volumeOpts);
1409
+ volumeOpts.plugins = [volumePluginInst];
1410
+ if (!volumeChart) volumeChart = new uPlot(volumeOpts, data.volumeData, volumeEl);
1411
+ else volumeChart.setData(data.volumeData, false);
1412
+
1413
+ charts = [priceChart, volumeChart];
1414
+ lastRenderedPriceScale = currentPriceScale;
1415
+ return priceChart;
1416
+ }
1417
+ function clampRange(min, max) {
1418
+ if (!currentCandles.length || !Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
1419
+ const first = currentCandles[0].time;
1420
+ const last = currentCandles[currentCandles.length - 1].time;
1421
+ let lo = min;
1422
+ let hi = max;
1423
+ if (lo < first) { hi += first - lo; lo = first; }
1424
+ if (hi > last) { lo -= hi - last; hi = last; }
1425
+ if (lo < first) lo = first;
1426
+ if (hi > last) hi = last;
1427
+ if (hi <= lo) return null;
1428
+ return { min: lo, max: hi };
1429
+ }
1430
+ function syncXRange(min, max) {
1431
+ pendingRange = clampRange(min, max);
1432
+ if (pendingRangeRaf) return;
1433
+ pendingRangeRaf = requestAnimationFrame(() => {
1434
+ const next = pendingRange;
1435
+ pendingRange = null;
1436
+ pendingRangeRaf = 0;
1437
+ if (!next || charts.length === 0) return;
1438
+ charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
1439
+ });
1440
+ }
1441
+ function bindWheelZoom(chart) {
1442
+ chart.root.addEventListener('wheel', (e) => {
1443
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
1444
+ e.preventDefault();
1445
+ const rect = chart.root.getBoundingClientRect();
1446
+ const center = chart.posToVal(e.clientX - rect.left, 'x');
1447
+ const s = chart.scales.x || {};
1448
+ const currMin = Number.isFinite(s.min) ? s.min : currentCandles[0].time;
1449
+ const currMax = Number.isFinite(s.max) ? s.max : currentCandles[currentCandles.length - 1].time;
1450
+ const span = currMax - currMin;
1451
+ const first = currentCandles[0]?.time;
1452
+ const last = currentCandles[currentCandles.length - 1]?.time;
1453
+ const fullSpan = Number.isFinite(first) && Number.isFinite(last) ? Math.max(1, last - first) : span;
1454
+ if (!Number.isFinite(span) || span <= 0) return;
1455
+ const factor = e.deltaY < 0 ? 0.85 : 1.15;
1456
+ const nextSpan = Math.max(1, Math.min(fullSpan, span * factor));
1457
+ const ratio = (center - currMin) / span;
1458
+ syncXRange(center - nextSpan * ratio, center - nextSpan * ratio + nextSpan);
1459
+ }, { passive: false });
1460
+ }
1461
+ function bindPan(chart) {
1462
+ let dragging = false;
1463
+ let startClientX = 0;
1464
+ let startMin = 0;
1465
+ let startMax = 0;
1466
+ const onMove = (e) => {
1467
+ if (!dragging) return;
1468
+ e.preventDefault();
1469
+ const rect = chart.root.getBoundingClientRect();
1470
+ const delta = chart.posToVal(e.clientX - rect.left, 'x') - chart.posToVal(startClientX - rect.left, 'x');
1471
+ syncXRange(startMin - delta, startMax - delta);
1472
+ };
1473
+ const endDrag = () => {
1474
+ if (!dragging) return;
1475
+ dragging = false;
1476
+ document.body.style.cursor = '';
1477
+ window.removeEventListener('mousemove', onMove);
1478
+ window.removeEventListener('mouseup', endDrag);
1479
+ };
1480
+ chart.root.addEventListener('mousedown', (e) => {
1481
+ if (!e || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
1482
+ const rect = chart.root.getBoundingClientRect();
1483
+ if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
1484
+ e.preventDefault();
1485
+ e.stopPropagation();
1486
+ dragging = true;
1487
+ startClientX = e.clientX;
1488
+ const s = chart.scales.x || {};
1489
+ startMin = Number.isFinite(s.min) ? s.min : currentCandles[0].time;
1490
+ startMax = Number.isFinite(s.max) ? s.max : currentCandles[currentCandles.length - 1].time;
1491
+ document.body.style.cursor = 'grabbing';
1492
+ window.addEventListener('mousemove', onMove);
1493
+ window.addEventListener('mouseup', endDrag, { once: true });
1494
+ });
1495
+ }
1496
+ function refreshLegend() {
1497
+ updateLegend(Math.max(0, currentCandles.length - 1));
1498
+ }
1499
+ function setControls() {
1500
+ setActivePairMode(currentPairMode);
1501
+ setActivePriceScale(currentPriceScale);
1502
+ document.getElementById('sma-toggle').checked = currentSmaEnabled;
1503
+ document.getElementById('sma-period').value = String(currentSmaPeriod);
1504
+ document.getElementById('ama-toggle').checked = currentAmaEnabled;
1505
+ document.getElementById('ama-er').value = String(currentAmaErPeriod);
1506
+ document.getElementById('ama-fast').value = currentAmaFastPeriod.toFixed(1);
1507
+ document.getElementById('ama-slow').value = currentAmaSlowPeriod.toFixed(1);
1508
+ document.getElementById('vwap-toggle').checked = currentVwapEnabled;
1509
+ document.getElementById('vwap-bars').value = String(currentVwapBars);
1510
+ document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
1511
+ document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
1512
+ document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
1513
+ document.getElementById('ama-init-offset-val').textContent = currentAmaInitOffset + '%';
1514
+ setActiveTimeframe(currentTimeframe);
1515
+ refreshSubtitle();
1516
+ }
1517
+ function rerender(keepRange = true) {
1518
+ const oldRange = keepRange && priceChart ? priceChart.scales.x : null;
1519
+ const needsRebuild = !priceChart || !volumeChart || lastRenderedPriceScale !== currentPriceScale;
1520
+ if (needsRebuild) {
1521
+ if (priceEl) priceEl.innerHTML = '';
1522
+ if (volumeEl) volumeEl.innerHTML = '';
1523
+ priceChart = null;
1524
+ volumeChart = null;
1525
+ charts = [];
1526
+ chartEventsBound = false;
1527
+ }
1528
+ const data = buildData();
1529
+ if (needsRebuild) {
1530
+ makeChart();
1531
+ } else {
1532
+ priceChart.setData(data.priceData, false);
1533
+ volumeChart.setData(data.volumeData, false);
1534
+ }
1535
+ syncIndicatorSeriesVisibility();
1536
+ if (!priceChart || !volumeChart) return;
1537
+ if (!chartEventsBound) {
1538
+ charts.forEach((chart) => {
1539
+ bindWheelZoom(chart);
1540
+ bindPan(chart);
1541
+ chart.over.addEventListener('mousemove', () => {
1542
+ if (chart.cursor.idx != null) updateLegend(chart.cursor.idx);
1543
+ });
1544
+ chart.over.addEventListener('mouseleave', refreshLegend);
1545
+ chart.root.addEventListener('mouseenter', () => chart.root.classList.add('is-hovered'));
1546
+ chart.root.addEventListener('mouseleave', () => chart.root.classList.remove('is-hovered'));
1547
+ });
1548
+ chartEventsBound = true;
1549
+ }
1550
+ if (oldRange && Number.isFinite(oldRange.min) && Number.isFinite(oldRange.max)) {
1551
+ const next = clampRange(oldRange.min, oldRange.max);
1552
+ if (next) charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
1553
+ }
1554
+ refreshLegend();
1555
+ saveState();
1556
+ }
1557
+
1558
+ setControls();
1559
+ rerender(false);
1560
+
1561
+ document.querySelectorAll('.time-btn').forEach((btn) => {
1562
+ btn.addEventListener('click', () => {
1563
+ if (btn.disabled) return;
1564
+ const label = btn.dataset.timeframe;
1565
+ if (!label || label === currentTimeframe) return;
1566
+ currentTimeframe = label;
1567
+ setActiveTimeframe(label);
1568
+ rerender(true);
1569
+ });
1570
+ });
1571
+
1572
+ const scaleToggle = document.getElementById('scale-toggle');
1573
+ if (scaleToggle) {
1574
+ scaleToggle.addEventListener('click', () => {
1575
+ currentPriceScale = currentPriceScale === 'linear' ? 'log' : 'linear';
1576
+ setActivePriceScale(currentPriceScale);
1577
+ rerender(false);
1578
+ });
1579
+ }
1580
+ const pairToggle = document.getElementById('pair-toggle');
1581
+ if (pairToggle) {
1582
+ pairToggle.addEventListener('click', () => {
1583
+ currentPairMode = currentPairMode === 'inverse' ? 'normal' : 'inverse';
1584
+ setControls();
1585
+ rerender(true);
1586
+ });
1587
+ }
1588
+
1589
+ const syncInputs = () => {
1590
+ currentSmaEnabled = document.getElementById('sma-toggle').checked;
1591
+ currentSmaPeriod = clamp(Math.round(Number(document.getElementById('sma-period').value) || 20), 1, 9999);
1592
+ currentAmaEnabled = document.getElementById('ama-toggle').checked;
1593
+ currentAmaErPeriod = clamp(Math.round(Number(document.getElementById('ama-er').value) || ${MARKET_ADAPTER.AMAS.AMA3.erPeriod}), 1, 999999);
1594
+ currentAmaFastPeriod = Math.round(clamp(Number(document.getElementById('ama-fast').value) || Number(payload.amaDefaults?.fastPeriod || ${MARKET_ADAPTER.AMAS.AMA3.fastPeriod}), 0.1, 999999) * 10) / 10;
1595
+ currentAmaSlowPeriod = Math.round(clamp(Number(document.getElementById('ama-slow').value) || Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod}), 0.1, 999999) * 10) / 10;
1596
+ currentAmaInitOffsetEnabled = document.getElementById('ama-init-offset-toggle').checked;
1597
+ currentAmaInitOffset = currentAmaInitOffsetEnabled ? snapInitOffset(document.getElementById('ama-init-offset').value) : 0;
1598
+ document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
1599
+ document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
1600
+ document.getElementById('ama-init-offset-val').textContent = currentAmaInitOffset + '%';
1601
+ currentVwapEnabled = document.getElementById('vwap-toggle').checked;
1602
+ currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
1603
+ setControls();
1604
+ rerender(false);
1605
+ };
1606
+
1607
+ document.getElementById('sma-toggle').addEventListener('change', () => {
1608
+ const nextEnabled = document.getElementById('sma-toggle').checked;
1609
+ if (currentSmaEnabled === nextEnabled) return;
1610
+ currentSmaEnabled = nextEnabled;
1611
+ setControls();
1612
+ if (!currentSmaEnabled) {
1613
+ hideSmaSeriesImmediate();
1614
+ return;
1615
+ }
1616
+ rerender(false);
1617
+ });
1618
+ document.getElementById('ama-toggle').addEventListener('change', () => {
1619
+ const nextEnabled = document.getElementById('ama-toggle').checked;
1620
+ if (currentAmaEnabled === nextEnabled) return;
1621
+ currentAmaEnabled = nextEnabled;
1622
+ setControls();
1623
+ if (!currentAmaEnabled) {
1624
+ hideAmaSeriesImmediate();
1625
+ return;
1626
+ }
1627
+ rerender(false);
1628
+ });
1629
+ document.getElementById('vwap-toggle').addEventListener('change', () => {
1630
+ const nextEnabled = document.getElementById('vwap-toggle').checked;
1631
+ if (currentVwapEnabled === nextEnabled) return;
1632
+ currentVwapEnabled = nextEnabled;
1633
+ setControls();
1634
+ if (!currentVwapEnabled) {
1635
+ hideVwapSeriesImmediate();
1636
+ return;
1637
+ }
1638
+ rerender(false);
1639
+ });
1640
+ document.getElementById('ama-init-offset-toggle').addEventListener('change', syncInputs);
1641
+ ['sma-period', 'ama-er', 'ama-fast', 'ama-slow', 'vwap-bars'].forEach((id) => {
1642
+ document.getElementById(id).addEventListener('change', syncInputs);
1643
+ document.getElementById(id).addEventListener('blur', syncInputs);
1644
+ });
1645
+ ['ama-er'].forEach((id) => {
1646
+ document.getElementById(id).addEventListener('input', syncInputs);
1647
+ });
1648
+ function makeStepper(id, step, lo, precision) {
1649
+ const dec = document.getElementById(id + '-dec');
1650
+ const inc = document.getElementById(id + '-inc');
1651
+ if (!dec || !inc) return;
1652
+ const input = document.getElementById(id);
1653
+ function stepVal(dir) {
1654
+ const val = parseFloat(input.value) || 0;
1655
+ let next = dir < 0 ? Math.max(lo, val - step) : val + step;
1656
+ if (precision === 0) next = Math.round(next);
1657
+ else if (precision > 0) next = Math.round(next * Math.pow(10, precision)) / Math.pow(10, precision);
1658
+ input.value = precision >= 0 ? next.toFixed(precision) : String(next);
1659
+ syncInputs();
1660
+ }
1661
+ function addHold(el, dir) {
1662
+ let timer = null;
1663
+ function start() {
1664
+ stepVal(dir);
1665
+ timer = setTimeout(() => {
1666
+ timer = setInterval(() => stepVal(dir), 100);
1667
+ }, 300);
1668
+ }
1669
+ function stop() {
1670
+ if (timer) { clearInterval(timer); clearTimeout(timer); timer = null; }
1671
+ }
1672
+ el.addEventListener('mousedown', start);
1673
+ el.addEventListener('mouseup', stop);
1674
+ el.addEventListener('mouseleave', stop);
1675
+ }
1676
+ addHold(dec, -1);
1677
+ addHold(inc, 1);
1678
+ }
1679
+ makeStepper('sma-period', 1, 1, 0);
1680
+ makeStepper('vwap-bars', 1, 24, 0);
1681
+ makeStepper('ama-er', 1, 1, 0);
1682
+ makeStepper('ama-fast', 0.1, 0.1, 1);
1683
+ makeStepper('ama-slow', 1, 0.1, 1);
1684
+ document.getElementById('ama-reset').addEventListener('click', resetAmaDefaults);
1685
+ document.getElementById('ama-init-offset').addEventListener('input', () => {
1686
+ const input = document.getElementById('ama-init-offset');
1687
+ const snapped = snapInitOffset(input.value);
1688
+ if (String(snapped) !== String(input.value)) input.value = String(snapped);
1689
+ document.getElementById('ama-init-offset-val').textContent = snapped + '%';
1690
+ syncInputs();
1691
+ });
1692
+
1693
+ window.addEventListener('resize', () => {
1694
+ if (!charts.length) return;
1695
+ charts.forEach((chart) => {
1696
+ const el = chart === priceChart ? priceEl : volumeEl;
1697
+ chart.setSize({ width: el.clientWidth, height: el.clientHeight });
1698
+ });
1699
+ });
1700
+
1701
+ window.addEventListener('keydown', (e) => {
1702
+ if ((e.ctrlKey || e.metaKey) && e.key === '0' && charts.length) {
1703
+ const first = currentCandles[0]?.time;
1704
+ const last = currentCandles[currentCandles.length - 1]?.time;
1705
+ if (Number.isFinite(first) && Number.isFinite(last)) syncXRange(first, last);
1706
+ }
1707
+ });
1708
+ })();
1709
+ </script>
1710
+ </body>
1711
+ </html>`;
1712
+ }
1713
+ export { generateHTML, normalizeCandle, loadMarketProfiles };
1714
+ //# sourceMappingURL=tradingview_uplot_chart_generator.js.map