dexbot 1.5.2 → 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 +79 -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 +11 -1
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/tradingview/README.md +32 -10
- package/analysis/tradingview/analyze_tradingview.ts +118 -3
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1392 -125
- 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 +8 -1
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +8 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +137 -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 +1387 -125
- 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/asymmetric_bounds.d.ts +33 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
- package/dist/market_adapter/core/asymmetric_bounds.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 +10 -18
- 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 +62 -37
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +797 -569
- 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 +585 -97
- 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 +170 -51
- 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 +66 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +238 -12
- 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 +90 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +20 -12
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +33 -12
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +308 -28
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +862 -38
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +37 -30
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +130 -101
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +3 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +11 -3
- 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 +9 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { MARKET_ADAPTER } from '../../modules/constants.js';
|
|
4
|
-
import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT } from '../chart_utils.js';
|
|
4
|
+
import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT, embedFunctionSources } from '../chart_utils.js';
|
|
5
5
|
import { cursorCSS, uplotBgCSS } from '../chart_css.js';
|
|
6
6
|
import { zoomResetScript } from '../chart_ui.js';
|
|
7
7
|
import { normalizeCandle } from '../math_utils.js';
|
|
@@ -9,6 +9,9 @@ import { PATHS } from '../../modules/paths.js';
|
|
|
9
9
|
import { getStorage } from '../../modules/storage/index.js';
|
|
10
10
|
const { readJSON } = getStorage();
|
|
11
11
|
import { getErrorMessage } from '../../modules/utils/errors.js';
|
|
12
|
+
import { parseRelativeMultiplier } from '../../modules/order/utils/math.js';
|
|
13
|
+
import { computeAverageAmaSlopePct, computeAmaSlopeClipThreshold } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
14
|
+
import { resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, } from '../../market_adapter/core/asymmetric_bounds.js';
|
|
12
15
|
function inferBaseIntervalSeconds(candles, fallback = 3600) {
|
|
13
16
|
if (!Array.isArray(candles) || candles.length < 2)
|
|
14
17
|
return fallback;
|
|
@@ -83,6 +86,25 @@ function resolveAmaDefaults({ meta, data, marketProfiles } = {}) {
|
|
|
83
86
|
: Number(source.slowPeriod),
|
|
84
87
|
};
|
|
85
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
|
+
}
|
|
86
108
|
function generateHTML(data, title = 'TradingView Style Research') {
|
|
87
109
|
const rawCandles = Array.isArray(data.candles) ? data.candles : [];
|
|
88
110
|
const candles = rawCandles.map(normalizeCandle).filter(Boolean);
|
|
@@ -97,12 +119,38 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
97
119
|
{ label: '4h', seconds: 14400 },
|
|
98
120
|
{ label: '1d', seconds: 86400 },
|
|
99
121
|
{ label: '1w', seconds: 604800 },
|
|
122
|
+
{ label: '1M', seconds: 2592000, calendar: 'month' },
|
|
100
123
|
].map((item) => ({ ...item, enabled: item.seconds >= baseIntervalSeconds }));
|
|
101
124
|
const defaultTimeframe = timeframes.find((item) => item.label === data.defaultTimeframe && item.enabled)
|
|
102
125
|
|| timeframes.find((item) => item.enabled)
|
|
103
126
|
|| timeframes[0];
|
|
104
127
|
const marketProfiles = data.marketProfiles || loadMarketProfiles();
|
|
105
128
|
const defaultAmaConfig = resolveAmaDefaults({ meta, data, marketProfiles });
|
|
129
|
+
const rangeSlope = {
|
|
130
|
+
lookbackBars: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_LOOKBACK_BARS,
|
|
131
|
+
maxSlopePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_MAX_SLOPE_PCT,
|
|
132
|
+
neutralZonePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_NEUTRAL_ZONE_PCT,
|
|
133
|
+
maxSlopeOffset: MARKET_ADAPTER.DYNAMIC_WEIGHT_ASYMMETRIC_OFFSET_CLAMP,
|
|
134
|
+
maxAsymmetryFactor: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR,
|
|
135
|
+
minScaleSlots: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS,
|
|
136
|
+
clipPercentile: MARKET_ADAPTER.DYNAMIC_WEIGHT_CLIP_PERCENTILE,
|
|
137
|
+
};
|
|
138
|
+
function defaultRangeSpan(input) {
|
|
139
|
+
const cli = Number(input.rangeSpan);
|
|
140
|
+
if (Number.isFinite(cli))
|
|
141
|
+
return Math.min(2, Math.max(1.2, cli));
|
|
142
|
+
const down = parseRelativeMultiplier(input.grid?.minPrice);
|
|
143
|
+
const up = parseRelativeMultiplier(input.grid?.maxPrice);
|
|
144
|
+
if (down != null && down > 1 && up != null && up > 1)
|
|
145
|
+
return Math.min(2, Math.max(1.2, (down + up) / 2));
|
|
146
|
+
const vd = Number(input.grid?.minPrice);
|
|
147
|
+
const vu = Number(input.grid?.maxPrice);
|
|
148
|
+
if (Number.isFinite(vd) && Number.isFinite(vu) && vd > 0 && vu > vd) {
|
|
149
|
+
const ref = Math.sqrt(vd * vu);
|
|
150
|
+
return Math.min(2, Math.max(1.2, (ref / vd + vu / ref) / 2));
|
|
151
|
+
}
|
|
152
|
+
return 1.55;
|
|
153
|
+
}
|
|
106
154
|
const defaults = {
|
|
107
155
|
smaPeriod: Math.max(1, Math.round(data.smaPeriod ?? 500)),
|
|
108
156
|
amaDefaults: defaultAmaConfig,
|
|
@@ -111,6 +159,21 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
111
159
|
vwapEnabled: data.vwapEnabled === true,
|
|
112
160
|
vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
|
|
113
161
|
priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
|
|
162
|
+
rangeEnabled: data.rangeEnabled === true,
|
|
163
|
+
rangeScaleEnabled: data.rangeScaleEnabled === true,
|
|
164
|
+
rangeWidthPct: Number.isFinite(Number(data.rangeWidthPct)) && Number(data.rangeWidthPct) > 0
|
|
165
|
+
? Number(data.rangeWidthPct)
|
|
166
|
+
: 2,
|
|
167
|
+
rangeSpan: defaultRangeSpan(data),
|
|
168
|
+
grid: data.grid && data.grid.minPrice != null && data.grid.maxPrice != null ? {
|
|
169
|
+
minPrice: data.grid.minPrice,
|
|
170
|
+
maxPrice: data.grid.maxPrice,
|
|
171
|
+
incrementPercent: Number(data.grid.incrementPercent) > 0 ? Number(data.grid.incrementPercent) : null,
|
|
172
|
+
maxAsymmetryFactor: Number.isFinite(Number(data.grid.maxAsymmetryFactor)) && Number(data.grid.maxAsymmetryFactor) > 0
|
|
173
|
+
? Number(data.grid.maxAsymmetryFactor)
|
|
174
|
+
: rangeSlope.maxAsymmetryFactor,
|
|
175
|
+
} : null,
|
|
176
|
+
rangeSlope,
|
|
114
177
|
};
|
|
115
178
|
const assetLabelA = meta.assetA?.symbol || meta.assetA?.id || 'Asset A';
|
|
116
179
|
const assetLabelB = meta.assetB?.symbol || meta.assetB?.id || 'Asset B';
|
|
@@ -121,7 +184,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
121
184
|
? `${Math.round(baseIntervalSeconds / 86400)}d`
|
|
122
185
|
: `${Math.round(baseIntervalSeconds / 3600)}h`;
|
|
123
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);
|
|
124
190
|
const payload = {
|
|
191
|
+
storageKey,
|
|
192
|
+
syncKey: TRADINGVIEW_SYNC_KEY,
|
|
125
193
|
candles,
|
|
126
194
|
timeframes,
|
|
127
195
|
defaultTimeframe: defaultTimeframe.label,
|
|
@@ -132,6 +200,20 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
132
200
|
vwapEnabled: defaults.vwapEnabled,
|
|
133
201
|
vwapBars: defaults.vwapBars,
|
|
134
202
|
priceScale: defaults.priceScale,
|
|
203
|
+
rangeEnabled: defaults.rangeEnabled,
|
|
204
|
+
rangeScaleEnabled: defaults.rangeScaleEnabled,
|
|
205
|
+
rangeWidthPct: defaults.rangeWidthPct,
|
|
206
|
+
rangeSpan: defaults.rangeSpan,
|
|
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,
|
|
216
|
+
rangeSlope,
|
|
135
217
|
defaultPairMode,
|
|
136
218
|
assetLabelA,
|
|
137
219
|
assetLabelB,
|
|
@@ -140,6 +222,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
140
222
|
poolLabel,
|
|
141
223
|
intervalLabel,
|
|
142
224
|
amaDefaultsSource: marketProfiles ? 'market_profiles' : 'constants',
|
|
225
|
+
volumeIsCount: !!(meta?.feed),
|
|
143
226
|
};
|
|
144
227
|
return `<!doctype html>
|
|
145
228
|
<html lang="en">
|
|
@@ -174,9 +257,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
174
257
|
#brand { display: flex; flex-direction: column; gap: 4px; min-width: 240px; }
|
|
175
258
|
#title { font-size: 15px; font-weight: 700; color: #f5f8fb; }
|
|
176
259
|
#subtitle { font-size: 11px; color: #b9c6d4; text-transform: uppercase; letter-spacing: 0.6px; }
|
|
177
|
-
#toolbar { display: flex; flex-
|
|
260
|
+
#toolbar { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
|
|
261
|
+
.toolbar-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
|
|
178
262
|
.group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
|
|
179
263
|
.group:first-child { padding-left: 0; border-left: 0; }
|
|
264
|
+
.primary-toolbar-row .group { padding-left: 0; border-left: 0; }
|
|
180
265
|
:root {
|
|
181
266
|
--control-height: 36px;
|
|
182
267
|
}
|
|
@@ -199,6 +284,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
199
284
|
background: linear-gradient(180deg, rgba(35,53,79,0.95), rgba(22,32,47,0.98));
|
|
200
285
|
}
|
|
201
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
|
+
}
|
|
202
292
|
.indicator {
|
|
203
293
|
display: inline-flex;
|
|
204
294
|
align-items: center;
|
|
@@ -384,11 +474,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
384
474
|
background: rgba(10,14,20,0.72);
|
|
385
475
|
backdrop-filter: blur(10px);
|
|
386
476
|
}
|
|
387
|
-
.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; }
|
|
388
478
|
.legend-line + .legend-line { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.06); }
|
|
389
479
|
.legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
390
480
|
.legend-dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
|
|
391
|
-
.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; }
|
|
392
482
|
.legend-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #eef4fb; font-weight: 700; }
|
|
393
483
|
.status { display: inline-flex; align-items: center; gap: 8px; font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
394
484
|
.pill { border: 1px solid rgba(255,255,255,0.06); border-radius: 999px; padding: 4px 8px; background: rgba(255,255,255,0.03); }
|
|
@@ -396,7 +486,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
396
486
|
${cursorCSS()}
|
|
397
487
|
@media (max-width: 980px) {
|
|
398
488
|
#topbar { flex-direction: column; align-items: flex-start; }
|
|
399
|
-
#toolbar {
|
|
489
|
+
#toolbar { align-items: flex-start; }
|
|
490
|
+
.toolbar-row { justify-content: flex-start; }
|
|
400
491
|
.group { padding-left: 0; border-left: 0; flex-wrap: wrap; }
|
|
401
492
|
#legend { min-width: 0; max-width: calc(100vw - 24px); }
|
|
402
493
|
}
|
|
@@ -410,9 +501,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
410
501
|
<div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
|
|
411
502
|
</div>
|
|
412
503
|
<div id="toolbar">
|
|
413
|
-
<div class="
|
|
414
|
-
${timeframes.map((item) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
|
|
415
|
-
</div>
|
|
504
|
+
<div class="toolbar-row primary-toolbar-row">
|
|
416
505
|
<div class="group">
|
|
417
506
|
<div class="indicator">
|
|
418
507
|
<span class="tag">pair</span>
|
|
@@ -426,9 +515,13 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
426
515
|
<span class="tag">scale</span>
|
|
427
516
|
<button type="button" class="scale-toggle${defaults.priceScale === 'linear' ? ' is-linear' : ''}" id="scale-toggle" data-scale="${escapeHtml(defaults.priceScale)}" aria-label="Toggle price scale" title="Toggle price scale">${defaults.priceScale === 'linear' ? 'Linear' : 'Log'}</button>
|
|
428
517
|
</div>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="group" id="tf-group">
|
|
520
|
+
${timeframes.map((item) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
|
|
521
|
+
</div>
|
|
522
|
+
<div class="group">
|
|
429
523
|
<div class="indicator">
|
|
430
524
|
<label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
|
|
431
|
-
<span class="tag">period</span>
|
|
432
525
|
<input type="text" inputmode="numeric" id="sma-period" value="${defaults.smaPeriod}">
|
|
433
526
|
<span class="step-stack">
|
|
434
527
|
<button type="button" class="step-btn" id="sma-period-inc">▲</button>
|
|
@@ -437,13 +530,18 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
437
530
|
</div>
|
|
438
531
|
<div class="indicator">
|
|
439
532
|
<label><input type="checkbox" id="vwap-toggle"${defaults.vwapEnabled ? ' checked' : ''}> VWMA</label>
|
|
440
|
-
<span class="tag">bars</span>
|
|
441
533
|
<input type="text" inputmode="numeric" id="vwap-bars" value="${defaults.vwapBars}">
|
|
442
534
|
<span class="step-stack">
|
|
443
535
|
<button type="button" class="step-btn" id="vwap-bars-inc">▲</button>
|
|
444
536
|
<button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
|
|
445
537
|
</span>
|
|
446
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>' : '')}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="toolbar-row">
|
|
544
|
+
<div class="group">
|
|
447
545
|
<div class="indicator">
|
|
448
546
|
<label><input type="checkbox" id="ama-toggle"${defaults.amaEnabled ? ' checked' : ''}> AMA</label>
|
|
449
547
|
<span class="tag">er</span>
|
|
@@ -464,15 +562,26 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
464
562
|
<button type="button" class="step-btn" id="ama-slow-inc">▲</button>
|
|
465
563
|
<button type="button" class="step-btn" id="ama-slow-dec">▼</button>
|
|
466
564
|
</span>
|
|
467
|
-
<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>
|
|
468
569
|
</div>
|
|
469
|
-
<div class="indicator"
|
|
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">
|
|
571
|
+
<label><input type="checkbox" id="range-toggle"${defaults.rangeEnabled ? ' checked' : ''}> Range</label>
|
|
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>
|
|
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">
|
|
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>
|
|
576
|
+
</span>
|
|
577
|
+
</div>
|
|
578
|
+
<div class="indicator">
|
|
470
579
|
<label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
|
|
471
|
-
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:
|
|
580
|
+
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:90px;vertical-align:middle" disabled>
|
|
472
581
|
<span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
|
|
473
582
|
</div>
|
|
474
583
|
</div>
|
|
475
|
-
|
|
584
|
+
</div>
|
|
476
585
|
</div>
|
|
477
586
|
</div>
|
|
478
587
|
<div id="chart-shell">
|
|
@@ -481,20 +590,16 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
481
590
|
<span class="legend-item"><span class="legend-label">Time</span> <span class="legend-value" id="legend-time">-</span></span>
|
|
482
591
|
<span class="legend-item"><span class="legend-label">C</span> <span class="legend-value" id="legend-close">-</span></span>
|
|
483
592
|
<span class="legend-item"><span class="legend-label">Delta</span> <span class="legend-value" id="legend-delta">-</span></span>
|
|
484
|
-
<span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume">-</span></span>
|
|
485
|
-
<span class="legend-item"><span class="legend-label">Scale</span> <span class="legend-value" id="legend-scale">-</span></span>
|
|
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>
|
|
486
594
|
</div>
|
|
487
595
|
<div class="legend-line">
|
|
488
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>
|
|
489
|
-
<span class="legend-item"><span class="legend-dot" style="background:#2dd4bf"></span><span class="legend-label">AMA</span> <span class="legend-value" id="legend-ama">-</span></span>
|
|
490
|
-
<span class="legend-item"><span class="legend-dot" style="background:#22c55e"></span><span class="legend-label">AMA Init</span> <span class="legend-value" id="legend-sma-init">-</span></span>
|
|
491
|
-
<span class="legend-item"><span class="legend-dot" style="background:#a855f7"></span><span class="legend-label">Off Init</span> <span class="legend-value" id="legend-off-init">-</span></span>
|
|
492
|
-
<span class="legend-item"><span class="legend-dot" style="background:#a855f7"></span><span class="legend-label">AMA Off</span> <span class="legend-value" id="legend-ama-off">-</span></span>
|
|
493
|
-
|
|
494
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>
|
|
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>
|
|
495
600
|
</div>
|
|
496
601
|
</div>
|
|
497
|
-
<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>
|
|
498
603
|
<div id="volume-chart"></div>
|
|
499
604
|
</div>
|
|
500
605
|
</div>
|
|
@@ -509,7 +614,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
509
614
|
const baseCloseValues = baseCandles.map((c) => c.close);
|
|
510
615
|
const timeframes = Array.isArray(payload.timeframes) ? payload.timeframes.slice() : [];
|
|
511
616
|
const timeframeMap = new Map(timeframes.map((item) => [item.label, item]));
|
|
512
|
-
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';
|
|
513
623
|
|
|
514
624
|
const state = loadState();
|
|
515
625
|
let currentTimeframe = state.timeframe || payload.defaultTimeframe || '1h';
|
|
@@ -527,6 +637,25 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
527
637
|
: Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod});
|
|
528
638
|
let currentVwapEnabled = state.vwapEnabled ?? !!payload.vwapEnabled;
|
|
529
639
|
let currentVwapBars = Number.isFinite(state.vwapBars) ? state.vwapBars : Number(payload.vwapBars || 500);
|
|
640
|
+
let currentRangeEnabled = state.rangeEnabled ?? !!payload.rangeEnabled;
|
|
641
|
+
let currentRangeScaleEnabled = state.rangeScaleEnabled ?? !!payload.rangeScaleEnabled;
|
|
642
|
+
let currentRangeWidthPct = Number.isFinite(state.rangeWidthPct) && Number(state.rangeWidthPct) > 0
|
|
643
|
+
? Number(state.rangeWidthPct)
|
|
644
|
+
: (Number.isFinite(Number(payload.rangeWidthPct)) && Number(payload.rangeWidthPct) > 0 ? Number(payload.rangeWidthPct) : 2);
|
|
645
|
+
let currentRangeSpan = Number.isFinite(state.rangeSpan) && Number(state.rangeSpan) > 0
|
|
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;
|
|
530
659
|
let currentAmaInitOffsetEnabled = false;
|
|
531
660
|
let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
|
|
532
661
|
let currentPriceScale = state.priceScale || payload.priceScale || 'log';
|
|
@@ -548,6 +677,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
548
677
|
let currentSmaInit = [];
|
|
549
678
|
let currentSmaInitOff = [];
|
|
550
679
|
let currentAmaOff = [];
|
|
680
|
+
let currentRangeUpper = [];
|
|
681
|
+
let currentRangeLower = [];
|
|
551
682
|
let priceChart = null;
|
|
552
683
|
let volumeChart = null;
|
|
553
684
|
let lastRenderedPriceScale = null;
|
|
@@ -557,6 +688,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
557
688
|
let lastCandleKey = null;
|
|
558
689
|
let pendingRange = null;
|
|
559
690
|
let pendingRangeRaf = 0;
|
|
691
|
+
let rangePanelRaf = 0;
|
|
692
|
+
let yRefitRaf = 0;
|
|
560
693
|
let xMin = 0;
|
|
561
694
|
let xMax = 0;
|
|
562
695
|
let smaWorker = null;
|
|
@@ -575,27 +708,18 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
575
708
|
// TradingView-exporter interaction overrides (PR #3 / #11 feature):
|
|
576
709
|
// redeclare after UPLOT_SHARED_SCRIPT so these hoisted declarations win
|
|
577
710
|
// for this page only, without mutating the shared stack used by other charts.
|
|
578
|
-
function clampXRange(min, max) {
|
|
579
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return { min: xMin, max: xMax };
|
|
580
|
-
// Salli tyhjaa tilaa datan molemmin puolin (TradingView-tyyli):
|
|
581
|
-
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
582
|
-
const dataSpan = Math.max(1, xMax - xMin);
|
|
583
|
-
const maxPad = dataSpan * 0.75;
|
|
584
|
-
let lo = Math.max(xMin - maxPad, min);
|
|
585
|
-
let hi = Math.min(xMax + maxPad, max);
|
|
586
|
-
if (lo < xMin && hi <= lo) lo = xMin;
|
|
587
|
-
if (hi > xMax + maxPad) hi = xMax + maxPad;
|
|
588
|
-
if (hi <= lo) return { min: xMin, max: xMax };
|
|
589
|
-
return { min: lo, max: hi };
|
|
590
|
-
}
|
|
591
711
|
function bindWheelZoom(chart) {
|
|
592
712
|
chart.root.addEventListener('wheel', (e) => {
|
|
593
713
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
594
714
|
e.preventDefault();
|
|
595
715
|
const rect = chart.root.getBoundingClientRect();
|
|
596
|
-
|
|
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))) {
|
|
597
720
|
const centerY = chart.posToVal(e.clientY - rect.top, 'y');
|
|
598
|
-
|
|
721
|
+
const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
722
|
+
zoomYAt(chart, centerY, delta < 0 ? 0.91 : 1.10);
|
|
599
723
|
return;
|
|
600
724
|
}
|
|
601
725
|
e.stopPropagation();
|
|
@@ -614,12 +738,132 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
614
738
|
syncXRange(center - nextSpan * ratio, center - nextSpan * ratio + nextSpan);
|
|
615
739
|
}, { passive: false });
|
|
616
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
|
+
}
|
|
617
861
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
618
|
-
// Significant-digit price formatting (
|
|
619
|
-
// DEXBot order-price style (formatCurrency)
|
|
862
|
+
// Significant-digit price formatting (4 "active" digits), matching the
|
|
863
|
+
// DEXBot order-price style (formatCurrency).
|
|
620
864
|
// Very small values get SI micro-suffixes (m/µ/n/p/...); large values
|
|
621
|
-
// get comma grouping. Examples: 34.
|
|
622
|
-
// 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".
|
|
623
867
|
function fmtPrice(v) {
|
|
624
868
|
if (v == null || !Number.isFinite(v)) return '-';
|
|
625
869
|
const num = Number(v);
|
|
@@ -723,6 +967,9 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
723
967
|
ticks: { stroke: '#30363d', width: 1 },
|
|
724
968
|
font: '11px Segoe UI, sans-serif',
|
|
725
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
|
+
}
|
|
726
973
|
const xScale = u.scales.x || {};
|
|
727
974
|
const spanSec = Number.isFinite(xScale.min) && Number.isFinite(xScale.max)
|
|
728
975
|
? Math.max(0, xScale.max - xScale.min)
|
|
@@ -746,6 +993,13 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
746
993
|
amaSlowPeriod: currentAmaSlowPeriod,
|
|
747
994
|
vwapEnabled: currentVwapEnabled,
|
|
748
995
|
vwapBars: currentVwapBars,
|
|
996
|
+
rangeEnabled: currentRangeEnabled,
|
|
997
|
+
rangeScaleEnabled: currentRangeScaleEnabled,
|
|
998
|
+
rangeWidthPct: currentRangeWidthPct,
|
|
999
|
+
rangeSpan: currentRangeSpan,
|
|
1000
|
+
ordersVisible: currentOrdersVisible,
|
|
1001
|
+
volumeVisible: currentVolumeVisible,
|
|
1002
|
+
volumeMode: currentVolumeMode,
|
|
749
1003
|
amaInitOffset: currentAmaInitOffset,
|
|
750
1004
|
priceScale: currentPriceScale,
|
|
751
1005
|
pairMode: currentPairMode,
|
|
@@ -760,6 +1014,75 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
760
1014
|
? (payload.pairLabelInverse || (payload.assetLabelB + '/' + payload.assetLabelA))
|
|
761
1015
|
: (payload.pairLabelNormal || (payload.assetLabelA + '/' + payload.assetLabelB));
|
|
762
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
|
+
}
|
|
763
1086
|
function refreshSubtitle() {
|
|
764
1087
|
const subtitleEl = document.getElementById('subtitle');
|
|
765
1088
|
if (!subtitleEl) return;
|
|
@@ -808,38 +1131,65 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
808
1131
|
seriesCache.set(key, state);
|
|
809
1132
|
return state;
|
|
810
1133
|
}
|
|
811
|
-
function aggregateCandles(rows, seconds) {
|
|
1134
|
+
function aggregateCandles(rows, seconds, label) {
|
|
1135
|
+
const monthly = String(label || '') === '1M';
|
|
812
1136
|
const bucketSec = Math.max(1, Math.round(seconds || 3600));
|
|
813
|
-
const cacheKey = currentPairMode + '|' + bucketSec;
|
|
1137
|
+
const cacheKey = currentPairMode + '|' + (monthly ? '1M' : String(bucketSec));
|
|
814
1138
|
if (aggregateCache.has(cacheKey)) return aggregateCache.get(cacheKey);
|
|
815
1139
|
const out = [];
|
|
816
1140
|
const idxs = [];
|
|
817
1141
|
let cur = null;
|
|
818
1142
|
let curBucket = null;
|
|
819
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
|
+
};
|
|
820
1155
|
for (let i = 0; i < rows.length; i++) {
|
|
821
1156
|
const row = rows[i];
|
|
822
1157
|
const ts = Number(row.time);
|
|
823
1158
|
if (!Number.isFinite(ts)) continue;
|
|
824
|
-
|
|
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
|
+
}
|
|
825
1166
|
if (!cur || bucket !== curBucket) {
|
|
826
1167
|
if (cur) {
|
|
827
|
-
|
|
1168
|
+
finishBucket(cur);
|
|
828
1169
|
idxs.push(curIdx);
|
|
829
1170
|
}
|
|
830
1171
|
curBucket = bucket;
|
|
831
1172
|
curIdx = i;
|
|
832
|
-
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
|
+
};
|
|
833
1182
|
} else {
|
|
834
1183
|
cur.high = Math.max(cur.high, row.high);
|
|
835
1184
|
cur.low = Math.min(cur.low, row.low);
|
|
836
1185
|
cur.close = row.close;
|
|
837
1186
|
cur.volume += row.volume || 0;
|
|
1187
|
+
cur.barCount += 1;
|
|
838
1188
|
curIdx = i;
|
|
839
1189
|
}
|
|
840
1190
|
}
|
|
841
1191
|
if (cur) {
|
|
842
|
-
|
|
1192
|
+
finishBucket(cur);
|
|
843
1193
|
idxs.push(curIdx);
|
|
844
1194
|
}
|
|
845
1195
|
const result = { candles: out, idxs };
|
|
@@ -1075,6 +1425,90 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1075
1425
|
vwapCache.set(cacheKey, out);
|
|
1076
1426
|
return out;
|
|
1077
1427
|
}
|
|
1428
|
+
// Bot-grid range band: min/max derive ONLY from the live AMA price.
|
|
1429
|
+
// All math below runs the canonical market_adapter/grid sources,
|
|
1430
|
+
// embedded verbatim (not hand-copied): computeAverageAmaSlopePct,
|
|
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].
|
|
1436
|
+
// Never reads candles, pair-display mapping, or axis/zoom state —
|
|
1437
|
+
// inversion and timeframe sampling apply to the AMA first.
|
|
1438
|
+
${embedFunctionSources([computeAverageAmaSlopePct, computeAmaSlopeClipThreshold, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1439
|
+
function computeRangeBand(baseAma) {
|
|
1440
|
+
const n = Array.isArray(baseAma) ? baseAma.length : 0;
|
|
1441
|
+
const upper = new Array(n).fill(null);
|
|
1442
|
+
const lower = new Array(n).fill(null);
|
|
1443
|
+
const trend = new Array(n).fill(0);
|
|
1444
|
+
const slopeCfg = payload.rangeSlope || {};
|
|
1445
|
+
const gridCfg = payload.grid || null;
|
|
1446
|
+
const lookback = Math.max(1, Math.round(Number(slopeCfg.lookbackBars) || 9));
|
|
1447
|
+
const maxSlope = Number(slopeCfg.maxSlopePct) > 0 ? Number(slopeCfg.maxSlopePct) : 0.09;
|
|
1448
|
+
const neutral = Number(slopeCfg.neutralZonePct) >= 0 ? Number(slopeCfg.neutralZonePct) : 0;
|
|
1449
|
+
const maxSlopeOffset = Number(slopeCfg.maxSlopeOffset) > 0 ? Number(slopeCfg.maxSlopeOffset) : 0.5;
|
|
1450
|
+
const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
|
|
1451
|
+
? Number(gridCfg.maxAsymmetryFactor)
|
|
1452
|
+
: (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
|
|
1453
|
+
const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
|
|
1454
|
+
const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
|
|
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);
|
|
1466
|
+
const slopeScaling = !!currentRangeScaleEnabled;
|
|
1467
|
+
if (!currentAmaEnabled || n === 0) return { upper, lower, trend };
|
|
1468
|
+
for (let i = 0; i < n; i++) {
|
|
1469
|
+
const ama = baseAma[i];
|
|
1470
|
+
const past = i - lookback >= 0 ? baseAma[i - lookback] : null;
|
|
1471
|
+
if (!Number.isFinite(ama) || ama <= 0 || !Number.isFinite(past) || past <= 0) continue;
|
|
1472
|
+
const slopePct = computeAverageAmaSlopePct(ama, past, lookback);
|
|
1473
|
+
if (slopePct == null || !Number.isFinite(slopePct)) continue;
|
|
1474
|
+
const csp = Math.max(-clipThreshold, Math.min(clipThreshold, slopePct));
|
|
1475
|
+
const dir = Math.abs(csp) <= neutral ? 0 : (csp > 0 ? 1 : -1);
|
|
1476
|
+
// Symmetric base from this bar's AMA and the user span —
|
|
1477
|
+
// no chart or config price level.
|
|
1478
|
+
let rMin = ama / span;
|
|
1479
|
+
let rMax = ama * span;
|
|
1480
|
+
if (slopeScaling && dir !== 0) {
|
|
1481
|
+
// Canonical tilt: clipped slope offset applied through
|
|
1482
|
+
// asymmetric_bounds, then the narrowing-side guard.
|
|
1483
|
+
const trendName = dir > 0 ? 'UP' : 'DOWN';
|
|
1484
|
+
const slopeOffset = clamp(csp / maxSlope, -1, 1) * maxSlopeOffset;
|
|
1485
|
+
const tilt = applyAsymmetricBounds({
|
|
1486
|
+
centerPrice: ama,
|
|
1487
|
+
minPrice: ama / span,
|
|
1488
|
+
maxPrice: ama * span,
|
|
1489
|
+
trend: trendName,
|
|
1490
|
+
slopeOffset,
|
|
1491
|
+
maxSlopeOffset,
|
|
1492
|
+
maxAsymmetryFactor: maxAsym,
|
|
1493
|
+
});
|
|
1494
|
+
const guard = applyNarrowingSideGuard({
|
|
1495
|
+
centerPrice: ama,
|
|
1496
|
+
minPrice: tilt.resolvedMinPrice,
|
|
1497
|
+
maxPrice: tilt.resolvedMaxPrice,
|
|
1498
|
+
trend: trendName,
|
|
1499
|
+
incrementPercent: inc,
|
|
1500
|
+
minScaleSlots: minSlots,
|
|
1501
|
+
});
|
|
1502
|
+
rMin = guard.minPrice;
|
|
1503
|
+
rMax = guard.maxPrice;
|
|
1504
|
+
}
|
|
1505
|
+
if (!Number.isFinite(rMin) || !Number.isFinite(rMax) || rMin <= 0 || rMax <= rMin) continue;
|
|
1506
|
+
upper[i] = rMax;
|
|
1507
|
+
lower[i] = rMin;
|
|
1508
|
+
trend[i] = dir;
|
|
1509
|
+
}
|
|
1510
|
+
return { upper, lower, trend };
|
|
1511
|
+
}
|
|
1078
1512
|
function lowerBound(arr, value) {
|
|
1079
1513
|
let lo = 0;
|
|
1080
1514
|
let hi = arr.length;
|
|
@@ -1095,6 +1529,25 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1095
1529
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1096
1530
|
let min = Infinity;
|
|
1097
1531
|
let max = -Infinity;
|
|
1532
|
+
// Range scaling active: fit the price axis to the full envelope
|
|
1533
|
+
// (lower ↔ upper through AMA) instead of the candles, so the
|
|
1534
|
+
// switch visibly adjusts the range. Falls through to the candle
|
|
1535
|
+
// fit when the band has no finite values in view.
|
|
1536
|
+
if (currentRangeEnabled && currentRangeScaleEnabled && currentAmaEnabled) {
|
|
1537
|
+
let bmin = Infinity;
|
|
1538
|
+
let bmax = -Infinity;
|
|
1539
|
+
for (let i = start; i < end; i++) {
|
|
1540
|
+
const a = currentAma[i];
|
|
1541
|
+
const up = currentRangeUpper[i];
|
|
1542
|
+
const lo = currentRangeLower[i];
|
|
1543
|
+
if (Number.isFinite(a) && a > 0) { if (a < bmin) bmin = a; if (a > bmax) bmax = a; }
|
|
1544
|
+
if (Number.isFinite(up) && up > 0) { if (up < bmin) bmin = up; if (up > bmax) bmax = up; }
|
|
1545
|
+
if (Number.isFinite(lo) && lo > 0) { if (lo < bmin) bmin = lo; if (lo > bmax) bmax = lo; }
|
|
1546
|
+
}
|
|
1547
|
+
if (Number.isFinite(bmin) && Number.isFinite(bmax) && bmin > 0 && bmax > bmin) {
|
|
1548
|
+
return [bmin * 0.97, bmax * 1.03];
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1098
1551
|
for (let i = start; i < end; i++) {
|
|
1099
1552
|
const lo = currentLow[i];
|
|
1100
1553
|
const hi = currentHigh[i];
|
|
@@ -1122,7 +1575,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1122
1575
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1123
1576
|
let max = 0;
|
|
1124
1577
|
for (let i = start; i < end; i++) {
|
|
1125
|
-
const v = currentCandles[i]
|
|
1578
|
+
const v = getDisplayVolume(currentCandles[i]);
|
|
1126
1579
|
if (Number.isFinite(v) && v > max) max = v;
|
|
1127
1580
|
}
|
|
1128
1581
|
if (!Number.isFinite(max) || max <= 0) return [0, 1];
|
|
@@ -1141,12 +1594,29 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1141
1594
|
btn.title = next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale';
|
|
1142
1595
|
btn.setAttribute('aria-label', next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale');
|
|
1143
1596
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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();
|
|
1150
1620
|
}
|
|
1151
1621
|
function setIndicatorSeriesVisible(seriesIndex, visible) {
|
|
1152
1622
|
if (!priceChart) return;
|
|
@@ -1171,11 +1641,22 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1171
1641
|
refreshLegend();
|
|
1172
1642
|
saveState();
|
|
1173
1643
|
}
|
|
1644
|
+
function hideRangeBandImmediate() {
|
|
1645
|
+
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1646
|
+
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1647
|
+
// The band is a draw hook with no uPlot series: nothing else
|
|
1648
|
+
// repaints on hide, so redraw explicitly (stale pixels otherwise linger).
|
|
1649
|
+
if (priceChart && typeof priceChart.redraw === 'function') priceChart.redraw();
|
|
1650
|
+
refreshLegend();
|
|
1651
|
+
saveState();
|
|
1652
|
+
}
|
|
1174
1653
|
function hideAmaSeriesImmediate() {
|
|
1175
1654
|
currentAma = new Array(currentCandles.length).fill(null);
|
|
1176
1655
|
currentAmaOff = new Array(currentCandles.length).fill(null);
|
|
1177
1656
|
currentSmaInit = new Array(currentCandles.length).fill(null);
|
|
1178
1657
|
currentSmaInitOff = new Array(currentCandles.length).fill(null);
|
|
1658
|
+
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1659
|
+
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1179
1660
|
if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
|
|
1180
1661
|
currentPriceData[6] = currentAma;
|
|
1181
1662
|
currentPriceData[8] = currentSmaInit;
|
|
@@ -1209,14 +1690,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1209
1690
|
document.getElementById('legend-delta').textContent = Number.isFinite(delta)
|
|
1210
1691
|
? ((delta >= 0 ? '+' : '') + fmtPrice(delta))
|
|
1211
1692
|
: '-';
|
|
1212
|
-
document.getElementById('legend-volume').textContent =
|
|
1213
|
-
document.getElementById('legend-scale').textContent = currentPriceScale === 'linear' ? 'Linear' : 'Log';
|
|
1693
|
+
document.getElementById('legend-volume').textContent = fmtVolumeWithSym(getDisplayVolume(c));
|
|
1214
1694
|
document.getElementById('legend-sma').textContent = Number.isFinite(currentSma[idx]) ? fmtPrice(currentSma[idx]) : (smaPending ? '...' : '-');
|
|
1215
|
-
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1216
|
-
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1217
|
-
document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
|
|
1218
|
-
document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
|
|
1219
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]) : '-';
|
|
1220
1698
|
}
|
|
1221
1699
|
function candlePlugin() {
|
|
1222
1700
|
function drawCandles(u) {
|
|
@@ -1359,12 +1837,86 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1359
1837
|
hooks: { drawAxes: [drawVolumeBars] },
|
|
1360
1838
|
};
|
|
1361
1839
|
}
|
|
1840
|
+
function rangeBandPlugin() {
|
|
1841
|
+
function drawRangeBand(u) {
|
|
1842
|
+
if (!currentRangeEnabled || !currentAmaEnabled || !currentCandles.length) return;
|
|
1843
|
+
const xs = u.data[0];
|
|
1844
|
+
if (!Array.isArray(xs) || xs.length === 0) return;
|
|
1845
|
+
u.ctx.save();
|
|
1846
|
+
u.ctx.beginPath();
|
|
1847
|
+
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
|
1848
|
+
u.ctx.clip();
|
|
1849
|
+
// Behind candles/AMA lines (same trick as the dynamic-weight signal background).
|
|
1850
|
+
u.ctx.globalCompositeOperation = 'destination-over';
|
|
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
|
|
1855
|
+
// grid's range scaling. Paint per contiguous valid segment.
|
|
1856
|
+
let segTop = [];
|
|
1857
|
+
let segBot = [];
|
|
1858
|
+
const flush = () => {
|
|
1859
|
+
if (segTop.length > 1) {
|
|
1860
|
+
u.ctx.fillStyle = UP_FILL;
|
|
1861
|
+
u.ctx.beginPath();
|
|
1862
|
+
u.ctx.moveTo(segTop[0][0], segTop[0][1]);
|
|
1863
|
+
for (let k = 1; k < segTop.length; k++) u.ctx.lineTo(segTop[k][0], segTop[k][1]);
|
|
1864
|
+
for (let k = segTop.length - 1; k >= 0; k--) u.ctx.lineTo(segTop[k][0], segTop[k][2]);
|
|
1865
|
+
u.ctx.closePath();
|
|
1866
|
+
u.ctx.fill();
|
|
1867
|
+
}
|
|
1868
|
+
if (segBot.length > 1) {
|
|
1869
|
+
u.ctx.fillStyle = DOWN_FILL;
|
|
1870
|
+
u.ctx.beginPath();
|
|
1871
|
+
u.ctx.moveTo(segBot[0][0], segBot[0][1]);
|
|
1872
|
+
for (let k = 1; k < segBot.length; k++) u.ctx.lineTo(segBot[k][0], segBot[k][1]);
|
|
1873
|
+
for (let k = segBot.length - 1; k >= 0; k--) u.ctx.lineTo(segBot[k][0], segBot[k][2]);
|
|
1874
|
+
u.ctx.closePath();
|
|
1875
|
+
u.ctx.fill();
|
|
1876
|
+
}
|
|
1877
|
+
u.ctx.strokeStyle = 'rgba(239,83,80,0.55)';
|
|
1878
|
+
u.ctx.lineWidth = 1;
|
|
1879
|
+
u.ctx.beginPath();
|
|
1880
|
+
segTop.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1881
|
+
u.ctx.stroke();
|
|
1882
|
+
u.ctx.strokeStyle = 'rgba(38,166,154,0.55)';
|
|
1883
|
+
u.ctx.beginPath();
|
|
1884
|
+
segBot.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1885
|
+
u.ctx.stroke();
|
|
1886
|
+
segTop = [];
|
|
1887
|
+
segBot = [];
|
|
1888
|
+
};
|
|
1889
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1890
|
+
const a = currentAma[i];
|
|
1891
|
+
const up = currentRangeUpper[i];
|
|
1892
|
+
const lo = currentRangeLower[i];
|
|
1893
|
+
if (!Number.isFinite(a) || a <= 0 || !Number.isFinite(up) || up <= a
|
|
1894
|
+
|| !Number.isFinite(lo) || lo <= 0 || lo >= a) {
|
|
1895
|
+
flush();
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const x = u.valToPos(xs[i], 'x', true);
|
|
1899
|
+
const yA = u.valToPos(a, 'y', true);
|
|
1900
|
+
const yU = u.valToPos(up, 'y', true);
|
|
1901
|
+
const yL = u.valToPos(lo, 'y', true);
|
|
1902
|
+
if (!Number.isFinite(x) || !Number.isFinite(yA) || !Number.isFinite(yU) || !Number.isFinite(yL)) {
|
|
1903
|
+
flush();
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
segTop.push([x, yU, yA]);
|
|
1907
|
+
segBot.push([x, yL, yA]);
|
|
1908
|
+
}
|
|
1909
|
+
flush();
|
|
1910
|
+
u.ctx.restore();
|
|
1911
|
+
}
|
|
1912
|
+
return { hooks: { draw: [drawRangeBand] } };
|
|
1913
|
+
}
|
|
1362
1914
|
function buildData() {
|
|
1363
1915
|
const tf = timeframeMap.get(currentTimeframe) || timeframeMap.get(defaultTimeframe.label) || timeframes[0];
|
|
1364
1916
|
currentSeriesState = getSeriesState(currentPairMode);
|
|
1365
1917
|
currentSeriesCandles = currentSeriesState.candles;
|
|
1366
1918
|
currentSeriesCloseValues = currentSeriesState.closeValues;
|
|
1367
|
-
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600);
|
|
1919
|
+
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600, tf?.label || currentTimeframe);
|
|
1368
1920
|
currentCandles = aggregated.candles;
|
|
1369
1921
|
if (currentCandles.length) {
|
|
1370
1922
|
xMin = currentCandles[0].time;
|
|
@@ -1393,21 +1945,33 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1393
1945
|
currentSma = new Array(currentCandles.length).fill(null);
|
|
1394
1946
|
}
|
|
1395
1947
|
currentAma = currentAmaEnabled ? sampleSeriesByIndex(baseAma, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1948
|
+
const baseRange = computeRangeBand(baseAma);
|
|
1949
|
+
currentRangeUpper = sampleSeriesByIndex(baseRange.upper, aggregated.idxs);
|
|
1950
|
+
currentRangeLower = sampleSeriesByIndex(baseRange.lower, aggregated.idxs);
|
|
1396
1951
|
currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1397
1952
|
currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1398
1953
|
const amaCfg = currentAmaConfig();
|
|
1399
1954
|
const warmupInit = currentAmaEnabled && baseAma.length > amaCfg.erPeriod ? baseAma[amaCfg.erPeriod] : null;
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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);
|
|
1404
1968
|
const offsetInit = currentAmaEnabled && currentAmaInitOffset !== 0 && baseAmaOff.length > amaCfg.erPeriod
|
|
1405
1969
|
? baseAmaOff[amaCfg.erPeriod]
|
|
1406
1970
|
: null;
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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);
|
|
1411
1975
|
currentPriceData = [
|
|
1412
1976
|
currentCandles.map((c) => c.time),
|
|
1413
1977
|
currentOpen,
|
|
@@ -1423,7 +1987,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1423
1987
|
];
|
|
1424
1988
|
currentVolumeData = [
|
|
1425
1989
|
currentCandles.map((c) => c.time),
|
|
1426
|
-
currentCandles.map((c) => c
|
|
1990
|
+
currentCandles.map((c) => getDisplayVolume(c)),
|
|
1427
1991
|
];
|
|
1428
1992
|
return {
|
|
1429
1993
|
priceData: currentPriceData,
|
|
@@ -1440,7 +2004,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1440
2004
|
padding: [14, 8, 8, 8],
|
|
1441
2005
|
legend: { show: false },
|
|
1442
2006
|
select: { show: false },
|
|
1443
|
-
cursor: { sync: { key:
|
|
2007
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1444
2008
|
scales: {
|
|
1445
2009
|
x: { time: true },
|
|
1446
2010
|
y: {
|
|
@@ -1491,13 +2055,18 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1491
2055
|
},
|
|
1492
2056
|
],
|
|
1493
2057
|
hooks: {
|
|
1494
|
-
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(); }],
|
|
1495
2064
|
},
|
|
1496
2065
|
};
|
|
1497
2066
|
|
|
1498
2067
|
const plugin = candlePlugin();
|
|
1499
2068
|
plugin.opts(null, priceOpts);
|
|
1500
|
-
priceOpts.plugins = [plugin];
|
|
2069
|
+
priceOpts.plugins = [rangeBandPlugin(), plugin];
|
|
1501
2070
|
if (!priceChart) priceChart = new uPlot(priceOpts, data.priceData, priceEl);
|
|
1502
2071
|
else priceChart.setData(data.priceData, false);
|
|
1503
2072
|
|
|
@@ -1507,7 +2076,7 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1507
2076
|
padding: [6, 8, 8, 8],
|
|
1508
2077
|
legend: { show: false },
|
|
1509
2078
|
select: { show: false },
|
|
1510
|
-
cursor: { sync: { key:
|
|
2079
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1511
2080
|
scales: {
|
|
1512
2081
|
x: { time: true },
|
|
1513
2082
|
y: {
|
|
@@ -1528,6 +2097,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1528
2097
|
makeTimeAxis(true),
|
|
1529
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))) },
|
|
1530
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
|
+
},
|
|
1531
2105
|
};
|
|
1532
2106
|
|
|
1533
2107
|
const volumePluginInst = volumePlugin();
|
|
@@ -1540,32 +2114,33 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1540
2114
|
lastRenderedPriceScale = currentPriceScale;
|
|
1541
2115
|
return priceChart;
|
|
1542
2116
|
}
|
|
1543
|
-
// True when clientX sits over the price-axis
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
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.
|
|
1548
2122
|
function inYAxisZone(chart, clientX) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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;
|
|
1556
2136
|
}
|
|
1557
2137
|
let priceMarkerLabel = null;
|
|
1558
|
-
let priceMarkerLine = null;
|
|
1559
2138
|
function ensurePriceMarker(u) {
|
|
1560
2139
|
if (priceMarkerLabel && priceMarkerLabel.parentNode === u.root) return;
|
|
1561
2140
|
if (priceMarkerLabel && priceMarkerLabel.parentNode) priceMarkerLabel.parentNode.removeChild(priceMarkerLabel);
|
|
1562
|
-
if (priceMarkerLine && priceMarkerLine.parentNode) priceMarkerLine.parentNode.removeChild(priceMarkerLine);
|
|
1563
2141
|
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
1564
2142
|
priceMarkerLabel = document.createElement('div');
|
|
1565
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;';
|
|
1566
|
-
priceMarkerLine = document.createElement('div');
|
|
1567
|
-
priceMarkerLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:1px dashed currentColor;opacity:0.6;';
|
|
1568
|
-
u.root.appendChild(priceMarkerLine);
|
|
1569
2144
|
u.root.appendChild(priceMarkerLabel);
|
|
1570
2145
|
}
|
|
1571
2146
|
function positionPriceMarker(u) {
|
|
@@ -1576,7 +2151,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1576
2151
|
const price = last.close;
|
|
1577
2152
|
if (!Number.isFinite(price)) {
|
|
1578
2153
|
priceMarkerLabel.style.display = 'none';
|
|
1579
|
-
priceMarkerLine.style.display = 'none';
|
|
1580
2154
|
return;
|
|
1581
2155
|
}
|
|
1582
2156
|
const up = price >= (Number.isFinite(prev.close) ? prev.close : price);
|
|
@@ -1586,7 +2160,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1586
2160
|
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
1587
2161
|
if (sMin == null || sMax == null || sMax <= sMin) {
|
|
1588
2162
|
priceMarkerLabel.style.display = 'none';
|
|
1589
|
-
priceMarkerLine.style.display = 'none';
|
|
1590
2163
|
return;
|
|
1591
2164
|
}
|
|
1592
2165
|
let frac;
|
|
@@ -1603,7 +2176,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1603
2176
|
if (!overRect || overRect.height <= 0) return;
|
|
1604
2177
|
const plotTop = overRect.top - rootRect.top;
|
|
1605
2178
|
const yRel = (1 - frac) * overRect.height;
|
|
1606
|
-
|
|
2179
|
+
// No full-width dashed line by design (disturbs reading) — the
|
|
2180
|
+
// axis label above carries the last price on its own.
|
|
1607
2181
|
priceMarkerLabel.style.display = 'block';
|
|
1608
2182
|
priceMarkerLabel.textContent = fmtPriceLabel(price);
|
|
1609
2183
|
priceMarkerLabel.style.background = color;
|
|
@@ -1619,15 +2193,335 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1619
2193
|
priceMarkerLabel.style.right = '0px';
|
|
1620
2194
|
priceMarkerLabel.style.width = Math.max(40, rightAxisW - 4) + 'px';
|
|
1621
2195
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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);
|
|
1628
2230
|
} else {
|
|
1629
|
-
|
|
2231
|
+
frac = (price - sMin) / (sMax - sMin);
|
|
1630
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';
|
|
2440
|
+
} else {
|
|
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';
|
|
2496
|
+
}
|
|
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';
|
|
1631
2525
|
}
|
|
1632
2526
|
function currentYRange(chart) {
|
|
1633
2527
|
const s = chart.scales.y || {};
|
|
@@ -1654,12 +2548,13 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1654
2548
|
priceBounds = null;
|
|
1655
2549
|
return;
|
|
1656
2550
|
}
|
|
1657
|
-
const range = max - min;
|
|
1658
2551
|
// Uloszoomauksen rajat: alin naytettava hinta voi painua viimeistaan
|
|
1659
|
-
// ~
|
|
1660
|
-
//
|
|
1661
|
-
// ei siis paase "lipsahtamaan" candleita nakyvasta
|
|
1662
|
-
|
|
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 };
|
|
1663
2558
|
}
|
|
1664
2559
|
function applyYRange(chart, min, max) {
|
|
1665
2560
|
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return false;
|
|
@@ -1670,18 +2565,37 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1670
2565
|
if (max <= min) return false;
|
|
1671
2566
|
}
|
|
1672
2567
|
if (priceBounds) {
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
+
}
|
|
1681
2597
|
}
|
|
1682
|
-
if (
|
|
1683
|
-
min = nMin;
|
|
1684
|
-
max = nMax;
|
|
2598
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return false;
|
|
1685
2599
|
}
|
|
1686
2600
|
manualYRange = { min: min, max: max };
|
|
1687
2601
|
chart.setScale('y', { min: min, max: max });
|
|
@@ -1740,6 +2654,56 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1740
2654
|
window.addEventListener('mouseup', endDrag, { once: true });
|
|
1741
2655
|
}, true);
|
|
1742
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
|
+
}
|
|
1743
2707
|
function bindYAxisReset(chart) {
|
|
1744
2708
|
chart.root.addEventListener('dblclick', (e) => {
|
|
1745
2709
|
if (chart !== priceChart) return;
|
|
@@ -1754,6 +2718,12 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1754
2718
|
function setControls() {
|
|
1755
2719
|
setActivePairMode(currentPairMode);
|
|
1756
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();
|
|
1757
2727
|
document.getElementById('sma-toggle').checked = currentSmaEnabled;
|
|
1758
2728
|
document.getElementById('sma-period').value = String(currentSmaPeriod);
|
|
1759
2729
|
document.getElementById('ama-toggle').checked = currentAmaEnabled;
|
|
@@ -1762,6 +2732,11 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1762
2732
|
document.getElementById('ama-slow').value = currentAmaSlowPeriod.toFixed(1);
|
|
1763
2733
|
document.getElementById('vwap-toggle').checked = currentVwapEnabled;
|
|
1764
2734
|
document.getElementById('vwap-bars').value = String(currentVwapBars);
|
|
2735
|
+
document.getElementById('range-toggle').checked = currentRangeEnabled;
|
|
2736
|
+
document.getElementById('range-scale-toggle').checked = currentRangeScaleEnabled;
|
|
2737
|
+
document.getElementById('range-span').value = Number(currentRangeSpan).toFixed(2);
|
|
2738
|
+
document.getElementById('range-span-val').textContent = Number(currentRangeSpan).toFixed(2) + 'x';
|
|
2739
|
+
document.getElementById('range-grid-wrap').style.display = 'inline';
|
|
1765
2740
|
document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
|
|
1766
2741
|
document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
|
|
1767
2742
|
document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
|
|
@@ -1775,7 +2750,8 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1775
2750
|
if (needsRebuild) {
|
|
1776
2751
|
manualYRange = null;
|
|
1777
2752
|
priceMarkerLabel = null;
|
|
1778
|
-
|
|
2753
|
+
resetOverlayNodes();
|
|
2754
|
+
volumeHoverTip = null;
|
|
1779
2755
|
if (priceEl) priceEl.innerHTML = '';
|
|
1780
2756
|
if (volumeEl) volumeEl.innerHTML = '';
|
|
1781
2757
|
priceChart = null;
|
|
@@ -1806,16 +2782,53 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1806
2782
|
charts.forEach((chart) => {
|
|
1807
2783
|
bindWheelZoom(chart);
|
|
1808
2784
|
bindPan(chart);
|
|
2785
|
+
bindXAxisDrag(chart);
|
|
1809
2786
|
if (chart === priceChart) {
|
|
1810
2787
|
bindYAxisDrag(chart);
|
|
1811
2788
|
bindYAxisReset(chart);
|
|
1812
2789
|
chart.root.addEventListener('mousemove', (e) => {
|
|
1813
|
-
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';
|
|
1814
2821
|
});
|
|
1815
2822
|
}
|
|
1816
2823
|
chart.root.addEventListener('mousemove', () => {
|
|
1817
2824
|
if (chart.cursor.idx != null) updateLegend(chart.cursor.idx);
|
|
1818
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
|
+
}
|
|
1819
2832
|
chart.root.addEventListener('mouseleave', refreshLegend);
|
|
1820
2833
|
chart.root.addEventListener('mouseenter', () => chart.root.classList.add('is-hovered'));
|
|
1821
2834
|
chart.root.addEventListener('mouseleave', () => chart.root.classList.remove('is-hovered'));
|
|
@@ -1823,16 +2836,18 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1823
2836
|
chartEventsBound = true;
|
|
1824
2837
|
}
|
|
1825
2838
|
if (oldRange && Number.isFinite(oldRange.min) && Number.isFinite(oldRange.max)) {
|
|
1826
|
-
const next =
|
|
2839
|
+
const next = clampRange(oldRange.min, oldRange.max);
|
|
1827
2840
|
if (next) charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
1828
2841
|
}
|
|
1829
2842
|
refreshLegend();
|
|
2843
|
+
renderMarketPanel();
|
|
2844
|
+
renderRangePanel();
|
|
2845
|
+
renderVolumePanel();
|
|
1830
2846
|
saveState();
|
|
1831
2847
|
}
|
|
1832
2848
|
|
|
1833
2849
|
setControls();
|
|
1834
2850
|
rerender(false);
|
|
1835
|
-
|
|
1836
2851
|
document.querySelectorAll('.time-btn').forEach((btn) => {
|
|
1837
2852
|
btn.addEventListener('click', () => {
|
|
1838
2853
|
if (btn.disabled) return;
|
|
@@ -1843,7 +2858,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1843
2858
|
rerender(true);
|
|
1844
2859
|
});
|
|
1845
2860
|
});
|
|
1846
|
-
|
|
1847
2861
|
const scaleToggle = document.getElementById('scale-toggle');
|
|
1848
2862
|
if (scaleToggle) {
|
|
1849
2863
|
scaleToggle.addEventListener('click', () => {
|
|
@@ -1857,10 +2871,62 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1857
2871
|
pairToggle.addEventListener('click', () => {
|
|
1858
2872
|
currentPairMode = currentPairMode === 'inverse' ? 'normal' : 'inverse';
|
|
1859
2873
|
manualYRange = null;
|
|
2874
|
+
lastOverlayKey = '';
|
|
1860
2875
|
setControls();
|
|
1861
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();
|
|
1862
2893
|
});
|
|
1863
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
|
+
});
|
|
1864
2930
|
|
|
1865
2931
|
const syncInputs = () => {
|
|
1866
2932
|
currentSmaEnabled = document.getElementById('sma-toggle').checked;
|
|
@@ -1876,7 +2942,13 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1876
2942
|
document.getElementById('ama-init-offset-val').textContent = currentAmaInitOffset + '%';
|
|
1877
2943
|
currentVwapEnabled = document.getElementById('vwap-toggle').checked;
|
|
1878
2944
|
currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
|
|
2945
|
+
currentRangeEnabled = document.getElementById('range-toggle').checked;
|
|
2946
|
+
currentRangeScaleEnabled = document.getElementById('range-scale-toggle').checked;
|
|
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;
|
|
1879
2950
|
setControls();
|
|
2951
|
+
markActiveAmaPreset();
|
|
1880
2952
|
rerender(false);
|
|
1881
2953
|
};
|
|
1882
2954
|
|
|
@@ -1914,6 +2986,25 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1914
2986
|
rerender(false);
|
|
1915
2987
|
});
|
|
1916
2988
|
document.getElementById('ama-init-offset-toggle').addEventListener('change', syncInputs);
|
|
2989
|
+
document.getElementById('range-toggle').addEventListener('change', () => {
|
|
2990
|
+
const nextEnabled = document.getElementById('range-toggle').checked;
|
|
2991
|
+
if (currentRangeEnabled === nextEnabled) return;
|
|
2992
|
+
currentRangeEnabled = nextEnabled;
|
|
2993
|
+
setControls();
|
|
2994
|
+
if (!currentRangeEnabled) {
|
|
2995
|
+
hideRangeBandImmediate();
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
rerender(false);
|
|
2999
|
+
});
|
|
3000
|
+
document.getElementById('range-scale-toggle').addEventListener('change', () => {
|
|
3001
|
+
const nextEnabled = document.getElementById('range-scale-toggle').checked;
|
|
3002
|
+
if (currentRangeScaleEnabled === nextEnabled) return;
|
|
3003
|
+
currentRangeScaleEnabled = nextEnabled;
|
|
3004
|
+
manualYRange = null;
|
|
3005
|
+
setControls();
|
|
3006
|
+
rerender(false);
|
|
3007
|
+
});
|
|
1917
3008
|
['sma-period', 'ama-er', 'ama-fast', 'ama-slow', 'vwap-bars'].forEach((id) => {
|
|
1918
3009
|
document.getElementById(id).addEventListener('change', syncInputs);
|
|
1919
3010
|
document.getElementById(id).addEventListener('blur', syncInputs);
|
|
@@ -1957,7 +3048,6 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1957
3048
|
makeStepper('ama-er', 1, 1, 0);
|
|
1958
3049
|
makeStepper('ama-fast', 0.1, 0.1, 1);
|
|
1959
3050
|
makeStepper('ama-slow', 1, 0.1, 1);
|
|
1960
|
-
document.getElementById('ama-reset').addEventListener('click', resetAmaDefaults);
|
|
1961
3051
|
document.getElementById('ama-init-offset').addEventListener('input', () => {
|
|
1962
3052
|
const input = document.getElementById('ama-init-offset');
|
|
1963
3053
|
const snapped = snapInitOffset(input.value);
|
|
@@ -1965,20 +3055,192 @@ function generateHTML(data, title = 'TradingView Style Research') {
|
|
|
1965
3055
|
document.getElementById('ama-init-offset-val').textContent = snapped + '%';
|
|
1966
3056
|
syncInputs();
|
|
1967
3057
|
});
|
|
3058
|
+
document.getElementById('range-span').addEventListener('input', () => {
|
|
3059
|
+
const input = document.getElementById('range-span');
|
|
3060
|
+
document.getElementById('range-span-val').textContent = Number(input.value).toFixed(2) + 'x';
|
|
3061
|
+
syncInputs();
|
|
3062
|
+
});
|
|
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();
|
|
1968
3219
|
|
|
1969
3220
|
window.addEventListener('resize', () => {
|
|
1970
3221
|
if (!charts.length) return;
|
|
1971
3222
|
charts.forEach((chart) => {
|
|
1972
3223
|
const el = chart === priceChart ? priceEl : volumeEl;
|
|
3224
|
+
if (el.clientWidth <= 0 || el.clientHeight <= 0) return;
|
|
1973
3225
|
chart.setSize({ width: el.clientWidth, height: el.clientHeight });
|
|
1974
3226
|
});
|
|
1975
3227
|
});
|
|
1976
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
|
+
|
|
1977
3239
|
${zoomResetScript()}
|
|
1978
3240
|
})();
|
|
1979
3241
|
</script>
|
|
1980
3242
|
</body>
|
|
1981
3243
|
</html>`;
|
|
1982
3244
|
}
|
|
1983
|
-
export { generateHTML, normalizeCandle, loadMarketProfiles };
|
|
3245
|
+
export { generateHTML, normalizeCandle, loadMarketProfiles, resolveChartStorageKey, sanitizeStorageComponent, TRADINGVIEW_PREFS_KEY_PREFIX, TRADINGVIEW_SYNC_KEY };
|
|
1984
3246
|
//# sourceMappingURL=tradingview_uplot_chart_generator.js.map
|