dexbot 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +4 -3
- package/analysis/README.md +1 -2
- package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
- package/analysis/resolve_source.ts +9 -2
- package/analysis/tradingview/README.md +29 -11
- package/analysis/tradingview/analyze_tradingview.ts +92 -3
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
- package/analysis/trend_detection/package.json +1 -1
- package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
- package/claw/modules/dexbot_profiles.ts +22 -2
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/claw/tests/test_dexbot_profiles.ts +65 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +6 -2
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.js +3 -3
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/candle_utils.d.ts +6 -0
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +6 -0
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +101 -19
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +2 -0
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +31 -4
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +0 -6
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
- package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
- package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +10 -5
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
- package/dist/market_adapter/inputs/window_cache.js +500 -0
- package/dist/market_adapter/inputs/window_cache.js.map +1 -0
- package/dist/market_adapter/interval_utils.d.ts +8 -1
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +14 -1
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -0
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +21 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +25 -4
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +67 -4
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +3 -1
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +13 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +2 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +122 -28
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +59 -8
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +11 -0
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +68 -4
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +8 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +57 -2
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +157 -28
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +135 -5
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +554 -46
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +13 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +15 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +36 -7
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/fund_registry.d.ts +1 -1
- package/dist/modules/fund_registry.js +1 -1
- package/dist/modules/node_connect_policy.d.ts +52 -0
- package/dist/modules/node_connect_policy.d.ts.map +1 -0
- package/dist/modules/node_connect_policy.js +35 -0
- package/dist/modules/node_connect_policy.js.map +1 -0
- package/dist/modules/node_manager.d.ts +32 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +76 -9
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +30 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +176 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -0
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +143 -15
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +5 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +182 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +64 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +231 -14
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -0
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +79 -7
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +16 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +11 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +24 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +258 -13
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +662 -12
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +36 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +102 -6
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +6 -2
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +13 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +20 -0
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +16 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +31 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/pm2.js +7 -7
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +13 -3
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/dist/scripts/analyze-orders.js +2 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.js +0 -1
- package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/tv.d.ts +24 -2
- package/dist/scripts/tv.d.ts.map +1 -1
- package/dist/scripts/tv.js +190 -65
- package/dist/scripts/tv.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -2
- package/docs/COW_INVARIANTS.md +18 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +8 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
- package/docs/GRID_RECALCULATION.md +5 -6
- package/docs/GRID_RECONCILE.md +12 -2
- package/docs/MPA_CREDIT_USAGE.md +2 -2
- package/docs/README.md +5 -6
- package/docs/architecture.md +0 -58
- package/docs/developer_guide.md +2 -1
- package/docs/docker.md +1 -1
- package/market_adapter/README.md +4 -0
- package/modules/README.md +1 -1
- package/package.json +1 -1
- package/scripts/README.md +5 -3
- package/tests/README.md +2 -2
|
@@ -10,7 +10,7 @@ import { getStorage } from '../../modules/storage/index.js';
|
|
|
10
10
|
const { readJSON } = getStorage();
|
|
11
11
|
import { getErrorMessage } from '../../modules/utils/errors.js';
|
|
12
12
|
import { parseRelativeMultiplier } from '../../modules/order/utils/math.js';
|
|
13
|
-
import { computeAverageAmaSlopePct } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
13
|
+
import { computeAverageAmaSlopePct, computeAmaSlopeClipThreshold } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
14
14
|
import { resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, } from '../../market_adapter/core/asymmetric_bounds.js';
|
|
15
15
|
function inferBaseIntervalSeconds(candles, fallback = 3600) {
|
|
16
16
|
if (!Array.isArray(candles) || candles.length < 2)
|
|
@@ -86,6 +86,25 @@ function resolveAmaDefaults({ meta, data, marketProfiles } = {}) {
|
|
|
86
86
|
: Number(source.slowPeriod),
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
const TRADINGVIEW_PREFS_KEY_PREFIX = 'dexbot2-tradingview-uplot-v3';
|
|
90
|
+
const TRADINGVIEW_SYNC_KEY = 'dexbot2-tradingview-sync';
|
|
91
|
+
function sanitizeStorageComponent(value, fallback) {
|
|
92
|
+
// Objects (e.g. a meta asset node missing both id and symbol) stringify to
|
|
93
|
+
// "[object Object]", which would collapse distinct assets onto one key.
|
|
94
|
+
if (value == null || typeof value === 'object')
|
|
95
|
+
return fallback;
|
|
96
|
+
const raw = String(value).trim();
|
|
97
|
+
const cleaned = raw.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
98
|
+
return cleaned || fallback;
|
|
99
|
+
}
|
|
100
|
+
// Prefer asset ids over symbols: ids are immutable, symbols can be relabeled.
|
|
101
|
+
function resolveChartStorageKey(meta = {}, baseIntervalSeconds = 0) {
|
|
102
|
+
const pool = sanitizeStorageComponent(meta?.pool ?? meta?.poolId, 'nipool');
|
|
103
|
+
const assetA = sanitizeStorageComponent(meta?.assetA?.id ?? meta?.assetA?.symbol ?? meta?.assetA, 'assetA');
|
|
104
|
+
const assetB = sanitizeStorageComponent(meta?.assetB?.id ?? meta?.assetB?.symbol ?? meta?.assetB, 'assetB');
|
|
105
|
+
const interval = Number(baseIntervalSeconds) > 0 ? Math.round(Number(baseIntervalSeconds)) : 0;
|
|
106
|
+
return `${TRADINGVIEW_PREFS_KEY_PREFIX}:${pool}:${assetA}_${assetB}:${interval || 'base'}`;
|
|
107
|
+
}
|
|
89
108
|
function generateHTML(data, title = 'TradingView Style Research') {
|
|
90
109
|
const rawCandles = Array.isArray(data.candles) ? data.candles : [];
|
|
91
110
|
const candles = rawCandles.map(normalizeCandle).filter(Boolean);
|
|
@@ -100,6 +119,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
100
119
|
{ label: '4h', seconds: 14400 },
|
|
101
120
|
{ label: '1d', seconds: 86400 },
|
|
102
121
|
{ label: '1w', seconds: 604800 },
|
|
122
|
+
{ label: '1M', seconds: 2592000, calendar: 'month' },
|
|
103
123
|
].map((item) => ({ ...item, enabled: item.seconds >= baseIntervalSeconds }));
|
|
104
124
|
const defaultTimeframe = timeframes.find((item) => item.label === data.defaultTimeframe && item.enabled)
|
|
105
125
|
|| timeframes.find((item) => item.enabled)
|
|
@@ -113,20 +133,21 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
113
133
|
maxSlopeOffset: MARKET_ADAPTER.DYNAMIC_WEIGHT_ASYMMETRIC_OFFSET_CLAMP,
|
|
114
134
|
maxAsymmetryFactor: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR,
|
|
115
135
|
minScaleSlots: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS,
|
|
136
|
+
clipPercentile: MARKET_ADAPTER.DYNAMIC_WEIGHT_CLIP_PERCENTILE,
|
|
116
137
|
};
|
|
117
138
|
function defaultRangeSpan(input) {
|
|
118
139
|
const cli = Number(input.rangeSpan);
|
|
119
140
|
if (Number.isFinite(cli))
|
|
120
|
-
return Math.min(2, Math.max(1.
|
|
141
|
+
return Math.min(2, Math.max(1.2, cli));
|
|
121
142
|
const down = parseRelativeMultiplier(input.grid?.minPrice);
|
|
122
143
|
const up = parseRelativeMultiplier(input.grid?.maxPrice);
|
|
123
144
|
if (down != null && down > 1 && up != null && up > 1)
|
|
124
|
-
return Math.min(2, Math.max(1.
|
|
145
|
+
return Math.min(2, Math.max(1.2, (down + up) / 2));
|
|
125
146
|
const vd = Number(input.grid?.minPrice);
|
|
126
147
|
const vu = Number(input.grid?.maxPrice);
|
|
127
148
|
if (Number.isFinite(vd) && Number.isFinite(vu) && vd > 0 && vu > vd) {
|
|
128
149
|
const ref = Math.sqrt(vd * vu);
|
|
129
|
-
return Math.min(2, Math.max(1.
|
|
150
|
+
return Math.min(2, Math.max(1.2, (ref / vd + vu / ref) / 2));
|
|
130
151
|
}
|
|
131
152
|
return 1.55;
|
|
132
153
|
}
|
|
@@ -138,7 +159,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
138
159
|
vwapEnabled: data.vwapEnabled === true,
|
|
139
160
|
vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
|
|
140
161
|
priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
|
|
141
|
-
rangeEnabled: data.rangeEnabled
|
|
162
|
+
rangeEnabled: data.rangeEnabled === true,
|
|
142
163
|
rangeScaleEnabled: data.rangeScaleEnabled === true,
|
|
143
164
|
rangeWidthPct: Number.isFinite(Number(data.rangeWidthPct)) && Number(data.rangeWidthPct) > 0
|
|
144
165
|
? Number(data.rangeWidthPct)
|
|
@@ -163,7 +184,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
163
184
|
? `${Math.round(baseIntervalSeconds / 86400)}d`
|
|
164
185
|
: `${Math.round(baseIntervalSeconds / 3600)}h`;
|
|
165
186
|
const defaultPairMode = data.defaultPairMode === 'inverse' ? 'inverse' : 'normal';
|
|
187
|
+
const storageKey = typeof data.storageKey === 'string' && data.storageKey.length > 0
|
|
188
|
+
? data.storageKey
|
|
189
|
+
: resolveChartStorageKey(meta, baseIntervalSeconds);
|
|
166
190
|
const payload = {
|
|
191
|
+
storageKey,
|
|
192
|
+
syncKey: TRADINGVIEW_SYNC_KEY,
|
|
167
193
|
candles,
|
|
168
194
|
timeframes,
|
|
169
195
|
defaultTimeframe: defaultTimeframe.label,
|
|
@@ -179,6 +205,14 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
179
205
|
rangeWidthPct: defaults.rangeWidthPct,
|
|
180
206
|
rangeSpan: defaults.rangeSpan,
|
|
181
207
|
grid: defaults.grid,
|
|
208
|
+
orderBuys: Array.isArray(data.orders?.buys) ? data.orders.buys.map(Number).filter(Number.isFinite) : [],
|
|
209
|
+
orderSells: Array.isArray(data.orders?.sells) ? data.orders.sells.map(Number).filter(Number.isFinite) : [],
|
|
210
|
+
gridBounds: {
|
|
211
|
+
low: Number.isFinite(Number(data.gridBounds?.low)) && Number(data.gridBounds.low) > 0 ? Number(data.gridBounds.low) : null,
|
|
212
|
+
high: Number.isFinite(Number(data.gridBounds?.high)) && Number(data.gridBounds.high) > 0 ? Number(data.gridBounds.high) : null,
|
|
213
|
+
},
|
|
214
|
+
updateMarkerTsSec: Number(data.updateMarkerTsSec) > 0 ? Number(data.updateMarkerTsSec) : null,
|
|
215
|
+
updateMarkerNewBars: Number(data.updateMarkerNewBars) || null,
|
|
182
216
|
rangeSlope,
|
|
183
217
|
defaultPairMode,
|
|
184
218
|
assetLabelA,
|
|
@@ -188,6 +222,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
188
222
|
poolLabel,
|
|
189
223
|
intervalLabel,
|
|
190
224
|
amaDefaultsSource: marketProfiles ? 'market_profiles' : 'constants',
|
|
225
|
+
volumeIsCount: !!(meta?.feed),
|
|
191
226
|
};
|
|
192
227
|
return `<!doctype html>
|
|
193
228
|
<html lang="en">
|
|
@@ -226,6 +261,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
226
261
|
.toolbar-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
|
|
227
262
|
.group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
|
|
228
263
|
.group:first-child { padding-left: 0; border-left: 0; }
|
|
264
|
+
.primary-toolbar-row .group { padding-left: 0; border-left: 0; }
|
|
229
265
|
:root {
|
|
230
266
|
--control-height: 36px;
|
|
231
267
|
}
|
|
@@ -248,6 +284,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
248
284
|
background: linear-gradient(180deg, rgba(35,53,79,0.95), rgba(22,32,47,0.98));
|
|
249
285
|
}
|
|
250
286
|
.time-btn:disabled { opacity: 0.35; cursor: not-allowed; text-decoration: line-through; }
|
|
287
|
+
.ama-preset-btn.active {
|
|
288
|
+
color: #fff;
|
|
289
|
+
border-color: rgba(250,204,21,0.85);
|
|
290
|
+
background: linear-gradient(180deg, rgba(79,66,22,0.95), rgba(47,40,18,0.98));
|
|
291
|
+
}
|
|
251
292
|
.indicator {
|
|
252
293
|
display: inline-flex;
|
|
253
294
|
align-items: center;
|
|
@@ -433,11 +474,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
433
474
|
background: rgba(10,14,20,0.72);
|
|
434
475
|
backdrop-filter: blur(10px);
|
|
435
476
|
}
|
|
436
|
-
.legend-line { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size:
|
|
477
|
+
.legend-line { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size: 12px; }
|
|
437
478
|
.legend-line + .legend-line { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.06); }
|
|
438
479
|
.legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
439
480
|
.legend-dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
|
|
440
|
-
.legend-label { color: #8290a2; text-transform: uppercase; letter-spacing: 0.45px; font-size:
|
|
481
|
+
.legend-label { color: #8290a2; text-transform: uppercase; letter-spacing: 0.45px; font-size: 12px; }
|
|
441
482
|
.legend-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #eef4fb; font-weight: 700; }
|
|
442
483
|
.status { display: inline-flex; align-items: center; gap: 8px; font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
443
484
|
.pill { border: 1px solid rgba(255,255,255,0.06); border-radius: 999px; padding: 4px 8px; background: rgba(255,255,255,0.03); }
|
|
@@ -460,7 +501,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
460
501
|
<div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
|
|
461
502
|
</div>
|
|
462
503
|
<div id="toolbar">
|
|
463
|
-
<div class="toolbar-row">
|
|
504
|
+
<div class="toolbar-row primary-toolbar-row">
|
|
464
505
|
<div class="group">
|
|
465
506
|
<div class="indicator">
|
|
466
507
|
<span class="tag">pair</span>
|
|
@@ -481,7 +522,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
481
522
|
<div class="group">
|
|
482
523
|
<div class="indicator">
|
|
483
524
|
<label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
|
|
484
|
-
<span class="tag">period</span>
|
|
485
525
|
<input type="text" inputmode="numeric" id="sma-period" value="${defaults.smaPeriod}">
|
|
486
526
|
<span class="step-stack">
|
|
487
527
|
<button type="button" class="step-btn" id="sma-period-inc">▲</button>
|
|
@@ -490,13 +530,14 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
490
530
|
</div>
|
|
491
531
|
<div class="indicator">
|
|
492
532
|
<label><input type="checkbox" id="vwap-toggle"${defaults.vwapEnabled ? ' checked' : ''}> VWMA</label>
|
|
493
|
-
<span class="tag">bars</span>
|
|
494
533
|
<input type="text" inputmode="numeric" id="vwap-bars" value="${defaults.vwapBars}">
|
|
495
534
|
<span class="step-stack">
|
|
496
535
|
<button type="button" class="step-btn" id="vwap-bars-inc">▲</button>
|
|
497
536
|
<button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
|
|
498
537
|
</span>
|
|
499
538
|
</div>
|
|
539
|
+
<div class="indicator"><label><input type="checkbox" id="volume-toggle" checked> Volume</label> <button type="button" class="reset-btn" id="volume-unit-toggle" data-volume-mode="base" title="Volume units: base asset — click for quote asset">${escapeHtml(defaultPairMode === 'inverse' ? assetLabelB : assetLabelA)}</button></div>
|
|
540
|
+
${(((payload.orderBuys?.length || 0) + (payload.orderSells?.length || 0) > 0 || payload.gridBounds?.low != null || payload.gridBounds?.high != null) ? '<div class="indicator"><label><input type="checkbox" id="orders-toggle" checked> Orders</label></div>' : '')}
|
|
500
541
|
</div>
|
|
501
542
|
</div>
|
|
502
543
|
<div class="toolbar-row">
|
|
@@ -521,20 +562,22 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
521
562
|
<button type="button" class="step-btn" id="ama-slow-inc">▲</button>
|
|
522
563
|
<button type="button" class="step-btn" id="ama-slow-dec">▼</button>
|
|
523
564
|
</span>
|
|
524
|
-
<button type="button" class="reset-btn"
|
|
565
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA1" title="AMA1 (slow 62.1)">1</button>
|
|
566
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA2" title="AMA2 (slow 72.0)">2</button>
|
|
567
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA3" title="AMA3 (slow 83.6)">3</button>
|
|
568
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA4" title="AMA4 (slow 96.9)">4</button>
|
|
525
569
|
</div>
|
|
526
|
-
<div class="indicator" title="Range min/max built only from the live AMA price (
|
|
570
|
+
<div class="indicator" title="Range min/max built only from the live AMA price (red above, green below); Scale sizes it by AMA slope like the grid build">
|
|
527
571
|
<label><input type="checkbox" id="range-toggle"${defaults.rangeEnabled ? ' checked' : ''}> Range</label>
|
|
528
572
|
<label title="Range Scaling: size the band by AMA slope like the grid build (trend side widens, opposite tightens) and fit the price axis to it"><input type="checkbox" id="range-scale-toggle"${defaults.rangeScaleEnabled ? ' checked' : ''}> Scale</label>
|
|
529
|
-
<span id="range-grid-wrap" style="display
|
|
530
|
-
<span
|
|
531
|
-
<input type="range" id="range-span" min="1.25" max="2" step="0.05" value="${defaults.rangeSpan.toFixed(2)}" style="width:90px;vertical-align:middle">
|
|
573
|
+
<span id="range-grid-wrap" style="display:inline" title="x-range around AMA (1.2x–2.0x)">
|
|
574
|
+
<input type="range" id="range-span" min="1.2" max="2" step="0.05" value="${defaults.rangeSpan.toFixed(2)}" style="width:90px;vertical-align:middle">
|
|
532
575
|
<span id="range-span-val" style="font-size:11px;color:#8b949e;width:40px;display:inline-block;text-align:right">${defaults.rangeSpan.toFixed(2)}x</span>
|
|
533
576
|
</span>
|
|
534
577
|
</div>
|
|
535
578
|
<div class="indicator">
|
|
536
579
|
<label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
|
|
537
|
-
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:
|
|
580
|
+
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:90px;vertical-align:middle" disabled>
|
|
538
581
|
<span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
|
|
539
582
|
</div>
|
|
540
583
|
</div>
|
|
@@ -547,21 +590,16 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
547
590
|
<span class="legend-item"><span class="legend-label">Time</span> <span class="legend-value" id="legend-time">-</span></span>
|
|
548
591
|
<span class="legend-item"><span class="legend-label">C</span> <span class="legend-value" id="legend-close">-</span></span>
|
|
549
592
|
<span class="legend-item"><span class="legend-label">Delta</span> <span class="legend-value" id="legend-delta">-</span></span>
|
|
550
|
-
<span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume">-</span></span>
|
|
551
|
-
<span class="legend-item"><span class="legend-label">Scale</span> <span class="legend-value" id="legend-scale">-</span></span>
|
|
593
|
+
<span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume" title="${payload.volumeIsCount ? 'Volume: feed publish count per bucket' : 'Volume units — click to switch base/quote'}" style="cursor:${payload.volumeIsCount ? 'default' : 'pointer'}">-</span></span>
|
|
552
594
|
</div>
|
|
553
595
|
<div class="legend-line">
|
|
554
596
|
<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>
|
|
555
|
-
<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>
|
|
556
|
-
<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>
|
|
557
|
-
<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>
|
|
558
|
-
<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>
|
|
559
|
-
|
|
560
597
|
<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>
|
|
561
|
-
<span class="legend-item"><span class="legend-dot"
|
|
598
|
+
<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>
|
|
599
|
+
<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>
|
|
562
600
|
</div>
|
|
563
601
|
</div>
|
|
564
|
-
<div id="price-chart" title="Wheel: zoom time (
|
|
602
|
+
<div id="price-chart" title="Wheel: zoom time (out = empty space around data) · Shift+wheel: zoom price · Drag: pan time + price (sets manual price scale) · Wheel/drag on price axis: zoom price · Drag on time axis: zoom time · Double-click price axis: autofit"></div>
|
|
565
603
|
<div id="volume-chart"></div>
|
|
566
604
|
</div>
|
|
567
605
|
</div>
|
|
@@ -576,7 +614,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
576
614
|
const baseCloseValues = baseCandles.map((c) => c.close);
|
|
577
615
|
const timeframes = Array.isArray(payload.timeframes) ? payload.timeframes.slice() : [];
|
|
578
616
|
const timeframeMap = new Map(timeframes.map((item) => [item.label, item]));
|
|
579
|
-
const STORAGE_KEY = '
|
|
617
|
+
const STORAGE_KEY = typeof payload.storageKey === 'string' && payload.storageKey.length > 0
|
|
618
|
+
? payload.storageKey
|
|
619
|
+
: 'dexbot2-tradingview-uplot-v3:default';
|
|
620
|
+
const SYNC_KEY = typeof payload.syncKey === 'string' && payload.syncKey.length > 0
|
|
621
|
+
? payload.syncKey
|
|
622
|
+
: 'dexbot2-tradingview-sync';
|
|
580
623
|
|
|
581
624
|
const state = loadState();
|
|
582
625
|
let currentTimeframe = state.timeframe || payload.defaultTimeframe || '1h';
|
|
@@ -600,8 +643,19 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
600
643
|
? Number(state.rangeWidthPct)
|
|
601
644
|
: (Number.isFinite(Number(payload.rangeWidthPct)) && Number(payload.rangeWidthPct) > 0 ? Number(payload.rangeWidthPct) : 2);
|
|
602
645
|
let currentRangeSpan = Number.isFinite(state.rangeSpan) && Number(state.rangeSpan) > 0
|
|
603
|
-
? Math.min(2, Math.max(1.
|
|
604
|
-
: (Number.isFinite(Number(payload.rangeSpan)) && Number(payload.rangeSpan) > 0 ? Math.min(2, Math.max(1.
|
|
646
|
+
? Math.min(2, Math.max(1.2, Number(state.rangeSpan)))
|
|
647
|
+
: (Number.isFinite(Number(payload.rangeSpan)) && Number(payload.rangeSpan) > 0 ? Math.min(2, Math.max(1.2, Number(payload.rangeSpan))) : 1.55);
|
|
648
|
+
let currentOrdersVisible = state.ordersVisible ?? true;
|
|
649
|
+
let currentVolumeVisible = state.volumeVisible ?? true;
|
|
650
|
+
let currentVolumeMode = state.volumeMode === 'quote' ? 'quote' : 'base';
|
|
651
|
+
// Static per generation: the overlay (levels, reserve/ceiling lines,
|
|
652
|
+
// spread label) exists only when order data was embedded.
|
|
653
|
+
// --no-orders or a missing orders file therefore removes the whole
|
|
654
|
+
// overlay, including the reserve/ceiling lines.
|
|
655
|
+
const hasOrders = ((payload.orderBuys?.length || 0) + (payload.orderSells?.length || 0)) > 0;
|
|
656
|
+
// Bounds-only grids (virtual slots, no live levels yet) still show
|
|
657
|
+
// the reserve/ceiling lines; levels/spread/panel rows need live data.
|
|
658
|
+
const hasBounds = payload.gridBounds?.low != null || payload.gridBounds?.high != null;
|
|
605
659
|
let currentAmaInitOffsetEnabled = false;
|
|
606
660
|
let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
|
|
607
661
|
let currentPriceScale = state.priceScale || payload.priceScale || 'log';
|
|
@@ -625,7 +679,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
625
679
|
let currentAmaOff = [];
|
|
626
680
|
let currentRangeUpper = [];
|
|
627
681
|
let currentRangeLower = [];
|
|
628
|
-
let currentRangeTrend = [];
|
|
629
682
|
let priceChart = null;
|
|
630
683
|
let volumeChart = null;
|
|
631
684
|
let lastRenderedPriceScale = null;
|
|
@@ -635,6 +688,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
635
688
|
let lastCandleKey = null;
|
|
636
689
|
let pendingRange = null;
|
|
637
690
|
let pendingRangeRaf = 0;
|
|
691
|
+
let rangePanelRaf = 0;
|
|
692
|
+
let yRefitRaf = 0;
|
|
638
693
|
let xMin = 0;
|
|
639
694
|
let xMax = 0;
|
|
640
695
|
let smaWorker = null;
|
|
@@ -653,27 +708,18 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
653
708
|
// TradingView-exporter interaction overrides (PR #3 / #11 feature):
|
|
654
709
|
// redeclare after UPLOT_SHARED_SCRIPT so these hoisted declarations win
|
|
655
710
|
// for this page only, without mutating the shared stack used by other charts.
|
|
656
|
-
function clampXRange(min, max) {
|
|
657
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return { min: xMin, max: xMax };
|
|
658
|
-
// Salli tyhjaa tilaa datan molemmin puolin (TradingView-tyyli):
|
|
659
|
-
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
660
|
-
const dataSpan = Math.max(1, xMax - xMin);
|
|
661
|
-
const maxPad = dataSpan * 0.75;
|
|
662
|
-
let lo = Math.max(xMin - maxPad, min);
|
|
663
|
-
let hi = Math.min(xMax + maxPad, max);
|
|
664
|
-
if (lo < xMin && hi <= lo) lo = xMin;
|
|
665
|
-
if (hi > xMax + maxPad) hi = xMax + maxPad;
|
|
666
|
-
if (hi <= lo) return { min: xMin, max: xMax };
|
|
667
|
-
return { min: lo, max: hi };
|
|
668
|
-
}
|
|
669
711
|
function bindWheelZoom(chart) {
|
|
670
712
|
chart.root.addEventListener('wheel', (e) => {
|
|
671
713
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
672
714
|
e.preventDefault();
|
|
673
715
|
const rect = chart.root.getBoundingClientRect();
|
|
674
|
-
|
|
716
|
+
// Shift+wheel scales price (cursor-anchored) anywhere over the
|
|
717
|
+
// price pane — same as wheeling over the price axis. Volume has
|
|
718
|
+
// no manual Y lock, so it keeps the timeframe zoom.
|
|
719
|
+
if (chart === priceChart && (e.shiftKey || inYAxisZone(chart, e.clientX))) {
|
|
675
720
|
const centerY = chart.posToVal(e.clientY - rect.top, 'y');
|
|
676
|
-
|
|
721
|
+
const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
722
|
+
zoomYAt(chart, centerY, delta < 0 ? 0.91 : 1.10);
|
|
677
723
|
return;
|
|
678
724
|
}
|
|
679
725
|
e.stopPropagation();
|
|
@@ -692,12 +738,132 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
692
738
|
syncXRange(center - nextSpan * ratio, center - nextSpan * ratio + nextSpan);
|
|
693
739
|
}, { passive: false });
|
|
694
740
|
}
|
|
741
|
+
function clampRange(min, max) {
|
|
742
|
+
if (!Number.isFinite(xMin) || !Number.isFinite(xMax) || xMax <= xMin) return null;
|
|
743
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
|
|
744
|
+
// Allow empty space on both sides of the data (TradingView-style):
|
|
745
|
+
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
746
|
+
// Span-locked: pans slide the window and never squash it — only a
|
|
747
|
+
// deliberate over-wide zoom-out is centered down to the limit.
|
|
748
|
+
// Plot drags therefore move but never scale; scaling lives on
|
|
749
|
+
// the axis gutters alone.
|
|
750
|
+
const dataSpan = Math.max(1, xMax - xMin);
|
|
751
|
+
const maxPad = dataSpan * 0.75;
|
|
752
|
+
const lo = xMin - maxPad;
|
|
753
|
+
const hi = xMax + maxPad;
|
|
754
|
+
const span = max - min;
|
|
755
|
+
if (span > hi - lo) {
|
|
756
|
+
const center = (min + max) / 2;
|
|
757
|
+
const half = (hi - lo) / 2;
|
|
758
|
+
return { min: center - half, max: center + half };
|
|
759
|
+
}
|
|
760
|
+
let nMin = min;
|
|
761
|
+
if (nMin < lo) nMin = lo;
|
|
762
|
+
if (nMin + span > hi) nMin = hi - span;
|
|
763
|
+
return { min: nMin, max: nMin + span };
|
|
764
|
+
}
|
|
765
|
+
function syncXRange(min, max) {
|
|
766
|
+
pendingRange = clampRange(min, max);
|
|
767
|
+
if (pendingRangeRaf) return;
|
|
768
|
+
pendingRangeRaf = requestAnimationFrame(() => {
|
|
769
|
+
const next = pendingRange;
|
|
770
|
+
pendingRange = null;
|
|
771
|
+
pendingRangeRaf = 0;
|
|
772
|
+
if (!next || charts.length === 0) return;
|
|
773
|
+
charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function bindPan(chart) {
|
|
777
|
+
let dragging = false;
|
|
778
|
+
let startClientX = 0;
|
|
779
|
+
let startClientY = 0;
|
|
780
|
+
let startMin = 0;
|
|
781
|
+
let startMax = 0;
|
|
782
|
+
let startYRange = null;
|
|
783
|
+
let panRaf = 0;
|
|
784
|
+
let pendingPan = null;
|
|
785
|
+
const applyPendingPan = () => {
|
|
786
|
+
panRaf = 0;
|
|
787
|
+
const p = pendingPan;
|
|
788
|
+
pendingPan = null;
|
|
789
|
+
if (!dragging || !p) return;
|
|
790
|
+
// Plot drag pans time and price (TradingView-style): the price
|
|
791
|
+
// move sets a manual Y range; double-click the price axis to
|
|
792
|
+
// return to autofit. Axis gutters never pan — they scale.
|
|
793
|
+
if (chart === priceChart && startYRange && Number.isFinite(p.clientY)) {
|
|
794
|
+
const vStart = chart.posToVal(p.startY - p.rectTop, 'y');
|
|
795
|
+
const vCur = chart.posToVal(p.clientY - p.rectTop, 'y');
|
|
796
|
+
if (Number.isFinite(vStart) && Number.isFinite(vCur) && vCur !== vStart) {
|
|
797
|
+
// Rigid move only: the span is locked by construction so a
|
|
798
|
+
// plot drag can never rescale — Y scaling happens solely
|
|
799
|
+
// on the price-axis gutter (wheel/drag there).
|
|
800
|
+
if (currentPriceScale === 'log') {
|
|
801
|
+
const ratio = Math.max(1e-12, vStart) / Math.max(1e-12, vCur);
|
|
802
|
+
const spanRatio = startYRange.max / startYRange.min;
|
|
803
|
+
if (Number.isFinite(ratio) && ratio > 0 && Number.isFinite(spanRatio) && spanRatio > 0) {
|
|
804
|
+
const nextMin = startYRange.min * ratio;
|
|
805
|
+
applyYRange(chart, nextMin, nextMin * spanRatio);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
const dy = vStart - vCur;
|
|
809
|
+
const span = startYRange.max - startYRange.min;
|
|
810
|
+
if (Number.isFinite(dy) && Number.isFinite(span) && span > 0) {
|
|
811
|
+
const nextMin = startYRange.min + dy;
|
|
812
|
+
applyYRange(chart, nextMin, nextMin + span);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const delta = chart.posToVal(p.clientX - p.rectLeft, 'x') - chart.posToVal(p.startX - p.rectLeft, 'x');
|
|
818
|
+
syncXRange(startMin - delta, startMax - delta);
|
|
819
|
+
};
|
|
820
|
+
const onMove = (e) => {
|
|
821
|
+
if (!dragging) return;
|
|
822
|
+
e.preventDefault();
|
|
823
|
+
const rect = chart.root.getBoundingClientRect();
|
|
824
|
+
pendingPan = {
|
|
825
|
+
clientX: e.clientX, clientY: e.clientY,
|
|
826
|
+
startX: startClientX, startY: startClientY,
|
|
827
|
+
rectLeft: rect.left, rectTop: rect.top,
|
|
828
|
+
};
|
|
829
|
+
if (!panRaf) panRaf = requestAnimationFrame(applyPendingPan);
|
|
830
|
+
};
|
|
831
|
+
const endDrag = () => {
|
|
832
|
+
if (!dragging) return;
|
|
833
|
+
dragging = false;
|
|
834
|
+
startYRange = null;
|
|
835
|
+
pendingPan = null;
|
|
836
|
+
if (panRaf) { cancelAnimationFrame(panRaf); panRaf = 0; }
|
|
837
|
+
document.body.style.cursor = '';
|
|
838
|
+
window.removeEventListener('mousemove', onMove);
|
|
839
|
+
window.removeEventListener('mouseup', endDrag);
|
|
840
|
+
};
|
|
841
|
+
chart.root.addEventListener('mousedown', (e) => {
|
|
842
|
+
if (!e || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
843
|
+
const rect = chart.root.getBoundingClientRect();
|
|
844
|
+
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
|
|
845
|
+
if (chart === priceChart && inYAxisZone(chart, e.clientX)) return;
|
|
846
|
+
if (inXAxisZone(chart, e.clientY)) return;
|
|
847
|
+
e.preventDefault();
|
|
848
|
+
e.stopPropagation();
|
|
849
|
+
dragging = true;
|
|
850
|
+
startClientX = e.clientX;
|
|
851
|
+
startClientY = e.clientY;
|
|
852
|
+
const s = chart.scales.x || {};
|
|
853
|
+
startMin = Number.isFinite(s.min) ? s.min : currentCandles[0].time;
|
|
854
|
+
startMax = Number.isFinite(s.max) ? s.max : currentCandles[currentCandles.length - 1].time;
|
|
855
|
+
if (chart === priceChart) startYRange = currentYRange(chart);
|
|
856
|
+
document.body.style.cursor = 'grabbing';
|
|
857
|
+
window.addEventListener('mousemove', onMove);
|
|
858
|
+
window.addEventListener('mouseup', endDrag, { once: true });
|
|
859
|
+
});
|
|
860
|
+
}
|
|
695
861
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
696
|
-
// Significant-digit price formatting (
|
|
697
|
-
// DEXBot order-price style (formatCurrency)
|
|
862
|
+
// Significant-digit price formatting (4 "active" digits), matching the
|
|
863
|
+
// DEXBot order-price style (formatCurrency).
|
|
698
864
|
// Very small values get SI micro-suffixes (m/µ/n/p/...); large values
|
|
699
|
-
// get comma grouping. Examples: 34.
|
|
700
|
-
// 3014.9 -> "3,
|
|
865
|
+
// get comma grouping. Examples: 34.94 -> "34.94", 119.98 -> "120.0",
|
|
866
|
+
// 3014.9 -> "3,015", 0.00567 -> "5.670m".
|
|
701
867
|
function fmtPrice(v) {
|
|
702
868
|
if (v == null || !Number.isFinite(v)) return '-';
|
|
703
869
|
const num = Number(v);
|
|
@@ -801,6 +967,9 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
801
967
|
ticks: { stroke: '#30363d', width: 1 },
|
|
802
968
|
font: '11px Segoe UI, sans-serif',
|
|
803
969
|
values: showLabels ? (u, vals) => {
|
|
970
|
+
if (String(currentTimeframe) === '1M') {
|
|
971
|
+
return vals.map((ts) => new Date(ts * 1000).toLocaleString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' }));
|
|
972
|
+
}
|
|
804
973
|
const xScale = u.scales.x || {};
|
|
805
974
|
const spanSec = Number.isFinite(xScale.min) && Number.isFinite(xScale.max)
|
|
806
975
|
? Math.max(0, xScale.max - xScale.min)
|
|
@@ -828,6 +997,9 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
828
997
|
rangeScaleEnabled: currentRangeScaleEnabled,
|
|
829
998
|
rangeWidthPct: currentRangeWidthPct,
|
|
830
999
|
rangeSpan: currentRangeSpan,
|
|
1000
|
+
ordersVisible: currentOrdersVisible,
|
|
1001
|
+
volumeVisible: currentVolumeVisible,
|
|
1002
|
+
volumeMode: currentVolumeMode,
|
|
831
1003
|
amaInitOffset: currentAmaInitOffset,
|
|
832
1004
|
priceScale: currentPriceScale,
|
|
833
1005
|
pairMode: currentPairMode,
|
|
@@ -842,6 +1014,75 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
842
1014
|
? (payload.pairLabelInverse || (payload.assetLabelB + '/' + payload.assetLabelA))
|
|
843
1015
|
: (payload.pairLabelNormal || (payload.assetLabelA + '/' + payload.assetLabelB));
|
|
844
1016
|
}
|
|
1017
|
+
// Volume units: base = displayed pair's base asset (TradingView
|
|
1018
|
+
// standard, VWMA-correct), quote = base * display close (per-candle
|
|
1019
|
+
// approximation of true quote volume). Persisted per chart in
|
|
1020
|
+
// localStorage like the AMA settings (volumeMode).
|
|
1021
|
+
function getVolumeSymbols() {
|
|
1022
|
+
const inverse = normalizePairMode(currentPairMode) === 'inverse';
|
|
1023
|
+
return {
|
|
1024
|
+
base: inverse ? payload.assetLabelB : payload.assetLabelA,
|
|
1025
|
+
quote: inverse ? payload.assetLabelA : payload.assetLabelB,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function getVolumeUnit() {
|
|
1029
|
+
if (payload.volumeIsCount) return { mode: 'count', symbol: 'feeds' };
|
|
1030
|
+
const syms = getVolumeSymbols();
|
|
1031
|
+
return currentVolumeMode === 'quote'
|
|
1032
|
+
? { mode: 'quote', symbol: syms.quote }
|
|
1033
|
+
: { mode: 'base', symbol: syms.base };
|
|
1034
|
+
}
|
|
1035
|
+
function getDisplayVolume(c) {
|
|
1036
|
+
const v = Number(c?.volume);
|
|
1037
|
+
if (!Number.isFinite(v)) return NaN;
|
|
1038
|
+
if (payload.volumeIsCount) return v;
|
|
1039
|
+
if (currentVolumeMode === 'quote') {
|
|
1040
|
+
const p = Number(c?.close);
|
|
1041
|
+
return Number.isFinite(p) && p > 0 ? v * p : NaN;
|
|
1042
|
+
}
|
|
1043
|
+
return v;
|
|
1044
|
+
}
|
|
1045
|
+
function fmtVolumeWithSym(v) {
|
|
1046
|
+
const s = fmtVolume(v);
|
|
1047
|
+
if (s == null || s === '-') return '-';
|
|
1048
|
+
const u = getVolumeUnit();
|
|
1049
|
+
return u.symbol ? s + ' ' + u.symbol : s;
|
|
1050
|
+
}
|
|
1051
|
+
function setActiveVolumeUnit() {
|
|
1052
|
+
const btn = document.getElementById('volume-unit-toggle');
|
|
1053
|
+
if (!btn) return;
|
|
1054
|
+
const u = getVolumeUnit();
|
|
1055
|
+
btn.dataset.volumeMode = u.mode;
|
|
1056
|
+
btn.textContent = u.symbol || u.mode;
|
|
1057
|
+
btn.title = payload.volumeIsCount
|
|
1058
|
+
? 'Volume: feed publish count per bucket'
|
|
1059
|
+
: (u.mode === 'quote'
|
|
1060
|
+
? 'Volume units: quote asset (' + u.symbol + ') — click for base asset'
|
|
1061
|
+
: 'Volume units: base asset (' + u.symbol + ') — click for quote asset');
|
|
1062
|
+
btn.disabled = !!payload.volumeIsCount;
|
|
1063
|
+
btn.style.opacity = payload.volumeIsCount ? '0.45' : '';
|
|
1064
|
+
btn.style.cursor = payload.volumeIsCount ? 'not-allowed' : 'pointer';
|
|
1065
|
+
const lv = document.getElementById('legend-volume');
|
|
1066
|
+
if (lv) {
|
|
1067
|
+
lv.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
1068
|
+
lv.title = payload.volumeIsCount
|
|
1069
|
+
? 'Volume: feed publish count per bucket'
|
|
1070
|
+
: 'Volume units — click to switch base/quote';
|
|
1071
|
+
}
|
|
1072
|
+
const vp = document.getElementById('vol-panel');
|
|
1073
|
+
if (vp) {
|
|
1074
|
+
vp.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
1075
|
+
vp.title = payload.volumeIsCount
|
|
1076
|
+
? 'Volume: feed publish count per bucket'
|
|
1077
|
+
: 'Volume units — click to switch base/quote';
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function toggleVolumeUnit() {
|
|
1081
|
+
if (payload.volumeIsCount) return;
|
|
1082
|
+
currentVolumeMode = currentVolumeMode === 'quote' ? 'base' : 'quote';
|
|
1083
|
+
setActiveVolumeUnit();
|
|
1084
|
+
rerender(true);
|
|
1085
|
+
}
|
|
845
1086
|
function refreshSubtitle() {
|
|
846
1087
|
const subtitleEl = document.getElementById('subtitle');
|
|
847
1088
|
if (!subtitleEl) return;
|
|
@@ -890,38 +1131,65 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
890
1131
|
seriesCache.set(key, state);
|
|
891
1132
|
return state;
|
|
892
1133
|
}
|
|
893
|
-
function aggregateCandles(rows, seconds) {
|
|
1134
|
+
function aggregateCandles(rows, seconds, label) {
|
|
1135
|
+
const monthly = String(label || '') === '1M';
|
|
894
1136
|
const bucketSec = Math.max(1, Math.round(seconds || 3600));
|
|
895
|
-
const cacheKey = currentPairMode + '|' + bucketSec;
|
|
1137
|
+
const cacheKey = currentPairMode + '|' + (monthly ? '1M' : String(bucketSec));
|
|
896
1138
|
if (aggregateCache.has(cacheKey)) return aggregateCache.get(cacheKey);
|
|
897
1139
|
const out = [];
|
|
898
1140
|
const idxs = [];
|
|
899
1141
|
let cur = null;
|
|
900
1142
|
let curBucket = null;
|
|
901
1143
|
let curIdx = -1;
|
|
1144
|
+
const finishBucket = (bucket) => {
|
|
1145
|
+
if (!bucket) return;
|
|
1146
|
+
// Feed volume is a publication rate, not trade quantity. Keep
|
|
1147
|
+
// it comparable across 1h/week/month views instead of turning
|
|
1148
|
+
// a monthly candle into a sum such as 388 publishes.
|
|
1149
|
+
if (payload.volumeIsCount && bucket.barCount > 0) {
|
|
1150
|
+
bucket.volume /= bucket.barCount;
|
|
1151
|
+
}
|
|
1152
|
+
delete bucket.barCount;
|
|
1153
|
+
out.push(bucket);
|
|
1154
|
+
};
|
|
902
1155
|
for (let i = 0; i < rows.length; i++) {
|
|
903
1156
|
const row = rows[i];
|
|
904
1157
|
const ts = Number(row.time);
|
|
905
1158
|
if (!Number.isFinite(ts)) continue;
|
|
906
|
-
|
|
1159
|
+
let bucket;
|
|
1160
|
+
if (monthly) {
|
|
1161
|
+
const d = new Date(ts * 1000);
|
|
1162
|
+
bucket = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) / 1000;
|
|
1163
|
+
} else {
|
|
1164
|
+
bucket = Math.floor(ts / bucketSec) * bucketSec;
|
|
1165
|
+
}
|
|
907
1166
|
if (!cur || bucket !== curBucket) {
|
|
908
1167
|
if (cur) {
|
|
909
|
-
|
|
1168
|
+
finishBucket(cur);
|
|
910
1169
|
idxs.push(curIdx);
|
|
911
1170
|
}
|
|
912
1171
|
curBucket = bucket;
|
|
913
1172
|
curIdx = i;
|
|
914
|
-
cur = {
|
|
1173
|
+
cur = {
|
|
1174
|
+
time: bucket,
|
|
1175
|
+
open: row.open,
|
|
1176
|
+
high: row.high,
|
|
1177
|
+
low: row.low,
|
|
1178
|
+
close: row.close,
|
|
1179
|
+
volume: row.volume || 0,
|
|
1180
|
+
barCount: 1,
|
|
1181
|
+
};
|
|
915
1182
|
} else {
|
|
916
1183
|
cur.high = Math.max(cur.high, row.high);
|
|
917
1184
|
cur.low = Math.min(cur.low, row.low);
|
|
918
1185
|
cur.close = row.close;
|
|
919
1186
|
cur.volume += row.volume || 0;
|
|
1187
|
+
cur.barCount += 1;
|
|
920
1188
|
curIdx = i;
|
|
921
1189
|
}
|
|
922
1190
|
}
|
|
923
1191
|
if (cur) {
|
|
924
|
-
|
|
1192
|
+
finishBucket(cur);
|
|
925
1193
|
idxs.push(curIdx);
|
|
926
1194
|
}
|
|
927
1195
|
const result = { candles: out, idxs };
|
|
@@ -1160,14 +1428,14 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1160
1428
|
// Bot-grid range band: min/max derive ONLY from the live AMA price.
|
|
1161
1429
|
// All math below runs the canonical market_adapter/grid sources,
|
|
1162
1430
|
// embedded verbatim (not hand-copied): computeAverageAmaSlopePct,
|
|
1163
|
-
// applyAsymmetricBounds (+ metrics/base),
|
|
1164
|
-
// parseRelativeMultiplier. With Range Scaling
|
|
1165
|
-
// the
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1431
|
+
// computeAmaSlopeClipThreshold, applyAsymmetricBounds (+ metrics/base),
|
|
1432
|
+
// applyNarrowingSideGuard, parseRelativeMultiplier. With Range Scaling
|
|
1433
|
+
// on, the clipped AMA slope tilts the span-symmetric band like the
|
|
1434
|
+
// grid build: trend side widens, opposite side tightens. Painted
|
|
1435
|
+
// red [AMA, upper], green [lower, AMA].
|
|
1168
1436
|
// Never reads candles, pair-display mapping, or axis/zoom state —
|
|
1169
1437
|
// inversion and timeframe sampling apply to the AMA first.
|
|
1170
|
-
${embedFunctionSources([computeAverageAmaSlopePct, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1438
|
+
${embedFunctionSources([computeAverageAmaSlopePct, computeAmaSlopeClipThreshold, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1171
1439
|
function computeRangeBand(baseAma) {
|
|
1172
1440
|
const n = Array.isArray(baseAma) ? baseAma.length : 0;
|
|
1173
1441
|
const upper = new Array(n).fill(null);
|
|
@@ -1182,19 +1450,19 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1182
1450
|
const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
|
|
1183
1451
|
? Number(gridCfg.maxAsymmetryFactor)
|
|
1184
1452
|
: (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
|
|
1185
|
-
const widthPct = Number(currentRangeWidthPct) > 0 ? Number(currentRangeWidthPct) : 2;
|
|
1186
1453
|
const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
|
|
1187
1454
|
const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
|
|
1188
|
-
// User x-range span (slider, default
|
|
1189
|
-
// width input — min = AMA/span, max = AMA*span.
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
(Number
|
|
1455
|
+
// User x-range span (slider 1.2x–2.0x, default 1.55x): the only
|
|
1456
|
+
// width input — symmetric base min = AMA/span, max = AMA*span.
|
|
1457
|
+
const span = Math.min(2, Math.max(1.2, Number(currentRangeSpan) > 0 ? Number(currentRangeSpan) : 1.55));
|
|
1458
|
+
// Canonical grid pipeline (ama_slope_model): adaptive percentile
|
|
1459
|
+
// clip over the AMA history, then offset + trend — the same numbers
|
|
1460
|
+
// the live grid build feeds into applyAsymmetricBounds.
|
|
1461
|
+
const clipPct = Number(slopeCfg.clipPercentile) >= 0 ? Number(slopeCfg.clipPercentile) : 10;
|
|
1462
|
+
const clipEr = Number.isFinite(currentAmaErPeriod) && currentAmaErPeriod > 0
|
|
1463
|
+
? Math.ceil(currentAmaErPeriod)
|
|
1464
|
+
: Math.ceil(Number(payload.amaDefaults?.erPeriod) || 781);
|
|
1465
|
+
const clipThreshold = computeAmaSlopeClipThreshold(baseAma, clipEr, lookback, clipPct);
|
|
1198
1466
|
const slopeScaling = !!currentRangeScaleEnabled;
|
|
1199
1467
|
if (!currentAmaEnabled || n === 0) return { upper, lower, trend };
|
|
1200
1468
|
for (let i = 0; i < n; i++) {
|
|
@@ -1203,29 +1471,17 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1203
1471
|
if (!Number.isFinite(ama) || ama <= 0 || !Number.isFinite(past) || past <= 0) continue;
|
|
1204
1472
|
const slopePct = computeAverageAmaSlopePct(ama, past, lookback);
|
|
1205
1473
|
if (slopePct == null || !Number.isFinite(slopePct)) continue;
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
1208
|
-
if (!hasGridShape) {
|
|
1209
|
-
// Width% envelope: symmetric around AMA; scaling sizes it
|
|
1210
|
-
// by slope strength ("real range"), otherwise full width.
|
|
1211
|
-
const w = slopeScaling ? strength * widthPct : widthPct;
|
|
1212
|
-
if (!(w > 0)) continue;
|
|
1213
|
-
const half = ama * w / 100;
|
|
1214
|
-
if (!Number.isFinite(half) || half <= 0) continue;
|
|
1215
|
-
upper[i] = ama + half;
|
|
1216
|
-
lower[i] = Math.max(ama - half, ama * 1e-6);
|
|
1217
|
-
trend[i] = dir;
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1474
|
+
const csp = Math.max(-clipThreshold, Math.min(clipThreshold, slopePct));
|
|
1475
|
+
const dir = Math.abs(csp) <= neutral ? 0 : (csp > 0 ? 1 : -1);
|
|
1220
1476
|
// Symmetric base from this bar's AMA and the user span —
|
|
1221
1477
|
// no chart or config price level.
|
|
1222
1478
|
let rMin = ama / span;
|
|
1223
1479
|
let rMax = ama * span;
|
|
1224
1480
|
if (slopeScaling && dir !== 0) {
|
|
1225
|
-
// Canonical tilt:
|
|
1226
|
-
//
|
|
1481
|
+
// Canonical tilt: clipped slope offset applied through
|
|
1482
|
+
// asymmetric_bounds, then the narrowing-side guard.
|
|
1227
1483
|
const trendName = dir > 0 ? 'UP' : 'DOWN';
|
|
1228
|
-
const slopeOffset = clamp(
|
|
1484
|
+
const slopeOffset = clamp(csp / maxSlope, -1, 1) * maxSlopeOffset;
|
|
1229
1485
|
const tilt = applyAsymmetricBounds({
|
|
1230
1486
|
centerPrice: ama,
|
|
1231
1487
|
minPrice: ama / span,
|
|
@@ -1253,10 +1509,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1253
1509
|
}
|
|
1254
1510
|
return { upper, lower, trend };
|
|
1255
1511
|
}
|
|
1256
|
-
function sampleTrendByIndex(series, idxs) {
|
|
1257
|
-
if (!Array.isArray(series) || !Array.isArray(idxs)) return [];
|
|
1258
|
-
return idxs.map((idx) => (Number.isFinite(idx) && series[idx] != null ? series[idx] : 0));
|
|
1259
|
-
}
|
|
1260
1512
|
function lowerBound(arr, value) {
|
|
1261
1513
|
let lo = 0;
|
|
1262
1514
|
let hi = arr.length;
|
|
@@ -1323,7 +1575,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1323
1575
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1324
1576
|
let max = 0;
|
|
1325
1577
|
for (let i = start; i < end; i++) {
|
|
1326
|
-
const v = currentCandles[i]
|
|
1578
|
+
const v = getDisplayVolume(currentCandles[i]);
|
|
1327
1579
|
if (Number.isFinite(v) && v > max) max = v;
|
|
1328
1580
|
}
|
|
1329
1581
|
if (!Number.isFinite(max) || max <= 0) return [0, 1];
|
|
@@ -1342,12 +1594,29 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1342
1594
|
btn.title = next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale';
|
|
1343
1595
|
btn.setAttribute('aria-label', next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale');
|
|
1344
1596
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1597
|
+
// AMA presets for the quick buttons (values from constants.ts AMAS).
|
|
1598
|
+
const AMA_PRESETS = {
|
|
1599
|
+
AMA1: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA1.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA1.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA1.slowPeriod} },
|
|
1600
|
+
AMA2: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA2.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA2.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA2.slowPeriod} },
|
|
1601
|
+
AMA3: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA3.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA3.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod} },
|
|
1602
|
+
AMA4: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA4.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA4.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA4.slowPeriod} },
|
|
1603
|
+
};
|
|
1604
|
+
function markActiveAmaPreset() {
|
|
1605
|
+
const cur = { e: Math.round(Number(currentAmaErPeriod)), f: Number(currentAmaFastPeriod), s: Number(currentAmaSlowPeriod) };
|
|
1606
|
+
document.querySelectorAll('.ama-preset-btn').forEach((b) => {
|
|
1607
|
+
const p = AMA_PRESETS[b.dataset.amaPreset];
|
|
1608
|
+
const match = p && Math.round(p.erPeriod) === cur.e && Number(p.fastPeriod) === cur.f && Number(p.slowPeriod) === cur.s;
|
|
1609
|
+
b.classList.toggle('active', !!match);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
function applyAmaPreset(key) {
|
|
1613
|
+
const p = AMA_PRESETS[key];
|
|
1614
|
+
if (!p) return;
|
|
1615
|
+
document.getElementById('ama-er').value = String(p.erPeriod);
|
|
1616
|
+
document.getElementById('ama-fast').value = String(p.fastPeriod);
|
|
1617
|
+
document.getElementById('ama-slow').value = String(p.slowPeriod);
|
|
1618
|
+
document.getElementById('ama-toggle').checked = true;
|
|
1619
|
+
syncInputs();
|
|
1351
1620
|
}
|
|
1352
1621
|
function setIndicatorSeriesVisible(seriesIndex, visible) {
|
|
1353
1622
|
if (!priceChart) return;
|
|
@@ -1375,7 +1644,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1375
1644
|
function hideRangeBandImmediate() {
|
|
1376
1645
|
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1377
1646
|
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1378
|
-
currentRangeTrend = new Array(currentCandles.length).fill(0);
|
|
1379
1647
|
// The band is a draw hook with no uPlot series: nothing else
|
|
1380
1648
|
// repaints on hide, so redraw explicitly (stale pixels otherwise linger).
|
|
1381
1649
|
if (priceChart && typeof priceChart.redraw === 'function') priceChart.redraw();
|
|
@@ -1389,7 +1657,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1389
1657
|
currentSmaInitOff = new Array(currentCandles.length).fill(null);
|
|
1390
1658
|
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1391
1659
|
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1392
|
-
currentRangeTrend = new Array(currentCandles.length).fill(0);
|
|
1393
1660
|
if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
|
|
1394
1661
|
currentPriceData[6] = currentAma;
|
|
1395
1662
|
currentPriceData[8] = currentSmaInit;
|
|
@@ -1423,26 +1690,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1423
1690
|
document.getElementById('legend-delta').textContent = Number.isFinite(delta)
|
|
1424
1691
|
? ((delta >= 0 ? '+' : '') + fmtPrice(delta))
|
|
1425
1692
|
: '-';
|
|
1426
|
-
document.getElementById('legend-volume').textContent =
|
|
1427
|
-
document.getElementById('legend-scale').textContent = currentPriceScale === 'linear' ? 'Linear' : 'Log';
|
|
1693
|
+
document.getElementById('legend-volume').textContent = fmtVolumeWithSym(getDisplayVolume(c));
|
|
1428
1694
|
document.getElementById('legend-sma').textContent = Number.isFinite(currentSma[idx]) ? fmtPrice(currentSma[idx]) : (smaPending ? '...' : '-');
|
|
1429
|
-
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1430
|
-
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1431
|
-
document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
|
|
1432
|
-
document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
|
|
1433
|
-
const rangeDir = currentRangeTrend[idx];
|
|
1434
|
-
const rangeDot = document.getElementById('legend-range-dot');
|
|
1435
|
-
const rangeVal = document.getElementById('legend-range');
|
|
1436
|
-
if (rangeDot) rangeDot.style.background = rangeDir === 1 ? '#26a69a' : (rangeDir === -1 ? '#ef5350' : '#8b949e');
|
|
1437
|
-
if (rangeVal) {
|
|
1438
|
-
const ru = currentRangeUpper[idx];
|
|
1439
|
-
const rl = currentRangeLower[idx];
|
|
1440
|
-
const ama = currentAma[idx];
|
|
1441
|
-
if (currentRangeEnabled && currentAmaEnabled && Number.isFinite(ru) && ru > ama && Number.isFinite(rl) && rl > 0 && rl < ama && ama > 0) {
|
|
1442
|
-
rangeVal.textContent = '+' + ((ru - ama) / ama * 100).toFixed(1) + '% / -' + ((ama - rl) / ama * 100).toFixed(1) + '%';
|
|
1443
|
-
} else rangeVal.textContent = '-';
|
|
1444
|
-
}
|
|
1445
1695
|
document.getElementById('legend-vwap').textContent = Number.isFinite(currentVwap[idx]) ? fmtPrice(currentVwap[idx]) : '-';
|
|
1696
|
+
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1697
|
+
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1446
1698
|
}
|
|
1447
1699
|
function candlePlugin() {
|
|
1448
1700
|
function drawCandles(u) {
|
|
@@ -1596,10 +1848,10 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1596
1848
|
u.ctx.clip();
|
|
1597
1849
|
// Behind candles/AMA lines (same trick as the dynamic-weight signal background).
|
|
1598
1850
|
u.ctx.globalCompositeOperation = 'destination-over';
|
|
1599
|
-
const UP_FILL = 'rgba(
|
|
1600
|
-
const DOWN_FILL = 'rgba(
|
|
1601
|
-
// Symmetric envelope, always both sides:
|
|
1602
|
-
// plus
|
|
1851
|
+
const UP_FILL = 'rgba(239,83,80,0.16)';
|
|
1852
|
+
const DOWN_FILL = 'rgba(38,166,154,0.16)';
|
|
1853
|
+
// Symmetric envelope, always both sides: red [AMA, upper]
|
|
1854
|
+
// plus green [lower, AMA] along the whole AMA, tilted by the
|
|
1603
1855
|
// grid's range scaling. Paint per contiguous valid segment.
|
|
1604
1856
|
let segTop = [];
|
|
1605
1857
|
let segBot = [];
|
|
@@ -1622,12 +1874,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1622
1874
|
u.ctx.closePath();
|
|
1623
1875
|
u.ctx.fill();
|
|
1624
1876
|
}
|
|
1625
|
-
u.ctx.strokeStyle = 'rgba(
|
|
1877
|
+
u.ctx.strokeStyle = 'rgba(239,83,80,0.55)';
|
|
1626
1878
|
u.ctx.lineWidth = 1;
|
|
1627
1879
|
u.ctx.beginPath();
|
|
1628
1880
|
segTop.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1629
1881
|
u.ctx.stroke();
|
|
1630
|
-
u.ctx.strokeStyle = 'rgba(
|
|
1882
|
+
u.ctx.strokeStyle = 'rgba(38,166,154,0.55)';
|
|
1631
1883
|
u.ctx.beginPath();
|
|
1632
1884
|
segBot.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1633
1885
|
u.ctx.stroke();
|
|
@@ -1664,7 +1916,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1664
1916
|
currentSeriesState = getSeriesState(currentPairMode);
|
|
1665
1917
|
currentSeriesCandles = currentSeriesState.candles;
|
|
1666
1918
|
currentSeriesCloseValues = currentSeriesState.closeValues;
|
|
1667
|
-
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600);
|
|
1919
|
+
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600, tf?.label || currentTimeframe);
|
|
1668
1920
|
currentCandles = aggregated.candles;
|
|
1669
1921
|
if (currentCandles.length) {
|
|
1670
1922
|
xMin = currentCandles[0].time;
|
|
@@ -1696,22 +1948,30 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1696
1948
|
const baseRange = computeRangeBand(baseAma);
|
|
1697
1949
|
currentRangeUpper = sampleSeriesByIndex(baseRange.upper, aggregated.idxs);
|
|
1698
1950
|
currentRangeLower = sampleSeriesByIndex(baseRange.lower, aggregated.idxs);
|
|
1699
|
-
currentRangeTrend = sampleTrendByIndex(baseRange.trend, aggregated.idxs);
|
|
1700
1951
|
currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1701
1952
|
currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1702
1953
|
const amaCfg = currentAmaConfig();
|
|
1703
1954
|
const warmupInit = currentAmaEnabled && baseAma.length > amaCfg.erPeriod ? baseAma[amaCfg.erPeriod] : null;
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1955
|
+
// Build the init segment in the selected timeframe. Sampling the
|
|
1956
|
+
// 1h init series by each bucket's final 1h index can make it end
|
|
1957
|
+
// inside a week/month bucket, leaving a gap before the first
|
|
1958
|
+
// sampled AMA value appears.
|
|
1959
|
+
const firstAmaIndex = currentAma.findIndex((value) => Number.isFinite(value));
|
|
1960
|
+
// The first visible higher-timeframe AMA sample can already be
|
|
1961
|
+
// several 1h updates past the true warmup point. Anchor that
|
|
1962
|
+
// sample to the warmup average so the AMA starts where the init
|
|
1963
|
+
// line ends instead of appearing detached at a different value.
|
|
1964
|
+
if (warmupInit !== null && firstAmaIndex >= 0) currentAma[firstAmaIndex] = warmupInit;
|
|
1965
|
+
currentSmaInit = warmupInit !== null
|
|
1966
|
+
? currentAma.map((value, i) => i <= firstAmaIndex || (firstAmaIndex < 0 && i === currentAma.length - 1) ? warmupInit : null)
|
|
1967
|
+
: new Array(currentCandles.length).fill(null);
|
|
1708
1968
|
const offsetInit = currentAmaEnabled && currentAmaInitOffset !== 0 && baseAmaOff.length > amaCfg.erPeriod
|
|
1709
1969
|
? baseAmaOff[amaCfg.erPeriod]
|
|
1710
1970
|
: null;
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1971
|
+
const firstAmaOffIndex = currentAmaOff.findIndex((value) => Number.isFinite(value));
|
|
1972
|
+
currentSmaInitOff = offsetInit !== null
|
|
1973
|
+
? currentAmaOff.map((value, i) => i < firstAmaOffIndex || (firstAmaOffIndex < 0 && i === currentAmaOff.length - 1) ? offsetInit : null)
|
|
1974
|
+
: new Array(currentCandles.length).fill(null);
|
|
1715
1975
|
currentPriceData = [
|
|
1716
1976
|
currentCandles.map((c) => c.time),
|
|
1717
1977
|
currentOpen,
|
|
@@ -1727,7 +1987,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1727
1987
|
];
|
|
1728
1988
|
currentVolumeData = [
|
|
1729
1989
|
currentCandles.map((c) => c.time),
|
|
1730
|
-
currentCandles.map((c) => c
|
|
1990
|
+
currentCandles.map((c) => getDisplayVolume(c)),
|
|
1731
1991
|
];
|
|
1732
1992
|
return {
|
|
1733
1993
|
priceData: currentPriceData,
|
|
@@ -1744,7 +2004,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1744
2004
|
padding: [14, 8, 8, 8],
|
|
1745
2005
|
legend: { show: false },
|
|
1746
2006
|
select: { show: false },
|
|
1747
|
-
cursor: { sync: { key:
|
|
2007
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1748
2008
|
scales: {
|
|
1749
2009
|
x: { time: true },
|
|
1750
2010
|
y: {
|
|
@@ -1795,7 +2055,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1795
2055
|
},
|
|
1796
2056
|
],
|
|
1797
2057
|
hooks: {
|
|
1798
|
-
draw: [(u) => positionPriceMarker(u)],
|
|
2058
|
+
draw: [(u) => { positionPriceMarker(u); positionReserveLine(u); positionOrderLines(u); positionUpdateMarker(u, true); }],
|
|
2059
|
+
// Keep the bottom-right stat badges glued to the visible
|
|
2060
|
+
// window while zooming/panning (rAF-throttled text swap).
|
|
2061
|
+
// Refit Y to the visible window on timeframe (x) moves —
|
|
2062
|
+
// uPlot does not recompute Y on setScale('x') by itself.
|
|
2063
|
+
setScale: [(u, key) => { if (key === 'x') scheduleYRefit(); scheduleStatPanels(); }],
|
|
1799
2064
|
},
|
|
1800
2065
|
};
|
|
1801
2066
|
|
|
@@ -1811,7 +2076,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1811
2076
|
padding: [6, 8, 8, 8],
|
|
1812
2077
|
legend: { show: false },
|
|
1813
2078
|
select: { show: false },
|
|
1814
|
-
cursor: { sync: { key:
|
|
2079
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1815
2080
|
scales: {
|
|
1816
2081
|
x: { time: true },
|
|
1817
2082
|
y: {
|
|
@@ -1832,6 +2097,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1832
2097
|
makeTimeAxis(true),
|
|
1833
2098
|
{ scale: 'y', side: 1, size: 84, space: 22, stroke: '#ffffff', grid: { stroke: '#1c2128' }, ticks: { stroke: '#30363d', width: 1 }, font: '600 12px Segoe UI, sans-serif', values: (u, vals) => vals.map((v) => (v == null ? '' : fmtVolume(v))) },
|
|
1834
2099
|
],
|
|
2100
|
+
hooks: {
|
|
2101
|
+
// x is synced from the price chart; refit volume Y to the
|
|
2102
|
+
// visible window and refresh the max badge.
|
|
2103
|
+
setScale: [(u, key) => { if (key === 'x') scheduleYRefit(); scheduleStatPanels(); }],
|
|
2104
|
+
},
|
|
1835
2105
|
};
|
|
1836
2106
|
|
|
1837
2107
|
const volumePluginInst = volumePlugin();
|
|
@@ -1844,32 +2114,33 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1844
2114
|
lastRenderedPriceScale = currentPriceScale;
|
|
1845
2115
|
return priceChart;
|
|
1846
2116
|
}
|
|
1847
|
-
// True when clientX sits over the price-axis
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
2117
|
+
// True when clientX sits over the true price-axis gutter (right of
|
|
2118
|
+
// the plot area). Keyed off uPlot's plot rect, not the container
|
|
2119
|
+
// width, so grabs on candles — even the newest ones at the plot's
|
|
2120
|
+
// right edge — always pan instead of scaling. Only the axis itself
|
|
2121
|
+
// zooms price; the rest pans time.
|
|
1852
2122
|
function inYAxisZone(chart, clientX) {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2123
|
+
if (chart !== priceChart || !chart.over) return false;
|
|
2124
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2125
|
+
if (!overRect || overRect.width <= 0) return false;
|
|
2126
|
+
return clientX >= overRect.right - 2;
|
|
2127
|
+
}
|
|
2128
|
+
// True when the pointer sits over the time-axis gutter (below the
|
|
2129
|
+
// plot area) of either chart. Dragging there scales the timeframe;
|
|
2130
|
+
// drags on candles always pan instead.
|
|
2131
|
+
function inXAxisZone(chart, clientY) {
|
|
2132
|
+
if (!chart || !chart.over) return false;
|
|
2133
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2134
|
+
if (!overRect || overRect.height <= 0) return false;
|
|
2135
|
+
return clientY >= overRect.bottom - 2;
|
|
1860
2136
|
}
|
|
1861
2137
|
let priceMarkerLabel = null;
|
|
1862
|
-
let priceMarkerLine = null;
|
|
1863
2138
|
function ensurePriceMarker(u) {
|
|
1864
2139
|
if (priceMarkerLabel && priceMarkerLabel.parentNode === u.root) return;
|
|
1865
2140
|
if (priceMarkerLabel && priceMarkerLabel.parentNode) priceMarkerLabel.parentNode.removeChild(priceMarkerLabel);
|
|
1866
|
-
if (priceMarkerLine && priceMarkerLine.parentNode) priceMarkerLine.parentNode.removeChild(priceMarkerLine);
|
|
1867
2141
|
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
1868
2142
|
priceMarkerLabel = document.createElement('div');
|
|
1869
2143
|
priceMarkerLabel.style.cssText = 'position:absolute;z-index:30;pointer-events:none;font:600 12px Segoe UI, sans-serif;line-height:19px;height:19px;padding:0 7px;border-radius:3px;color:#ffffff;white-space:nowrap;box-sizing:border-box;text-align:center;overflow:hidden;';
|
|
1870
|
-
priceMarkerLine = document.createElement('div');
|
|
1871
|
-
priceMarkerLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:1px dashed currentColor;opacity:0.6;';
|
|
1872
|
-
u.root.appendChild(priceMarkerLine);
|
|
1873
2144
|
u.root.appendChild(priceMarkerLabel);
|
|
1874
2145
|
}
|
|
1875
2146
|
function positionPriceMarker(u) {
|
|
@@ -1880,7 +2151,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1880
2151
|
const price = last.close;
|
|
1881
2152
|
if (!Number.isFinite(price)) {
|
|
1882
2153
|
priceMarkerLabel.style.display = 'none';
|
|
1883
|
-
priceMarkerLine.style.display = 'none';
|
|
1884
2154
|
return;
|
|
1885
2155
|
}
|
|
1886
2156
|
const up = price >= (Number.isFinite(prev.close) ? prev.close : price);
|
|
@@ -1890,7 +2160,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1890
2160
|
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
1891
2161
|
if (sMin == null || sMax == null || sMax <= sMin) {
|
|
1892
2162
|
priceMarkerLabel.style.display = 'none';
|
|
1893
|
-
priceMarkerLine.style.display = 'none';
|
|
1894
2163
|
return;
|
|
1895
2164
|
}
|
|
1896
2165
|
let frac;
|
|
@@ -1907,7 +2176,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1907
2176
|
if (!overRect || overRect.height <= 0) return;
|
|
1908
2177
|
const plotTop = overRect.top - rootRect.top;
|
|
1909
2178
|
const yRel = (1 - frac) * overRect.height;
|
|
1910
|
-
|
|
2179
|
+
// No full-width dashed line by design (disturbs reading) — the
|
|
2180
|
+
// axis label above carries the last price on its own.
|
|
1911
2181
|
priceMarkerLabel.style.display = 'block';
|
|
1912
2182
|
priceMarkerLabel.textContent = fmtPriceLabel(price);
|
|
1913
2183
|
priceMarkerLabel.style.background = color;
|
|
@@ -1923,16 +2193,336 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1923
2193
|
priceMarkerLabel.style.right = '0px';
|
|
1924
2194
|
priceMarkerLabel.style.width = Math.max(40, rightAxisW - 4) + 'px';
|
|
1925
2195
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2196
|
+
}
|
|
2197
|
+
// ── Order overlay ──
|
|
2198
|
+
// Active grid orders as dashed levels: green = buys, red = sells.
|
|
2199
|
+
// Grid bounds span the full grid (live + virtual slots), never
|
|
2200
|
+
// calculated: reserve = lowest grid buy, ceiling = highest grid sell.
|
|
2201
|
+
// Spread = best buy/best sell gap.
|
|
2202
|
+
// Pair-aware: order prices invert (1/p) with the candles so B/A shows
|
|
2203
|
+
// the same levels in display units (side colors preserved).
|
|
2204
|
+
let reserveLine = null;
|
|
2205
|
+
let reserveLabel = null;
|
|
2206
|
+
let ceilingLine = null;
|
|
2207
|
+
let ceilingLabel = null;
|
|
2208
|
+
let spreadMidLabel = null;
|
|
2209
|
+
let orderLineDivs = [];
|
|
2210
|
+
let orderBuyLabel = null;
|
|
2211
|
+
let orderSellLabel = null;
|
|
2212
|
+
let orderPriceTags = [];
|
|
2213
|
+
let lastOverlayKey = '';
|
|
2214
|
+
function getDisplayOrders() {
|
|
2215
|
+
const rawBuys = Array.isArray(payload.orderBuys) ? payload.orderBuys.filter(Number.isFinite).filter((p) => p > 0) : [];
|
|
2216
|
+
const rawSells = Array.isArray(payload.orderSells) ? payload.orderSells.filter(Number.isFinite).filter((p) => p > 0) : [];
|
|
2217
|
+
if (normalizePairMode(currentPairMode) !== 'inverse') return { buys: rawBuys, sells: rawSells };
|
|
2218
|
+
const inv = (arr) => arr.map((p) => 1 / p).filter(Number.isFinite).filter((p) => p > 0);
|
|
2219
|
+
return { buys: inv(rawBuys), sells: inv(rawSells) };
|
|
2220
|
+
}
|
|
2221
|
+
function yForPriceCached(ys, overRect, rootRect, price) {
|
|
2222
|
+
const sMin = Number.isFinite(ys.min) ? ys.min : null;
|
|
2223
|
+
const sMax = Number.isFinite(ys.max) ? ys.max : null;
|
|
2224
|
+
if (sMin == null || sMax == null || sMax <= sMin) return null;
|
|
2225
|
+
let frac;
|
|
2226
|
+
if (currentPriceScale === 'log') {
|
|
2227
|
+
if (!(sMin > 0) || !(price > 0)) return null;
|
|
2228
|
+
const lmin = Math.log10(sMin), lmax = Math.log10(sMax);
|
|
2229
|
+
frac = (Math.log10(price) - lmin) / (lmax - lmin);
|
|
2230
|
+
} else {
|
|
2231
|
+
frac = (price - sMin) / (sMax - sMin);
|
|
2232
|
+
}
|
|
2233
|
+
if (frac < 0 || frac > 1) return null;
|
|
2234
|
+
return (overRect.top - rootRect.top) + (1 - frac) * overRect.height;
|
|
2235
|
+
}
|
|
2236
|
+
function hideOverlayNodes() {
|
|
2237
|
+
[reserveLine, reserveLabel, ceilingLine, ceilingLabel, spreadMidLabel, orderBuyLabel, orderSellLabel].forEach((n) => { if (n) n.style.display = 'none'; });
|
|
2238
|
+
orderLineDivs.forEach((d) => { d.style.display = 'none'; });
|
|
2239
|
+
orderPriceTags.forEach((t) => { t.style.display = 'none'; });
|
|
2240
|
+
}
|
|
2241
|
+
function positionReserveLine(u) {
|
|
2242
|
+
if (!u || !u.over || !currentCandles.length) return;
|
|
2243
|
+
if (!currentOrdersVisible || (!hasOrders && !hasBounds)) { hideOverlayNodes(); return; }
|
|
2244
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2245
|
+
if (!reserveLine || reserveLine.parentNode !== u.root) {
|
|
2246
|
+
if (reserveLine && reserveLine.parentNode) reserveLine.parentNode.removeChild(reserveLine);
|
|
2247
|
+
if (reserveLabel && reserveLabel.parentNode) reserveLabel.parentNode.removeChild(reserveLabel);
|
|
2248
|
+
reserveLine = document.createElement('div');
|
|
2249
|
+
reserveLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:2px dashed #f97316;opacity:0.7;left:0;right:0;display:none;';
|
|
2250
|
+
reserveLabel = document.createElement('div');
|
|
2251
|
+
reserveLabel.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:600 10px Segoe UI, sans-serif;line-height:15px;height:15px;padding:0 5px;border-radius:3px;color:#f97316;background:rgba(30,41,59,0.9);white-space:nowrap;display:none;';
|
|
2252
|
+
u.root.appendChild(reserveLine);
|
|
2253
|
+
u.root.appendChild(reserveLabel);
|
|
2254
|
+
}
|
|
2255
|
+
// Grid bounds span the full DEXBot grid (live + virtual slots
|
|
2256
|
+
// in base units), never calculated: bottom = lowest grid buy,
|
|
2257
|
+
// top = highest grid sell. Reciprocals flip the ordering, so
|
|
2258
|
+
// the sides swap in B/A view. A missing side hides its line
|
|
2259
|
+
// instead of drawing an invented level.
|
|
2260
|
+
const { buys: boundBuys, sells: boundSells } = getDisplayOrders();
|
|
2261
|
+
const rawLow = Number(payload.gridBounds?.low);
|
|
2262
|
+
const rawHigh = Number(payload.gridBounds?.high);
|
|
2263
|
+
const gridLow = Number.isFinite(rawLow) && rawLow > 0 ? rawLow : null;
|
|
2264
|
+
const gridHigh = Number.isFinite(rawHigh) && rawHigh > 0 ? rawHigh : null;
|
|
2265
|
+
const inversePairs = normalizePairMode(currentPairMode) === 'inverse';
|
|
2266
|
+
const reservePrice = inversePairs
|
|
2267
|
+
? (gridHigh != null ? 1 / gridHigh : null)
|
|
2268
|
+
: gridLow;
|
|
2269
|
+
const ceilingPrice = inversePairs
|
|
2270
|
+
? (gridLow != null ? 1 / gridLow : null)
|
|
2271
|
+
: gridHigh;
|
|
2272
|
+
const ys = u.scales.y || {};
|
|
2273
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2274
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2275
|
+
const y = reservePrice != null ? yForPriceCached(ys, overRect, rootRect, reservePrice) : null;
|
|
2276
|
+
if (y == null) {
|
|
2277
|
+
reserveLine.style.display = 'none';
|
|
2278
|
+
reserveLabel.style.display = 'none';
|
|
2279
|
+
} else {
|
|
2280
|
+
reserveLine.style.display = 'block';
|
|
2281
|
+
reserveLine.style.top = y + 'px';
|
|
2282
|
+
const lastClose = currentCandles[currentCandles.length - 1].close;
|
|
2283
|
+
const dipRaw = Number.isFinite(lastClose) && lastClose > 0 ? (lastClose - reservePrice) / lastClose * 100 : NaN;
|
|
2284
|
+
const dipTxt = Number.isFinite(dipRaw) ? (dipRaw >= 0 ? '-' : '+') + Math.abs(Math.round(dipRaw)) + '%' : '-';
|
|
2285
|
+
reserveLabel.style.display = 'block';
|
|
2286
|
+
reserveLabel.textContent = 'reserve ' + fmtPriceLabel(reservePrice) + ' (' + dipTxt + ')';
|
|
2287
|
+
reserveLabel.style.left = '8px';
|
|
2288
|
+
reserveLabel.style.top = (y - 16) + 'px';
|
|
2289
|
+
}
|
|
2290
|
+
if (!ceilingLine || ceilingLine.parentNode !== u.root) {
|
|
2291
|
+
if (ceilingLine && ceilingLine.parentNode) ceilingLine.parentNode.removeChild(ceilingLine);
|
|
2292
|
+
if (ceilingLabel && ceilingLabel.parentNode) ceilingLabel.parentNode.removeChild(ceilingLabel);
|
|
2293
|
+
ceilingLine = document.createElement('div');
|
|
2294
|
+
ceilingLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:2px dashed #f97316;opacity:0.7;left:0;right:0;display:none;';
|
|
2295
|
+
ceilingLabel = document.createElement('div');
|
|
2296
|
+
ceilingLabel.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:600 10px Segoe UI, sans-serif;line-height:15px;height:15px;padding:0 5px;border-radius:3px;color:#f97316;background:rgba(30,41,59,0.9);white-space:nowrap;display:none;';
|
|
2297
|
+
u.root.appendChild(ceilingLine);
|
|
2298
|
+
u.root.appendChild(ceilingLabel);
|
|
2299
|
+
}
|
|
2300
|
+
const cY = ceilingPrice != null ? yForPriceCached(ys, overRect, rootRect, ceilingPrice) : null;
|
|
2301
|
+
if (cY == null) {
|
|
2302
|
+
ceilingLine.style.display = 'none';
|
|
2303
|
+
ceilingLabel.style.display = 'none';
|
|
2304
|
+
} else {
|
|
2305
|
+
ceilingLine.style.display = 'block';
|
|
2306
|
+
ceilingLine.style.top = cY + 'px';
|
|
2307
|
+
const lastCloseTop = currentCandles[currentCandles.length - 1].close;
|
|
2308
|
+
const upRaw = Number.isFinite(lastCloseTop) && lastCloseTop > 0 ? (ceilingPrice - lastCloseTop) / lastCloseTop * 100 : NaN;
|
|
2309
|
+
const upTxt = Number.isFinite(upRaw) ? (upRaw >= 0 ? '+' : '-') + Math.abs(Math.round(upRaw)) + '%' : '-';
|
|
2310
|
+
ceilingLabel.style.display = 'block';
|
|
2311
|
+
ceilingLabel.textContent = 'top ' + fmtPriceLabel(ceilingPrice) + ' (' + upTxt + ')';
|
|
2312
|
+
ceilingLabel.style.left = '8px';
|
|
2313
|
+
ceilingLabel.style.top = (cY + 4) + 'px';
|
|
2314
|
+
}
|
|
2315
|
+
if (!spreadMidLabel || spreadMidLabel.parentNode !== u.root) {
|
|
2316
|
+
if (spreadMidLabel && spreadMidLabel.parentNode) spreadMidLabel.parentNode.removeChild(spreadMidLabel);
|
|
2317
|
+
spreadMidLabel = document.createElement('div');
|
|
2318
|
+
spreadMidLabel.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:700 11px Segoe UI, sans-serif;line-height:16px;height:16px;padding:0 6px;border-radius:3px;color:#0b0f14;background:#facc15;white-space:nowrap;display:none;';
|
|
2319
|
+
u.root.appendChild(spreadMidLabel);
|
|
2320
|
+
}
|
|
2321
|
+
if (boundBuys.length && boundSells.length) {
|
|
2322
|
+
const bestBuy = Math.max(...boundBuys);
|
|
2323
|
+
const bestSell = Math.min(...boundSells);
|
|
2324
|
+
const yB = yForPriceCached(ys, overRect, rootRect, bestBuy);
|
|
2325
|
+
const yS = yForPriceCached(ys, overRect, rootRect, bestSell);
|
|
2326
|
+
if (yB != null && yS != null && bestBuy > 0) {
|
|
2327
|
+
const spr = (bestSell - bestBuy) / bestBuy * 100;
|
|
2328
|
+
spreadMidLabel.style.display = 'block';
|
|
2329
|
+
spreadMidLabel.textContent = 'spread ' + spr.toFixed(2) + '%';
|
|
2330
|
+
spreadMidLabel.style.left = '8px';
|
|
2331
|
+
spreadMidLabel.style.top = ((yB + yS) / 2 - 8) + 'px';
|
|
2332
|
+
} else {
|
|
2333
|
+
spreadMidLabel.style.display = 'none';
|
|
2334
|
+
}
|
|
2335
|
+
} else if (spreadMidLabel) {
|
|
2336
|
+
spreadMidLabel.style.display = 'none';
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function positionOrderLines(u) {
|
|
2340
|
+
if (!u || !u.over) return;
|
|
2341
|
+
if (!currentOrdersVisible) {
|
|
2342
|
+
orderLineDivs.forEach((d) => { d.style.display = 'none'; });
|
|
2343
|
+
orderPriceTags.forEach((t) => { t.style.display = 'none'; });
|
|
2344
|
+
if (orderBuyLabel) orderBuyLabel.style.display = 'none';
|
|
2345
|
+
if (orderSellLabel) orderSellLabel.style.display = 'none';
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2349
|
+
const { buys, sells } = getDisplayOrders();
|
|
2350
|
+
const total = buys.length + sells.length;
|
|
2351
|
+
if (orderLineDivs.length && orderLineDivs[0].parentNode !== u.root) {
|
|
2352
|
+
orderLineDivs.forEach((d) => u.root.appendChild(d));
|
|
2353
|
+
orderPriceTags.forEach((t) => u.root.appendChild(t));
|
|
2354
|
+
lastOverlayKey = '';
|
|
2355
|
+
}
|
|
2356
|
+
while (orderLineDivs.length < total) {
|
|
2357
|
+
const d = document.createElement('div');
|
|
2358
|
+
d.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;left:0;right:0;display:none;';
|
|
2359
|
+
u.root.appendChild(d);
|
|
2360
|
+
orderLineDivs.push(d);
|
|
2361
|
+
}
|
|
2362
|
+
const ys = u.scales.y || {};
|
|
2363
|
+
const overlayKey = (ys.min || 0) + '|' + (ys.max || 0) + '|' + (u.root.clientWidth || 0) + '|' + (u.root.clientHeight || 0) + '|' + currentPriceScale + '|' + currentPairMode;
|
|
2364
|
+
if (overlayKey === lastOverlayKey) return;
|
|
2365
|
+
lastOverlayKey = overlayKey;
|
|
2366
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2367
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2368
|
+
if (!orderBuyLabel || orderBuyLabel.parentNode !== u.root) {
|
|
2369
|
+
if (orderBuyLabel && orderBuyLabel.parentNode) orderBuyLabel.parentNode.removeChild(orderBuyLabel);
|
|
2370
|
+
orderBuyLabel = document.createElement('div');
|
|
2371
|
+
orderBuyLabel.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:600 10px Segoe UI, sans-serif;line-height:15px;height:15px;padding:0 5px;border-radius:3px;color:#26a69a;background:rgba(30,41,59,0.9);white-space:nowrap;display:none;';
|
|
2372
|
+
u.root.appendChild(orderBuyLabel);
|
|
2373
|
+
}
|
|
2374
|
+
if (!orderSellLabel || orderSellLabel.parentNode !== u.root) {
|
|
2375
|
+
if (orderSellLabel && orderSellLabel.parentNode) orderSellLabel.parentNode.removeChild(orderSellLabel);
|
|
2376
|
+
orderSellLabel = document.createElement('div');
|
|
2377
|
+
orderSellLabel.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:600 10px Segoe UI, sans-serif;line-height:15px;height:15px;padding:0 5px;border-radius:3px;color:#ef5350;background:rgba(30,41,59,0.9);white-space:nowrap;display:none;';
|
|
2378
|
+
u.root.appendChild(orderSellLabel);
|
|
2379
|
+
}
|
|
2380
|
+
let idx = 0;
|
|
2381
|
+
const place = (price, color) => {
|
|
2382
|
+
const d = orderLineDivs[idx++];
|
|
2383
|
+
const y = yForPriceCached(ys, overRect, rootRect, price);
|
|
2384
|
+
if (y == null) { d.style.display = 'none'; return null; }
|
|
2385
|
+
d.style.display = 'block';
|
|
2386
|
+
d.style.top = y + 'px';
|
|
2387
|
+
d.style.borderTop = '1.5px dashed ' + color;
|
|
2388
|
+
d.style.opacity = '0.55';
|
|
2389
|
+
return y;
|
|
2390
|
+
};
|
|
2391
|
+
let botBuyY = null;
|
|
2392
|
+
buys.forEach((p) => { const y = place(p, '#26a69a'); if (y != null && (botBuyY == null || y > botBuyY)) botBuyY = y; });
|
|
2393
|
+
let topSellY = null;
|
|
2394
|
+
sells.forEach((p) => { const y = place(p, '#ef5350'); if (y != null && (topSellY == null || y < topSellY)) topSellY = y; });
|
|
2395
|
+
for (; idx < orderLineDivs.length; idx++) orderLineDivs[idx].style.display = 'none';
|
|
2396
|
+
if (orderPriceTags.length && orderPriceTags[0].parentNode !== u.root) {
|
|
2397
|
+
orderPriceTags.forEach((t) => u.root.appendChild(t));
|
|
2398
|
+
}
|
|
2399
|
+
const levels = [
|
|
2400
|
+
...buys.map((p) => ({ p, c: '#26a69a', bg: 'rgba(20,30,28,0.92)' })),
|
|
2401
|
+
...sells.map((p) => ({ p, c: '#ef5350', bg: 'rgba(30,20,22,0.92)' })),
|
|
2402
|
+
].sort((a, b) => a.p - b.p);
|
|
2403
|
+
let ti = 0, lastTagY = -Infinity;
|
|
2404
|
+
const ensureTag = () => {
|
|
2405
|
+
if (ti >= orderPriceTags.length) {
|
|
2406
|
+
const t = document.createElement('div');
|
|
2407
|
+
t.style.cssText = 'position:absolute;z-index:25;pointer-events:none;font:600 9px Segoe UI, sans-serif;line-height:14px;height:14px;padding:0 4px;border-radius:3px;white-space:nowrap;display:none;';
|
|
2408
|
+
u.root.appendChild(t);
|
|
2409
|
+
orderPriceTags.push(t);
|
|
2410
|
+
}
|
|
2411
|
+
return orderPriceTags[ti++];
|
|
2412
|
+
};
|
|
2413
|
+
for (const lv of levels) {
|
|
2414
|
+
const tag = ensureTag();
|
|
2415
|
+
const y = yForPriceCached(ys, overRect, rootRect, lv.p);
|
|
2416
|
+
if (y == null || Math.abs(y - lastTagY) < 13) { tag.style.display = 'none'; continue; }
|
|
2417
|
+
tag.style.display = 'block';
|
|
2418
|
+
tag.style.top = (y - 7) + 'px';
|
|
2419
|
+
tag.style.right = '4px';
|
|
2420
|
+
tag.style.color = lv.c;
|
|
2421
|
+
tag.style.background = lv.bg;
|
|
2422
|
+
tag.style.border = '1px solid ' + lv.c;
|
|
2423
|
+
tag.textContent = fmtPriceLabel(lv.p);
|
|
2424
|
+
lastTagY = y;
|
|
2425
|
+
}
|
|
2426
|
+
for (; ti < orderPriceTags.length; ti++) orderPriceTags[ti].style.display = 'none';
|
|
2427
|
+
if (buys.length && botBuyY != null) {
|
|
2428
|
+
orderBuyLabel.style.display = 'block';
|
|
2429
|
+
orderBuyLabel.textContent = 'BUYS (' + buys.length + ') ' + fmtPriceLabel(Math.min(...buys));
|
|
2430
|
+
orderBuyLabel.style.left = '8px';
|
|
2431
|
+
orderBuyLabel.style.top = (botBuyY + 4) + 'px';
|
|
2432
|
+
} else {
|
|
2433
|
+
orderBuyLabel.style.display = 'none';
|
|
2434
|
+
}
|
|
2435
|
+
if (sells.length && topSellY != null) {
|
|
2436
|
+
orderSellLabel.style.display = 'block';
|
|
2437
|
+
orderSellLabel.textContent = 'SELLS (' + sells.length + ') ' + fmtPriceLabel(Math.max(...sells));
|
|
2438
|
+
orderSellLabel.style.left = '8px';
|
|
2439
|
+
orderSellLabel.style.top = (topSellY - 16) + 'px';
|
|
1932
2440
|
} else {
|
|
1933
|
-
|
|
2441
|
+
orderSellLabel.style.display = 'none';
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function resetOverlayNodes() {
|
|
2445
|
+
reserveLine = null;
|
|
2446
|
+
reserveLabel = null;
|
|
2447
|
+
ceilingLine = null;
|
|
2448
|
+
ceilingLabel = null;
|
|
2449
|
+
spreadMidLabel = null;
|
|
2450
|
+
orderLineDivs = [];
|
|
2451
|
+
orderBuyLabel = null;
|
|
2452
|
+
orderSellLabel = null;
|
|
2453
|
+
orderPriceTags = [];
|
|
2454
|
+
lastOverlayKey = '';
|
|
2455
|
+
}
|
|
2456
|
+
// ── Volume hover tooltip ──
|
|
2457
|
+
let volumeHoverTip = null;
|
|
2458
|
+
// ── Update marker ("updated from here"): vertical line where the
|
|
2459
|
+
// latest incremental fetch started, with a date label. Shown only
|
|
2460
|
+
// when the generator was given a marker timestamp.
|
|
2461
|
+
const UPDATE_MARKER_SEC = Number(payload.updateMarkerTsSec) > 0 ? Number(payload.updateMarkerTsSec) : null;
|
|
2462
|
+
function positionUpdateMarker(u, withLabel) {
|
|
2463
|
+
if (!UPDATE_MARKER_SEC || !u || !u.over || !currentCandles.length) return;
|
|
2464
|
+
let wrap = u.root.querySelector(':scope > .um-wrap');
|
|
2465
|
+
if (!wrap) {
|
|
2466
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2467
|
+
wrap = document.createElement('div');
|
|
2468
|
+
wrap.style.cssText = 'position:absolute;z-index:24;pointer-events:none;';
|
|
2469
|
+
wrap.innerHTML =
|
|
2470
|
+
'<div class="um-line" style="position:absolute;top:0;height:100%;width:0;border-left:2px dashed #22d3ee;opacity:0.75;"></div>' +
|
|
2471
|
+
'<div class="um-tag" style="position:absolute;top:2px;left:6px;font:600 10px Segoe UI, sans-serif;line-height:15px;color:#22d3ee;white-space:nowrap;"></div>';
|
|
2472
|
+
u.root.appendChild(wrap);
|
|
2473
|
+
}
|
|
2474
|
+
const s = u.scales.x || {};
|
|
2475
|
+
const sMin = Number.isFinite(s.min) ? s.min : null;
|
|
2476
|
+
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
2477
|
+
if (sMin == null || sMax == null || sMax <= sMin) { wrap.style.display = 'none'; return; }
|
|
2478
|
+
const frac = (UPDATE_MARKER_SEC - sMin) / (sMax - sMin);
|
|
2479
|
+
const inView = frac >= 0 && frac <= 1;
|
|
2480
|
+
if (!inView) { wrap.style.display = 'none'; return; }
|
|
2481
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2482
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2483
|
+
const xRoot = (overRect.left - rootRect.left) + frac * overRect.width;
|
|
2484
|
+
wrap.style.display = 'block';
|
|
2485
|
+
wrap.style.left = xRoot + 'px';
|
|
2486
|
+
wrap.style.top = (overRect.top - rootRect.top) + 'px';
|
|
2487
|
+
wrap.style.height = overRect.height + 'px';
|
|
2488
|
+
if (withLabel) {
|
|
2489
|
+
const tag = wrap.querySelector('.um-tag');
|
|
2490
|
+
const d = new Date(UPDATE_MARKER_SEC * 1000);
|
|
2491
|
+
const hh = String(d.getUTCHours()).padStart(2, '0');
|
|
2492
|
+
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
|
2493
|
+
const bars = Number(payload.updateMarkerNewBars) > 0 ? ' (+' + Math.round(Number(payload.updateMarkerNewBars)) + ')' : '';
|
|
2494
|
+
tag.textContent = 'update ' + d.toLocaleDateString(undefined) + ' ' + hh + ':' + mm + bars + ' →';
|
|
2495
|
+
tag.style.left = (frac > 0.8 ? -(tag.offsetWidth + 8) : 6) + 'px';
|
|
1934
2496
|
}
|
|
1935
2497
|
}
|
|
2498
|
+
function ensureVolumeHoverTip(u) {
|
|
2499
|
+
if (volumeHoverTip && volumeHoverTip.parentNode === u.root) return;
|
|
2500
|
+
if (volumeHoverTip && volumeHoverTip.parentNode) volumeHoverTip.parentNode.removeChild(volumeHoverTip);
|
|
2501
|
+
volumeHoverTip = document.createElement('div');
|
|
2502
|
+
volumeHoverTip.style.cssText = 'position:absolute;z-index:31;pointer-events:none;display:none;font:600 11px Segoe UI, sans-serif;line-height:18px;height:18px;padding:0 8px;border-radius:4px;color:#ffffff;background:rgba(48,54,61,0.95);border:1px solid #6e7681;white-space:nowrap;box-sizing:border-box;';
|
|
2503
|
+
u.root.appendChild(volumeHoverTip);
|
|
2504
|
+
}
|
|
2505
|
+
function showVolumeHover(u) {
|
|
2506
|
+
if (!u || !u.over || !currentCandles.length) return;
|
|
2507
|
+
ensureVolumeHoverTip(u);
|
|
2508
|
+
const idx = u.cursor.idx;
|
|
2509
|
+
if (idx == null || idx < 0 || idx >= currentCandles.length) {
|
|
2510
|
+
volumeHoverTip.style.display = 'none';
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const c = currentCandles[idx];
|
|
2514
|
+
const dv = getDisplayVolume(c);
|
|
2515
|
+
if (!Number.isFinite(dv)) {
|
|
2516
|
+
volumeHoverTip.style.display = 'none';
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const barX = u.valToPos(u.data[0][idx], 'x', true);
|
|
2520
|
+
volumeHoverTip.style.display = 'block';
|
|
2521
|
+
volumeHoverTip.textContent = 'Vol ' + fmtVolumeWithSym(dv);
|
|
2522
|
+
volumeHoverTip.style.left = Math.max(2, barX - volumeHoverTip.offsetWidth / 2) + 'px';
|
|
2523
|
+
// Pinned to the panel top — never covers the bars
|
|
2524
|
+
volumeHoverTip.style.top = '2px';
|
|
2525
|
+
}
|
|
1936
2526
|
function currentYRange(chart) {
|
|
1937
2527
|
const s = chart.scales.y || {};
|
|
1938
2528
|
let min = Number.isFinite(s.min) ? s.min : null;
|
|
@@ -1959,10 +2549,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1959
2549
|
return;
|
|
1960
2550
|
}
|
|
1961
2551
|
// Uloszoomauksen rajat: alin naytettava hinta voi painua viimeistaan
|
|
1962
|
-
// ~
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
|
|
2552
|
+
// ~1/2 datan minimin alapuolelle, ylin ~2x datan maksimin —
|
|
2553
|
+
// paneminen kayttaa tata 2x-bandia ja pysahtyy jaykkana sen
|
|
2554
|
+
// reunaan (ei siis paase "lipsahtamaan" candleita nakyvasta
|
|
2555
|
+
// kadottaen). Kokonaisvalille ei ole ylarajaa: pystyakselia saa
|
|
2556
|
+
// zoomata ulos vapaasti.
|
|
2557
|
+
priceBounds = { min: min, max: max, floor: min / 2, ceil: max * 2.0 };
|
|
1966
2558
|
}
|
|
1967
2559
|
function applyYRange(chart, min, max) {
|
|
1968
2560
|
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return false;
|
|
@@ -1973,11 +2565,37 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1973
2565
|
if (max <= min) return false;
|
|
1974
2566
|
}
|
|
1975
2567
|
if (priceBounds) {
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2568
|
+
// Span-locked slide within the 2x zoom band: a pan that would
|
|
2569
|
+
// cross the zoom-out limits stops as a rigid block instead of
|
|
2570
|
+
// getting squashed (squashing reads as scaling halfway through
|
|
2571
|
+
// a drag). Only a span wider than the band itself snaps to the
|
|
2572
|
+
// full band. Scaling lives on the axis gutters alone.
|
|
2573
|
+
if (currentPriceScale === 'log') {
|
|
2574
|
+
const spanRatio = max / min;
|
|
2575
|
+
if (spanRatio > priceBounds.ceil / priceBounds.floor) {
|
|
2576
|
+
min = priceBounds.floor;
|
|
2577
|
+
max = priceBounds.ceil;
|
|
2578
|
+
} else {
|
|
2579
|
+
if (max > priceBounds.ceil) {
|
|
2580
|
+
const f = max / priceBounds.ceil;
|
|
2581
|
+
min /= f; max /= f;
|
|
2582
|
+
}
|
|
2583
|
+
if (min < priceBounds.floor) {
|
|
2584
|
+
const f = priceBounds.floor / min;
|
|
2585
|
+
min *= f; max *= f;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
} else {
|
|
2589
|
+
const span = max - min;
|
|
2590
|
+
if (span > priceBounds.ceil - priceBounds.floor) {
|
|
2591
|
+
min = priceBounds.floor;
|
|
2592
|
+
max = priceBounds.ceil;
|
|
2593
|
+
} else {
|
|
2594
|
+
if (max > priceBounds.ceil) { max = priceBounds.ceil; min = max - span; }
|
|
2595
|
+
if (min < priceBounds.floor) { min = priceBounds.floor; max = min + span; }
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return false;
|
|
1981
2599
|
}
|
|
1982
2600
|
manualYRange = { min: min, max: max };
|
|
1983
2601
|
chart.setScale('y', { min: min, max: max });
|
|
@@ -2036,6 +2654,56 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2036
2654
|
window.addEventListener('mouseup', endDrag, { once: true });
|
|
2037
2655
|
}, true);
|
|
2038
2656
|
}
|
|
2657
|
+
// Time-axis drag scales the timeframe around its center (mirror of
|
|
2658
|
+
// the price-axis drag): drag right to zoom out, left to zoom in.
|
|
2659
|
+
// Synced across charts via syncXRange; capture phase pre-empts plot pan.
|
|
2660
|
+
function bindXAxisDrag(chart) {
|
|
2661
|
+
let dragging = false;
|
|
2662
|
+
let startClientX = 0;
|
|
2663
|
+
let startRange = null;
|
|
2664
|
+
const onMove = (e) => {
|
|
2665
|
+
if (!dragging || !startRange) return;
|
|
2666
|
+
e.preventDefault();
|
|
2667
|
+
chart.root.style.cursor = 'ew-resize';
|
|
2668
|
+
const deltaPx = e.clientX - startClientX;
|
|
2669
|
+
if (!Number.isFinite(deltaPx) || deltaPx === 0) return;
|
|
2670
|
+
const factor = Math.exp(deltaPx / 350);
|
|
2671
|
+
const span = startRange.max - startRange.min;
|
|
2672
|
+
if (!Number.isFinite(span) || span <= 0) return;
|
|
2673
|
+
const center = (startRange.min + startRange.max) / 2;
|
|
2674
|
+
const nextSpan = span * factor;
|
|
2675
|
+
syncXRange(center - nextSpan / 2, center + nextSpan / 2);
|
|
2676
|
+
};
|
|
2677
|
+
const endDrag = () => {
|
|
2678
|
+
if (!dragging) return;
|
|
2679
|
+
dragging = false;
|
|
2680
|
+
startRange = null;
|
|
2681
|
+
chart.root.style.cursor = '';
|
|
2682
|
+
window.removeEventListener('mousemove', onMove);
|
|
2683
|
+
window.removeEventListener('mouseup', endDrag);
|
|
2684
|
+
};
|
|
2685
|
+
chart.root.addEventListener('mousedown', (e) => {
|
|
2686
|
+
if (!e || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
2687
|
+
// capture phase so we pre-empt the plot pan when in the x-zone
|
|
2688
|
+
const rect = chart.root.getBoundingClientRect();
|
|
2689
|
+
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
|
|
2690
|
+
if (!inXAxisZone(chart, e.clientY)) return;
|
|
2691
|
+
const s = chart.scales.x || {};
|
|
2692
|
+
const range = {
|
|
2693
|
+
min: Number.isFinite(s.min) ? s.min : xMin,
|
|
2694
|
+
max: Number.isFinite(s.max) ? s.max : xMax,
|
|
2695
|
+
};
|
|
2696
|
+
if (!Number.isFinite(range.min) || !Number.isFinite(range.max) || range.max <= range.min) return;
|
|
2697
|
+
e.preventDefault();
|
|
2698
|
+
e.stopPropagation();
|
|
2699
|
+
dragging = true;
|
|
2700
|
+
startClientX = e.clientX;
|
|
2701
|
+
startRange = range;
|
|
2702
|
+
chart.root.style.cursor = 'ew-resize';
|
|
2703
|
+
window.addEventListener('mousemove', onMove);
|
|
2704
|
+
window.addEventListener('mouseup', endDrag, { once: true });
|
|
2705
|
+
}, true);
|
|
2706
|
+
}
|
|
2039
2707
|
function bindYAxisReset(chart) {
|
|
2040
2708
|
chart.root.addEventListener('dblclick', (e) => {
|
|
2041
2709
|
if (chart !== priceChart) return;
|
|
@@ -2050,6 +2718,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2050
2718
|
function setControls() {
|
|
2051
2719
|
setActivePairMode(currentPairMode);
|
|
2052
2720
|
setActivePriceScale(currentPriceScale);
|
|
2721
|
+
const ordersToggle = document.getElementById('orders-toggle');
|
|
2722
|
+
if (ordersToggle) ordersToggle.checked = currentOrdersVisible;
|
|
2723
|
+
const volumeToggleEl = document.getElementById('volume-toggle');
|
|
2724
|
+
if (volumeToggleEl) volumeToggleEl.checked = currentVolumeVisible;
|
|
2725
|
+
setActiveVolumeUnit();
|
|
2726
|
+
markActiveAmaPreset();
|
|
2053
2727
|
document.getElementById('sma-toggle').checked = currentSmaEnabled;
|
|
2054
2728
|
document.getElementById('sma-period').value = String(currentSmaPeriod);
|
|
2055
2729
|
document.getElementById('ama-toggle').checked = currentAmaEnabled;
|
|
@@ -2062,7 +2736,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2062
2736
|
document.getElementById('range-scale-toggle').checked = currentRangeScaleEnabled;
|
|
2063
2737
|
document.getElementById('range-span').value = Number(currentRangeSpan).toFixed(2);
|
|
2064
2738
|
document.getElementById('range-span-val').textContent = Number(currentRangeSpan).toFixed(2) + 'x';
|
|
2065
|
-
document.getElementById('range-grid-wrap').style.display =
|
|
2739
|
+
document.getElementById('range-grid-wrap').style.display = 'inline';
|
|
2066
2740
|
document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
|
|
2067
2741
|
document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
|
|
2068
2742
|
document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
|
|
@@ -2076,7 +2750,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2076
2750
|
if (needsRebuild) {
|
|
2077
2751
|
manualYRange = null;
|
|
2078
2752
|
priceMarkerLabel = null;
|
|
2079
|
-
|
|
2753
|
+
resetOverlayNodes();
|
|
2754
|
+
volumeHoverTip = null;
|
|
2080
2755
|
if (priceEl) priceEl.innerHTML = '';
|
|
2081
2756
|
if (volumeEl) volumeEl.innerHTML = '';
|
|
2082
2757
|
priceChart = null;
|
|
@@ -2107,16 +2782,53 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2107
2782
|
charts.forEach((chart) => {
|
|
2108
2783
|
bindWheelZoom(chart);
|
|
2109
2784
|
bindPan(chart);
|
|
2785
|
+
bindXAxisDrag(chart);
|
|
2110
2786
|
if (chart === priceChart) {
|
|
2111
2787
|
bindYAxisDrag(chart);
|
|
2112
2788
|
bindYAxisReset(chart);
|
|
2113
2789
|
chart.root.addEventListener('mousemove', (e) => {
|
|
2114
|
-
chart.root.style.cursor = inYAxisZone(chart, e.clientX) ? 'ns-resize' : '';
|
|
2790
|
+
chart.root.style.cursor = inYAxisZone(chart, e.clientX) ? 'ns-resize' : (inXAxisZone(chart, e.clientY) ? 'ew-resize' : '');
|
|
2791
|
+
});
|
|
2792
|
+
// Cursor price tag: floating label next to the mouse
|
|
2793
|
+
// showing the price under the cursor (price chart only).
|
|
2794
|
+
let cursorPriceTag = null;
|
|
2795
|
+
const ensureCursorPriceTag = () => {
|
|
2796
|
+
if (!cursorPriceTag || cursorPriceTag.parentNode !== chart.root) {
|
|
2797
|
+
if (cursorPriceTag && cursorPriceTag.parentNode) cursorPriceTag.parentNode.removeChild(cursorPriceTag);
|
|
2798
|
+
cursorPriceTag = document.createElement('div');
|
|
2799
|
+
cursorPriceTag.style.cssText = 'position:absolute;z-index:30;pointer-events:none;font:700 11px ui-monospace,SFMono-Regular,Menlo,monospace;line-height:16px;height:16px;padding:0 6px;border-radius:3px;color:#0b0f14;background:#e8eef5;white-space:nowrap;display:none;';
|
|
2800
|
+
chart.root.appendChild(cursorPriceTag);
|
|
2801
|
+
}
|
|
2802
|
+
return cursorPriceTag;
|
|
2803
|
+
};
|
|
2804
|
+
chart.over.addEventListener('mousemove', (e) => {
|
|
2805
|
+
const tag = ensureCursorPriceTag();
|
|
2806
|
+
const rect = chart.root.getBoundingClientRect();
|
|
2807
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2808
|
+
const price = chart.posToVal(e.clientY - overRect.top, 'y');
|
|
2809
|
+
if (!Number.isFinite(price)) { tag.style.display = 'none'; return; }
|
|
2810
|
+
tag.style.display = 'block';
|
|
2811
|
+
tag.textContent = fmtPriceLabel(price);
|
|
2812
|
+
let x = e.clientX - rect.left + 14;
|
|
2813
|
+
const y = e.clientY - rect.top - 8;
|
|
2814
|
+
const maxX = rect.width - tag.offsetWidth - 6;
|
|
2815
|
+
if (x > maxX) x = e.clientX - rect.left - tag.offsetWidth - 14;
|
|
2816
|
+
tag.style.left = Math.max(2, x) + 'px';
|
|
2817
|
+
tag.style.top = Math.max(2, y) + 'px';
|
|
2818
|
+
});
|
|
2819
|
+
chart.over.addEventListener('mouseleave', () => {
|
|
2820
|
+
if (cursorPriceTag) cursorPriceTag.style.display = 'none';
|
|
2115
2821
|
});
|
|
2116
2822
|
}
|
|
2117
2823
|
chart.root.addEventListener('mousemove', () => {
|
|
2118
2824
|
if (chart.cursor.idx != null) updateLegend(chart.cursor.idx);
|
|
2119
2825
|
});
|
|
2826
|
+
if (chart === volumeChart) {
|
|
2827
|
+
chart.over.addEventListener('mousemove', () => showVolumeHover(chart));
|
|
2828
|
+
chart.over.addEventListener('mouseleave', () => {
|
|
2829
|
+
if (volumeHoverTip) volumeHoverTip.style.display = 'none';
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2120
2832
|
chart.root.addEventListener('mouseleave', refreshLegend);
|
|
2121
2833
|
chart.root.addEventListener('mouseenter', () => chart.root.classList.add('is-hovered'));
|
|
2122
2834
|
chart.root.addEventListener('mouseleave', () => chart.root.classList.remove('is-hovered'));
|
|
@@ -2124,10 +2836,13 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2124
2836
|
chartEventsBound = true;
|
|
2125
2837
|
}
|
|
2126
2838
|
if (oldRange && Number.isFinite(oldRange.min) && Number.isFinite(oldRange.max)) {
|
|
2127
|
-
const next =
|
|
2839
|
+
const next = clampRange(oldRange.min, oldRange.max);
|
|
2128
2840
|
if (next) charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
2129
2841
|
}
|
|
2130
2842
|
refreshLegend();
|
|
2843
|
+
renderMarketPanel();
|
|
2844
|
+
renderRangePanel();
|
|
2845
|
+
renderVolumePanel();
|
|
2131
2846
|
saveState();
|
|
2132
2847
|
}
|
|
2133
2848
|
|
|
@@ -2156,10 +2871,62 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2156
2871
|
pairToggle.addEventListener('click', () => {
|
|
2157
2872
|
currentPairMode = currentPairMode === 'inverse' ? 'normal' : 'inverse';
|
|
2158
2873
|
manualYRange = null;
|
|
2874
|
+
lastOverlayKey = '';
|
|
2159
2875
|
setControls();
|
|
2160
2876
|
rerender(true);
|
|
2877
|
+
padXRight();
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
const ordersToggle = document.getElementById('orders-toggle');
|
|
2881
|
+
if (ordersToggle) {
|
|
2882
|
+
ordersToggle.addEventListener('change', () => {
|
|
2883
|
+
currentOrdersVisible = ordersToggle.checked;
|
|
2884
|
+
lastOverlayKey = '';
|
|
2885
|
+
if (!currentOrdersVisible && priceChart) hideOverlayNodes();
|
|
2886
|
+
setControls();
|
|
2887
|
+
saveState();
|
|
2888
|
+
if (priceChart) {
|
|
2889
|
+
positionReserveLine(priceChart);
|
|
2890
|
+
positionOrderLines(priceChart);
|
|
2891
|
+
}
|
|
2892
|
+
renderMarketPanel();
|
|
2161
2893
|
});
|
|
2162
2894
|
}
|
|
2895
|
+
// Volume toggle: hide the volume chart so flex yields the space to
|
|
2896
|
+
// the price chart (larger). No rerender, resize only.
|
|
2897
|
+
const applyVolumeVisibility = () => {
|
|
2898
|
+
if (!volumeEl || !priceChart) return;
|
|
2899
|
+
volumeEl.style.display = currentVolumeVisible ? '' : 'none';
|
|
2900
|
+
requestAnimationFrame(() => {
|
|
2901
|
+
if (!priceChart) return;
|
|
2902
|
+
priceChart.setSize({ width: priceEl.clientWidth, height: priceEl.clientHeight });
|
|
2903
|
+
if (currentVolumeVisible && volumeChart && volumeEl.clientWidth > 0) {
|
|
2904
|
+
volumeChart.setSize({ width: volumeEl.clientWidth, height: volumeEl.clientHeight });
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
};
|
|
2908
|
+
const volumeToggleInit = document.getElementById('volume-toggle');
|
|
2909
|
+
if (volumeToggleInit) {
|
|
2910
|
+
volumeToggleInit.checked = currentVolumeVisible;
|
|
2911
|
+
volumeToggleInit.addEventListener('change', () => {
|
|
2912
|
+
currentVolumeVisible = volumeToggleInit.checked;
|
|
2913
|
+
saveState();
|
|
2914
|
+
applyVolumeVisibility();
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
const volumeUnitToggle = document.getElementById('volume-unit-toggle');
|
|
2918
|
+
if (volumeUnitToggle) {
|
|
2919
|
+
volumeUnitToggle.addEventListener('click', () => toggleVolumeUnit());
|
|
2920
|
+
}
|
|
2921
|
+
const legendVolume = document.getElementById('legend-volume');
|
|
2922
|
+
if (legendVolume) {
|
|
2923
|
+
legendVolume.addEventListener('click', () => toggleVolumeUnit());
|
|
2924
|
+
}
|
|
2925
|
+
// Initial state on load (previously stored in localStorage)
|
|
2926
|
+
applyVolumeVisibility();
|
|
2927
|
+
document.querySelectorAll('.ama-preset-btn').forEach((b) => {
|
|
2928
|
+
b.addEventListener('click', () => applyAmaPreset(b.dataset.amaPreset));
|
|
2929
|
+
});
|
|
2163
2930
|
|
|
2164
2931
|
const syncInputs = () => {
|
|
2165
2932
|
currentSmaEnabled = document.getElementById('sma-toggle').checked;
|
|
@@ -2177,8 +2944,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2177
2944
|
currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
|
|
2178
2945
|
currentRangeEnabled = document.getElementById('range-toggle').checked;
|
|
2179
2946
|
currentRangeScaleEnabled = document.getElementById('range-scale-toggle').checked;
|
|
2180
|
-
currentRangeSpan = Math.min(2, Math.max(1.
|
|
2947
|
+
currentRangeSpan = Math.min(2, Math.max(1.2, Math.round((Number(document.getElementById('range-span').value) || 1.55) * 20) / 20));
|
|
2948
|
+
const ordersEl = document.getElementById('orders-toggle');
|
|
2949
|
+
if (ordersEl) currentOrdersVisible = ordersEl.checked;
|
|
2181
2950
|
setControls();
|
|
2951
|
+
markActiveAmaPreset();
|
|
2182
2952
|
rerender(false);
|
|
2183
2953
|
};
|
|
2184
2954
|
|
|
@@ -2278,7 +3048,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2278
3048
|
makeStepper('ama-er', 1, 1, 0);
|
|
2279
3049
|
makeStepper('ama-fast', 0.1, 0.1, 1);
|
|
2280
3050
|
makeStepper('ama-slow', 1, 0.1, 1);
|
|
2281
|
-
document.getElementById('ama-reset').addEventListener('click', resetAmaDefaults);
|
|
2282
3051
|
document.getElementById('ama-init-offset').addEventListener('input', () => {
|
|
2283
3052
|
const input = document.getElementById('ama-init-offset');
|
|
2284
3053
|
const snapped = snapInitOffset(input.value);
|
|
@@ -2292,19 +3061,186 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
2292
3061
|
syncInputs();
|
|
2293
3062
|
});
|
|
2294
3063
|
|
|
3064
|
+
// Room to the right past the last bar (~12%): for the market panel
|
|
3065
|
+
// and future candles. Applied at init and on pair flip; user
|
|
3066
|
+
// zoom/pan never trigger it, so it does not fight the user zoom.
|
|
3067
|
+
function padXRight() {
|
|
3068
|
+
if (!priceChart || !currentCandles.length) return;
|
|
3069
|
+
const first = currentCandles[0]?.time;
|
|
3070
|
+
const last = currentCandles[currentCandles.length - 1]?.time;
|
|
3071
|
+
if (!Number.isFinite(first) || !Number.isFinite(last) || last <= first) return;
|
|
3072
|
+
const span = last - first;
|
|
3073
|
+
syncXRange(first - span * 0.02, last + span * 0.12);
|
|
3074
|
+
}
|
|
3075
|
+
// Market panel top-right: SELL / Market / BUY with distance-to-market
|
|
3076
|
+
// %. Pair-aware (same display levels as the overlay). Static per
|
|
3077
|
+
// generation, so drawn once (no draw hook); refreshed on pair flip
|
|
3078
|
+
// via rerender. Hidden while the Orders overlay is unchecked — the
|
|
3079
|
+
// BUY/SELL rows are overlay data, so they go away with it.
|
|
3080
|
+
function renderMarketPanel() {
|
|
3081
|
+
if (!priceChart) return;
|
|
3082
|
+
let panel = document.getElementById('mkt-panel');
|
|
3083
|
+
if (!panel) {
|
|
3084
|
+
panel = document.createElement('div');
|
|
3085
|
+
panel.id = 'mkt-panel';
|
|
3086
|
+
panel.style.cssText = 'position:absolute;z-index:26;top:10px;right:88px;pointer-events:none;font:600 14px ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;padding:8px 12px;border-radius:8px;background:rgba(13,17,23,0.85);border:1px solid #263241;white-space:nowrap;text-align:right;';
|
|
3087
|
+
priceChart.root.appendChild(panel);
|
|
3088
|
+
}
|
|
3089
|
+
if (!currentOrdersVisible && (hasOrders || hasBounds)) { panel.style.display = 'none'; return; }
|
|
3090
|
+
panel.style.display = 'block';
|
|
3091
|
+
const last = currentCandles[currentCandles.length - 1];
|
|
3092
|
+
const mkt = last ? last.close : NaN;
|
|
3093
|
+
const { buys, sells } = getDisplayOrders();
|
|
3094
|
+
let html = '';
|
|
3095
|
+
if (sells.length && Number.isFinite(mkt)) {
|
|
3096
|
+
const s = Math.min(...sells);
|
|
3097
|
+
const p = (s - mkt) / mkt * 100;
|
|
3098
|
+
html += '<div style="color:#ef5350">SELL ' + fmtPriceLabel(s) + ' ' + (p >= 0 ? '+' : '') + p.toFixed(1) + '%</div>';
|
|
3099
|
+
}
|
|
3100
|
+
html += '<div style="color:#e8eef5">Market ' + (Number.isFinite(mkt) ? fmtPriceLabel(mkt) : '-') + '</div>';
|
|
3101
|
+
if (buys.length && Number.isFinite(mkt)) {
|
|
3102
|
+
const b = Math.max(...buys);
|
|
3103
|
+
const p = (b - mkt) / mkt * 100;
|
|
3104
|
+
html += '<div style="color:#26a69a">BUY ' + fmtPriceLabel(b) + ' ' + (p >= 0 ? '+' : '') + p.toFixed(1) + '%</div>';
|
|
3105
|
+
}
|
|
3106
|
+
panel.innerHTML = html;
|
|
3107
|
+
}
|
|
3108
|
+
// Range panel bottom-right: visible-window candle High (red) /
|
|
3109
|
+
// Low (green) — sell-side red on top, buy-side green below, mirroring
|
|
3110
|
+
// the top-right market badge. Volume max lives in its own badge on
|
|
3111
|
+
// the volume chart below. Slightly smaller type than the top-right market badge.
|
|
3112
|
+
// Pair-aware via currentCandles (already inverted in B/A view).
|
|
3113
|
+
// Refreshed on rerender and on x zoom/pan through the setScale hooks.
|
|
3114
|
+
function visibleCandleStats() {
|
|
3115
|
+
if (!currentCandles.length) return null;
|
|
3116
|
+
let minX = NaN;
|
|
3117
|
+
let maxX = NaN;
|
|
3118
|
+
if (priceChart) {
|
|
3119
|
+
const xs = priceChart.scales.x || {};
|
|
3120
|
+
if (Number.isFinite(xs.min)) minX = xs.min;
|
|
3121
|
+
if (Number.isFinite(xs.max)) maxX = xs.max;
|
|
3122
|
+
}
|
|
3123
|
+
if (!Number.isFinite(minX)) minX = currentCandles[0].time;
|
|
3124
|
+
if (!Number.isFinite(maxX)) maxX = currentCandles[currentCandles.length - 1].time;
|
|
3125
|
+
const times = currentCandles.map((c) => c.time);
|
|
3126
|
+
const start = Math.max(0, lowerBound(times, minX) - 1);
|
|
3127
|
+
const end = Math.min(times.length, lowerBound(times, maxX) + 2);
|
|
3128
|
+
let hi = -Infinity;
|
|
3129
|
+
let lo = Infinity;
|
|
3130
|
+
let vol = 0;
|
|
3131
|
+
for (let i = start; i < end; i++) {
|
|
3132
|
+
const c = currentCandles[i];
|
|
3133
|
+
if (!c) continue;
|
|
3134
|
+
if (Number.isFinite(c.high) && c.high > hi) hi = c.high;
|
|
3135
|
+
if (Number.isFinite(c.low) && c.low < lo) lo = c.low;
|
|
3136
|
+
const dv = getDisplayVolume(c);
|
|
3137
|
+
if (Number.isFinite(dv) && dv > vol) vol = dv;
|
|
3138
|
+
}
|
|
3139
|
+
if (!Number.isFinite(hi) || !Number.isFinite(lo)) return null;
|
|
3140
|
+
return { hi, lo, vol };
|
|
3141
|
+
}
|
|
3142
|
+
function renderRangePanel() {
|
|
3143
|
+
if (!priceChart) return;
|
|
3144
|
+
let panel = document.getElementById('range-panel');
|
|
3145
|
+
if (!panel) {
|
|
3146
|
+
panel = document.createElement('div');
|
|
3147
|
+
panel.id = 'range-panel';
|
|
3148
|
+
panel.style.cssText = 'position:absolute;z-index:26;bottom:10px;right:88px;pointer-events:none;font:600 12px ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;padding:6px 10px;border-radius:8px;background:rgba(13,17,23,0.85);border:1px solid #263241;white-space:nowrap;text-align:right;';
|
|
3149
|
+
priceChart.root.appendChild(panel);
|
|
3150
|
+
}
|
|
3151
|
+
const stats = visibleCandleStats();
|
|
3152
|
+
if (!stats) { panel.style.display = 'none'; return; }
|
|
3153
|
+
panel.style.display = 'block';
|
|
3154
|
+
panel.innerHTML = '<div style="color:#ef5350">H ' + fmtPriceLabel(stats.hi) + '</div>'
|
|
3155
|
+
+ '<div style="color:#26a69a">L ' + fmtPriceLabel(stats.lo) + '</div>';
|
|
3156
|
+
}
|
|
3157
|
+
// Volume badge top-right on the volume chart: max volume of the
|
|
3158
|
+
// visible window only. Same small type as the range panel. Lives on
|
|
3159
|
+
// the volume root, so hiding the volume chart hides the badge too.
|
|
3160
|
+
function renderVolumePanel() {
|
|
3161
|
+
if (!volumeChart) return;
|
|
3162
|
+
let panel = document.getElementById('vol-panel');
|
|
3163
|
+
if (!panel) {
|
|
3164
|
+
panel = document.createElement('div');
|
|
3165
|
+
panel.id = 'vol-panel';
|
|
3166
|
+
panel.style.cssText = 'position:absolute;z-index:26;top:10px;right:88px;pointer-events:auto;font:600 12px ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;padding:6px 10px;border-radius:8px;background:rgba(13,17,23,0.85);border:1px solid #263241;white-space:nowrap;text-align:right;';
|
|
3167
|
+
panel.addEventListener('click', () => toggleVolumeUnit());
|
|
3168
|
+
volumeChart.root.appendChild(panel);
|
|
3169
|
+
}
|
|
3170
|
+
panel.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
3171
|
+
panel.title = payload.volumeIsCount
|
|
3172
|
+
? 'Volume: feed publish count per bucket'
|
|
3173
|
+
: 'Volume units — click to switch base/quote';
|
|
3174
|
+
const stats = visibleCandleStats();
|
|
3175
|
+
if (!stats) { panel.style.display = 'none'; return; }
|
|
3176
|
+
panel.style.display = 'block';
|
|
3177
|
+
panel.innerHTML = '<div style="color:#e8eef5">max ' + fmtVolumeWithSym(stats.vol) + '</div>';
|
|
3178
|
+
}
|
|
3179
|
+
function scheduleStatPanels() {
|
|
3180
|
+
if (rangePanelRaf) return;
|
|
3181
|
+
rangePanelRaf = requestAnimationFrame(() => {
|
|
3182
|
+
rangePanelRaf = 0;
|
|
3183
|
+
renderRangePanel();
|
|
3184
|
+
renderVolumePanel();
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
// uPlot does not recompute an auto Y scale when X moves via
|
|
3188
|
+
// setScale('x') — the scale range fns only run on init/data
|
|
3189
|
+
// change. Refit Y to the visible window after every timeframe
|
|
3190
|
+
// move so candles/volume follow the pan/zoom instead of getting
|
|
3191
|
+
// stuck outside the view. A user-locked price range (axis
|
|
3192
|
+
// wheel/drag → manualYRange) is respected; double-click the
|
|
3193
|
+
// price axis to return to autofit. y-keyed setScale calls never
|
|
3194
|
+
// schedule a refit, so this cannot loop with itself.
|
|
3195
|
+
function yRangeDirty(u, vis) {
|
|
3196
|
+
const s = u.scales.y || {};
|
|
3197
|
+
if (!Number.isFinite(s.min) || !Number.isFinite(s.max)) return true;
|
|
3198
|
+
const span = Math.max(1e-12, Math.abs(vis[1] - vis[0]));
|
|
3199
|
+
return Math.abs(s.min - vis[0]) / span > 1e-9 || Math.abs(s.max - vis[1]) / span > 1e-9;
|
|
3200
|
+
}
|
|
3201
|
+
function scheduleYRefit() {
|
|
3202
|
+
if (yRefitRaf) return;
|
|
3203
|
+
yRefitRaf = requestAnimationFrame(() => {
|
|
3204
|
+
yRefitRaf = 0;
|
|
3205
|
+
if (priceChart && !manualYRange) {
|
|
3206
|
+
const vis = visiblePriceRange(priceChart);
|
|
3207
|
+
if (vis && yRangeDirty(priceChart, vis)) priceChart.setScale('y', { min: vis[0], max: vis[1] });
|
|
3208
|
+
}
|
|
3209
|
+
if (volumeChart) {
|
|
3210
|
+
const vvis = visibleVolumeRange(volumeChart);
|
|
3211
|
+
if (vvis && yRangeDirty(volumeChart, vvis)) volumeChart.setScale('y', { min: vvis[0], max: vvis[1] });
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
padXRight();
|
|
3216
|
+
renderMarketPanel();
|
|
3217
|
+
renderRangePanel();
|
|
3218
|
+
renderVolumePanel();
|
|
3219
|
+
|
|
2295
3220
|
window.addEventListener('resize', () => {
|
|
2296
3221
|
if (!charts.length) return;
|
|
2297
3222
|
charts.forEach((chart) => {
|
|
2298
3223
|
const el = chart === priceChart ? priceEl : volumeEl;
|
|
3224
|
+
if (el.clientWidth <= 0 || el.clientHeight <= 0) return;
|
|
2299
3225
|
chart.setSize({ width: el.clientWidth, height: el.clientHeight });
|
|
2300
3226
|
});
|
|
2301
3227
|
});
|
|
2302
3228
|
|
|
3229
|
+
window.addEventListener('keydown', (e) => {
|
|
3230
|
+
const tag = (document.activeElement && document.activeElement.tagName) || '';
|
|
3231
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
3232
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '0' && charts.length) {
|
|
3233
|
+
const first = currentCandles[0]?.time;
|
|
3234
|
+
const last = currentCandles[currentCandles.length - 1]?.time;
|
|
3235
|
+
if (Number.isFinite(first) && Number.isFinite(last)) syncXRange(first, last);
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
|
|
2303
3239
|
${zoomResetScript()}
|
|
2304
3240
|
})();
|
|
2305
3241
|
</script>
|
|
2306
3242
|
</body>
|
|
2307
3243
|
</html>`;
|
|
2308
3244
|
}
|
|
2309
|
-
export { generateHTML, normalizeCandle, loadMarketProfiles };
|
|
3245
|
+
export { generateHTML, normalizeCandle, loadMarketProfiles, resolveChartStorageKey, sanitizeStorageComponent, TRADINGVIEW_PREFS_KEY_PREFIX, TRADINGVIEW_SYNC_KEY };
|
|
2310
3246
|
//# sourceMappingURL=tradingview_uplot_chart_generator.js.map
|