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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { MARKET_ADAPTER } from '../../modules/constants.js';
|
|
5
|
-
import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT } from '../chart_utils.js';
|
|
5
|
+
import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT, embedFunctionSources } from '../chart_utils.js';
|
|
6
6
|
import { cursorCSS, uplotBgCSS } from '../chart_css.js';
|
|
7
7
|
import { zoomResetScript } from '../chart_ui.js';
|
|
8
8
|
import { normalizeCandle } from '../math_utils.js';
|
|
@@ -10,6 +10,14 @@ import { PATHS } from '../../modules/paths.js';
|
|
|
10
10
|
import { getStorage } from '../../modules/storage/index.js';
|
|
11
11
|
const { readJSON } = getStorage();
|
|
12
12
|
import { getErrorMessage } from '../../modules/utils/errors.js';
|
|
13
|
+
import { parseRelativeMultiplier } from '../../modules/order/utils/math.js';
|
|
14
|
+
import { computeAverageAmaSlopePct, computeAmaSlopeClipThreshold } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
15
|
+
import {
|
|
16
|
+
resolveBaseBounds,
|
|
17
|
+
computeAsymmetricBoundsMetrics,
|
|
18
|
+
applyAsymmetricBounds,
|
|
19
|
+
applyNarrowingSideGuard,
|
|
20
|
+
} from '../../market_adapter/core/asymmetric_bounds.js';
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
function inferBaseIntervalSeconds(candles: any[], fallback: any = 3600) {
|
|
@@ -80,6 +88,27 @@ function resolveAmaDefaults({ meta, data, marketProfiles }: any = {}) {
|
|
|
80
88
|
};
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
const TRADINGVIEW_PREFS_KEY_PREFIX = 'dexbot2-tradingview-uplot-v3';
|
|
92
|
+
const TRADINGVIEW_SYNC_KEY = 'dexbot2-tradingview-sync';
|
|
93
|
+
|
|
94
|
+
function sanitizeStorageComponent(value: any, fallback: string) {
|
|
95
|
+
// Objects (e.g. a meta asset node missing both id and symbol) stringify to
|
|
96
|
+
// "[object Object]", which would collapse distinct assets onto one key.
|
|
97
|
+
if (value == null || typeof value === 'object') return fallback;
|
|
98
|
+
const raw = String(value).trim();
|
|
99
|
+
const cleaned = raw.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
100
|
+
return cleaned || fallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Prefer asset ids over symbols: ids are immutable, symbols can be relabeled.
|
|
104
|
+
function resolveChartStorageKey(meta: any = {}, baseIntervalSeconds: any = 0) {
|
|
105
|
+
const pool = sanitizeStorageComponent(meta?.pool ?? meta?.poolId, 'nipool');
|
|
106
|
+
const assetA = sanitizeStorageComponent(meta?.assetA?.id ?? meta?.assetA?.symbol ?? meta?.assetA, 'assetA');
|
|
107
|
+
const assetB = sanitizeStorageComponent(meta?.assetB?.id ?? meta?.assetB?.symbol ?? meta?.assetB, 'assetB');
|
|
108
|
+
const interval = Number(baseIntervalSeconds) > 0 ? Math.round(Number(baseIntervalSeconds)) : 0;
|
|
109
|
+
return `${TRADINGVIEW_PREFS_KEY_PREFIX}:${pool}:${assetA}_${assetB}:${interval || 'base'}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
84
113
|
const rawCandles = Array.isArray(data.candles) ? data.candles : [];
|
|
85
114
|
const candles = rawCandles.map(normalizeCandle).filter(Boolean);
|
|
@@ -95,6 +124,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
95
124
|
{ label: '4h', seconds: 14400 },
|
|
96
125
|
{ label: '1d', seconds: 86400 },
|
|
97
126
|
{ label: '1w', seconds: 604800 },
|
|
127
|
+
{ label: '1M', seconds: 2592000, calendar: 'month' },
|
|
98
128
|
].map((item: any) => ({ ...item, enabled: item.seconds >= baseIntervalSeconds }));
|
|
99
129
|
|
|
100
130
|
const defaultTimeframe = timeframes.find((item: any) => item.label === data.defaultTimeframe && item.enabled)
|
|
@@ -103,6 +133,29 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
103
133
|
|
|
104
134
|
const marketProfiles = data.marketProfiles || loadMarketProfiles();
|
|
105
135
|
const defaultAmaConfig = resolveAmaDefaults({ meta, data, marketProfiles });
|
|
136
|
+
const rangeSlope = {
|
|
137
|
+
lookbackBars: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_LOOKBACK_BARS,
|
|
138
|
+
maxSlopePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_MAX_SLOPE_PCT,
|
|
139
|
+
neutralZonePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_NEUTRAL_ZONE_PCT,
|
|
140
|
+
maxSlopeOffset: MARKET_ADAPTER.DYNAMIC_WEIGHT_ASYMMETRIC_OFFSET_CLAMP,
|
|
141
|
+
maxAsymmetryFactor: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR,
|
|
142
|
+
minScaleSlots: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS,
|
|
143
|
+
clipPercentile: MARKET_ADAPTER.DYNAMIC_WEIGHT_CLIP_PERCENTILE,
|
|
144
|
+
};
|
|
145
|
+
function defaultRangeSpan(input: { rangeSpan?: unknown; grid?: { minPrice?: unknown; maxPrice?: unknown } | null }): number {
|
|
146
|
+
const cli = Number(input.rangeSpan);
|
|
147
|
+
if (Number.isFinite(cli)) return Math.min(2, Math.max(1.2, cli));
|
|
148
|
+
const down = parseRelativeMultiplier(input.grid?.minPrice);
|
|
149
|
+
const up = parseRelativeMultiplier(input.grid?.maxPrice);
|
|
150
|
+
if (down != null && down > 1 && up != null && up > 1) return Math.min(2, Math.max(1.2, (down + up) / 2));
|
|
151
|
+
const vd = Number(input.grid?.minPrice);
|
|
152
|
+
const vu = Number(input.grid?.maxPrice);
|
|
153
|
+
if (Number.isFinite(vd) && Number.isFinite(vu) && vd > 0 && vu > vd) {
|
|
154
|
+
const ref = Math.sqrt(vd * vu);
|
|
155
|
+
return Math.min(2, Math.max(1.2, (ref / vd + vu / ref) / 2));
|
|
156
|
+
}
|
|
157
|
+
return 1.55;
|
|
158
|
+
}
|
|
106
159
|
const defaults = {
|
|
107
160
|
smaPeriod: Math.max(1, Math.round(data.smaPeriod ?? 500)),
|
|
108
161
|
amaDefaults: defaultAmaConfig,
|
|
@@ -111,6 +164,21 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
111
164
|
vwapEnabled: data.vwapEnabled === true,
|
|
112
165
|
vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
|
|
113
166
|
priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
|
|
167
|
+
rangeEnabled: data.rangeEnabled === true,
|
|
168
|
+
rangeScaleEnabled: data.rangeScaleEnabled === true,
|
|
169
|
+
rangeWidthPct: Number.isFinite(Number(data.rangeWidthPct)) && Number(data.rangeWidthPct) > 0
|
|
170
|
+
? Number(data.rangeWidthPct)
|
|
171
|
+
: 2,
|
|
172
|
+
rangeSpan: defaultRangeSpan(data),
|
|
173
|
+
grid: data.grid && data.grid.minPrice != null && data.grid.maxPrice != null ? {
|
|
174
|
+
minPrice: data.grid.minPrice,
|
|
175
|
+
maxPrice: data.grid.maxPrice,
|
|
176
|
+
incrementPercent: Number(data.grid.incrementPercent) > 0 ? Number(data.grid.incrementPercent) : null,
|
|
177
|
+
maxAsymmetryFactor: Number.isFinite(Number(data.grid.maxAsymmetryFactor)) && Number(data.grid.maxAsymmetryFactor) > 0
|
|
178
|
+
? Number(data.grid.maxAsymmetryFactor)
|
|
179
|
+
: rangeSlope.maxAsymmetryFactor,
|
|
180
|
+
} : null,
|
|
181
|
+
rangeSlope,
|
|
114
182
|
};
|
|
115
183
|
|
|
116
184
|
const assetLabelA = meta.assetA?.symbol || meta.assetA?.id || 'Asset A';
|
|
@@ -122,8 +190,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
122
190
|
? `${Math.round(baseIntervalSeconds / 86400)}d`
|
|
123
191
|
: `${Math.round(baseIntervalSeconds / 3600)}h`;
|
|
124
192
|
const defaultPairMode = data.defaultPairMode === 'inverse' ? 'inverse' : 'normal';
|
|
193
|
+
const storageKey = typeof data.storageKey === 'string' && data.storageKey.length > 0
|
|
194
|
+
? data.storageKey
|
|
195
|
+
: resolveChartStorageKey(meta, baseIntervalSeconds);
|
|
125
196
|
|
|
126
197
|
const payload = {
|
|
198
|
+
storageKey,
|
|
199
|
+
syncKey: TRADINGVIEW_SYNC_KEY,
|
|
127
200
|
candles,
|
|
128
201
|
timeframes,
|
|
129
202
|
defaultTimeframe: defaultTimeframe.label,
|
|
@@ -134,6 +207,20 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
134
207
|
vwapEnabled: defaults.vwapEnabled,
|
|
135
208
|
vwapBars: defaults.vwapBars,
|
|
136
209
|
priceScale: defaults.priceScale,
|
|
210
|
+
rangeEnabled: defaults.rangeEnabled,
|
|
211
|
+
rangeScaleEnabled: defaults.rangeScaleEnabled,
|
|
212
|
+
rangeWidthPct: defaults.rangeWidthPct,
|
|
213
|
+
rangeSpan: defaults.rangeSpan,
|
|
214
|
+
grid: defaults.grid,
|
|
215
|
+
orderBuys: Array.isArray((data as any).orders?.buys) ? (data as any).orders.buys.map(Number).filter(Number.isFinite) : [],
|
|
216
|
+
orderSells: Array.isArray((data as any).orders?.sells) ? (data as any).orders.sells.map(Number).filter(Number.isFinite) : [],
|
|
217
|
+
gridBounds: {
|
|
218
|
+
low: Number.isFinite(Number((data as any).gridBounds?.low)) && Number((data as any).gridBounds.low) > 0 ? Number((data as any).gridBounds.low) : null,
|
|
219
|
+
high: Number.isFinite(Number((data as any).gridBounds?.high)) && Number((data as any).gridBounds.high) > 0 ? Number((data as any).gridBounds.high) : null,
|
|
220
|
+
},
|
|
221
|
+
updateMarkerTsSec: Number((data as any).updateMarkerTsSec) > 0 ? Number((data as any).updateMarkerTsSec) : null,
|
|
222
|
+
updateMarkerNewBars: Number((data as any).updateMarkerNewBars) || null,
|
|
223
|
+
rangeSlope,
|
|
137
224
|
defaultPairMode,
|
|
138
225
|
assetLabelA,
|
|
139
226
|
assetLabelB,
|
|
@@ -142,6 +229,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
142
229
|
poolLabel,
|
|
143
230
|
intervalLabel,
|
|
144
231
|
amaDefaultsSource: marketProfiles ? 'market_profiles' : 'constants',
|
|
232
|
+
volumeIsCount: !!((meta as any)?.feed),
|
|
145
233
|
};
|
|
146
234
|
|
|
147
235
|
return `<!doctype html>
|
|
@@ -177,9 +265,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
177
265
|
#brand { display: flex; flex-direction: column; gap: 4px; min-width: 240px; }
|
|
178
266
|
#title { font-size: 15px; font-weight: 700; color: #f5f8fb; }
|
|
179
267
|
#subtitle { font-size: 11px; color: #b9c6d4; text-transform: uppercase; letter-spacing: 0.6px; }
|
|
180
|
-
#toolbar { display: flex; flex-
|
|
268
|
+
#toolbar { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
|
|
269
|
+
.toolbar-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
|
|
181
270
|
.group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
|
|
182
271
|
.group:first-child { padding-left: 0; border-left: 0; }
|
|
272
|
+
.primary-toolbar-row .group { padding-left: 0; border-left: 0; }
|
|
183
273
|
:root {
|
|
184
274
|
--control-height: 36px;
|
|
185
275
|
}
|
|
@@ -202,6 +292,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
202
292
|
background: linear-gradient(180deg, rgba(35,53,79,0.95), rgba(22,32,47,0.98));
|
|
203
293
|
}
|
|
204
294
|
.time-btn:disabled { opacity: 0.35; cursor: not-allowed; text-decoration: line-through; }
|
|
295
|
+
.ama-preset-btn.active {
|
|
296
|
+
color: #fff;
|
|
297
|
+
border-color: rgba(250,204,21,0.85);
|
|
298
|
+
background: linear-gradient(180deg, rgba(79,66,22,0.95), rgba(47,40,18,0.98));
|
|
299
|
+
}
|
|
205
300
|
.indicator {
|
|
206
301
|
display: inline-flex;
|
|
207
302
|
align-items: center;
|
|
@@ -387,11 +482,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
387
482
|
background: rgba(10,14,20,0.72);
|
|
388
483
|
backdrop-filter: blur(10px);
|
|
389
484
|
}
|
|
390
|
-
.legend-line { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size:
|
|
485
|
+
.legend-line { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size: 12px; }
|
|
391
486
|
.legend-line + .legend-line { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.06); }
|
|
392
487
|
.legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
393
488
|
.legend-dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
|
|
394
|
-
.legend-label { color: #8290a2; text-transform: uppercase; letter-spacing: 0.45px; font-size:
|
|
489
|
+
.legend-label { color: #8290a2; text-transform: uppercase; letter-spacing: 0.45px; font-size: 12px; }
|
|
395
490
|
.legend-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #eef4fb; font-weight: 700; }
|
|
396
491
|
.status { display: inline-flex; align-items: center; gap: 8px; font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
397
492
|
.pill { border: 1px solid rgba(255,255,255,0.06); border-radius: 999px; padding: 4px 8px; background: rgba(255,255,255,0.03); }
|
|
@@ -399,7 +494,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
399
494
|
${cursorCSS()}
|
|
400
495
|
@media (max-width: 980px) {
|
|
401
496
|
#topbar { flex-direction: column; align-items: flex-start; }
|
|
402
|
-
#toolbar {
|
|
497
|
+
#toolbar { align-items: flex-start; }
|
|
498
|
+
.toolbar-row { justify-content: flex-start; }
|
|
403
499
|
.group { padding-left: 0; border-left: 0; flex-wrap: wrap; }
|
|
404
500
|
#legend { min-width: 0; max-width: calc(100vw - 24px); }
|
|
405
501
|
}
|
|
@@ -413,9 +509,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
413
509
|
<div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
|
|
414
510
|
</div>
|
|
415
511
|
<div id="toolbar">
|
|
416
|
-
<div class="
|
|
417
|
-
${timeframes.map((item: any) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
|
|
418
|
-
</div>
|
|
512
|
+
<div class="toolbar-row primary-toolbar-row">
|
|
419
513
|
<div class="group">
|
|
420
514
|
<div class="indicator">
|
|
421
515
|
<span class="tag">pair</span>
|
|
@@ -429,9 +523,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
429
523
|
<span class="tag">scale</span>
|
|
430
524
|
<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>
|
|
431
525
|
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="group" id="tf-group">
|
|
528
|
+
${timeframes.map((item: any) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
|
|
529
|
+
</div>
|
|
530
|
+
<div class="group">
|
|
432
531
|
<div class="indicator">
|
|
433
532
|
<label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
|
|
434
|
-
<span class="tag">period</span>
|
|
435
533
|
<input type="text" inputmode="numeric" id="sma-period" value="${defaults.smaPeriod}">
|
|
436
534
|
<span class="step-stack">
|
|
437
535
|
<button type="button" class="step-btn" id="sma-period-inc">▲</button>
|
|
@@ -440,13 +538,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
440
538
|
</div>
|
|
441
539
|
<div class="indicator">
|
|
442
540
|
<label><input type="checkbox" id="vwap-toggle"${defaults.vwapEnabled ? ' checked' : ''}> VWMA</label>
|
|
443
|
-
<span class="tag">bars</span>
|
|
444
541
|
<input type="text" inputmode="numeric" id="vwap-bars" value="${defaults.vwapBars}">
|
|
445
542
|
<span class="step-stack">
|
|
446
543
|
<button type="button" class="step-btn" id="vwap-bars-inc">▲</button>
|
|
447
544
|
<button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
|
|
448
545
|
</span>
|
|
449
546
|
</div>
|
|
547
|
+
<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>
|
|
548
|
+
${(((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>' : '')}
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="toolbar-row">
|
|
552
|
+
<div class="group">
|
|
450
553
|
<div class="indicator">
|
|
451
554
|
<label><input type="checkbox" id="ama-toggle"${defaults.amaEnabled ? ' checked' : ''}> AMA</label>
|
|
452
555
|
<span class="tag">er</span>
|
|
@@ -467,15 +570,26 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
467
570
|
<button type="button" class="step-btn" id="ama-slow-inc">▲</button>
|
|
468
571
|
<button type="button" class="step-btn" id="ama-slow-dec">▼</button>
|
|
469
572
|
</span>
|
|
470
|
-
<button type="button" class="reset-btn"
|
|
573
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA1" title="AMA1 (slow 62.1)">1</button>
|
|
574
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA2" title="AMA2 (slow 72.0)">2</button>
|
|
575
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA3" title="AMA3 (slow 83.6)">3</button>
|
|
576
|
+
<button type="button" class="reset-btn ama-preset-btn" data-ama-preset="AMA4" title="AMA4 (slow 96.9)">4</button>
|
|
471
577
|
</div>
|
|
472
|
-
<div class="indicator"
|
|
578
|
+
<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">
|
|
579
|
+
<label><input type="checkbox" id="range-toggle"${defaults.rangeEnabled ? ' checked' : ''}> Range</label>
|
|
580
|
+
<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>
|
|
581
|
+
<span id="range-grid-wrap" style="display:inline" title="x-range around AMA (1.2x–2.0x)">
|
|
582
|
+
<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">
|
|
583
|
+
<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>
|
|
584
|
+
</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div class="indicator">
|
|
473
587
|
<label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
|
|
474
|
-
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:
|
|
588
|
+
<input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:90px;vertical-align:middle" disabled>
|
|
475
589
|
<span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
|
|
476
590
|
</div>
|
|
477
591
|
</div>
|
|
478
|
-
|
|
592
|
+
</div>
|
|
479
593
|
</div>
|
|
480
594
|
</div>
|
|
481
595
|
<div id="chart-shell">
|
|
@@ -484,20 +598,16 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
484
598
|
<span class="legend-item"><span class="legend-label">Time</span> <span class="legend-value" id="legend-time">-</span></span>
|
|
485
599
|
<span class="legend-item"><span class="legend-label">C</span> <span class="legend-value" id="legend-close">-</span></span>
|
|
486
600
|
<span class="legend-item"><span class="legend-label">Delta</span> <span class="legend-value" id="legend-delta">-</span></span>
|
|
487
|
-
<span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume">-</span></span>
|
|
488
|
-
<span class="legend-item"><span class="legend-label">Scale</span> <span class="legend-value" id="legend-scale">-</span></span>
|
|
601
|
+
<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>
|
|
489
602
|
</div>
|
|
490
603
|
<div class="legend-line">
|
|
491
604
|
<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>
|
|
492
|
-
<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>
|
|
493
|
-
<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>
|
|
494
|
-
<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>
|
|
495
|
-
<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>
|
|
496
|
-
|
|
497
605
|
<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>
|
|
606
|
+
<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>
|
|
607
|
+
<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>
|
|
498
608
|
</div>
|
|
499
609
|
</div>
|
|
500
|
-
<div id="price-chart" title="Wheel: zoom time (
|
|
610
|
+
<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>
|
|
501
611
|
<div id="volume-chart"></div>
|
|
502
612
|
</div>
|
|
503
613
|
</div>
|
|
@@ -512,7 +622,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
512
622
|
const baseCloseValues = baseCandles.map((c) => c.close);
|
|
513
623
|
const timeframes = Array.isArray(payload.timeframes) ? payload.timeframes.slice() : [];
|
|
514
624
|
const timeframeMap = new Map(timeframes.map((item) => [item.label, item]));
|
|
515
|
-
const STORAGE_KEY = '
|
|
625
|
+
const STORAGE_KEY = typeof payload.storageKey === 'string' && payload.storageKey.length > 0
|
|
626
|
+
? payload.storageKey
|
|
627
|
+
: 'dexbot2-tradingview-uplot-v3:default';
|
|
628
|
+
const SYNC_KEY = typeof payload.syncKey === 'string' && payload.syncKey.length > 0
|
|
629
|
+
? payload.syncKey
|
|
630
|
+
: 'dexbot2-tradingview-sync';
|
|
516
631
|
|
|
517
632
|
const state = loadState();
|
|
518
633
|
let currentTimeframe = state.timeframe || payload.defaultTimeframe || '1h';
|
|
@@ -530,6 +645,25 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
530
645
|
: Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod});
|
|
531
646
|
let currentVwapEnabled = state.vwapEnabled ?? !!payload.vwapEnabled;
|
|
532
647
|
let currentVwapBars = Number.isFinite(state.vwapBars) ? state.vwapBars : Number(payload.vwapBars || 500);
|
|
648
|
+
let currentRangeEnabled = state.rangeEnabled ?? !!payload.rangeEnabled;
|
|
649
|
+
let currentRangeScaleEnabled = state.rangeScaleEnabled ?? !!payload.rangeScaleEnabled;
|
|
650
|
+
let currentRangeWidthPct = Number.isFinite(state.rangeWidthPct) && Number(state.rangeWidthPct) > 0
|
|
651
|
+
? Number(state.rangeWidthPct)
|
|
652
|
+
: (Number.isFinite(Number(payload.rangeWidthPct)) && Number(payload.rangeWidthPct) > 0 ? Number(payload.rangeWidthPct) : 2);
|
|
653
|
+
let currentRangeSpan = Number.isFinite(state.rangeSpan) && Number(state.rangeSpan) > 0
|
|
654
|
+
? Math.min(2, Math.max(1.2, Number(state.rangeSpan)))
|
|
655
|
+
: (Number.isFinite(Number(payload.rangeSpan)) && Number(payload.rangeSpan) > 0 ? Math.min(2, Math.max(1.2, Number(payload.rangeSpan))) : 1.55);
|
|
656
|
+
let currentOrdersVisible = state.ordersVisible ?? true;
|
|
657
|
+
let currentVolumeVisible = state.volumeVisible ?? true;
|
|
658
|
+
let currentVolumeMode = state.volumeMode === 'quote' ? 'quote' : 'base';
|
|
659
|
+
// Static per generation: the overlay (levels, reserve/ceiling lines,
|
|
660
|
+
// spread label) exists only when order data was embedded.
|
|
661
|
+
// --no-orders or a missing orders file therefore removes the whole
|
|
662
|
+
// overlay, including the reserve/ceiling lines.
|
|
663
|
+
const hasOrders = ((payload.orderBuys?.length || 0) + (payload.orderSells?.length || 0)) > 0;
|
|
664
|
+
// Bounds-only grids (virtual slots, no live levels yet) still show
|
|
665
|
+
// the reserve/ceiling lines; levels/spread/panel rows need live data.
|
|
666
|
+
const hasBounds = payload.gridBounds?.low != null || payload.gridBounds?.high != null;
|
|
533
667
|
let currentAmaInitOffsetEnabled = false;
|
|
534
668
|
let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
|
|
535
669
|
let currentPriceScale = state.priceScale || payload.priceScale || 'log';
|
|
@@ -551,6 +685,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
551
685
|
let currentSmaInit = [];
|
|
552
686
|
let currentSmaInitOff = [];
|
|
553
687
|
let currentAmaOff = [];
|
|
688
|
+
let currentRangeUpper = [];
|
|
689
|
+
let currentRangeLower = [];
|
|
554
690
|
let priceChart = null;
|
|
555
691
|
let volumeChart = null;
|
|
556
692
|
let lastRenderedPriceScale = null;
|
|
@@ -560,6 +696,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
560
696
|
let lastCandleKey = null;
|
|
561
697
|
let pendingRange = null;
|
|
562
698
|
let pendingRangeRaf = 0;
|
|
699
|
+
let rangePanelRaf = 0;
|
|
700
|
+
let yRefitRaf = 0;
|
|
563
701
|
let xMin = 0;
|
|
564
702
|
let xMax = 0;
|
|
565
703
|
let smaWorker = null;
|
|
@@ -578,27 +716,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
578
716
|
// TradingView-exporter interaction overrides (PR #3 / #11 feature):
|
|
579
717
|
// redeclare after UPLOT_SHARED_SCRIPT so these hoisted declarations win
|
|
580
718
|
// for this page only, without mutating the shared stack used by other charts.
|
|
581
|
-
function clampXRange(min, max) {
|
|
582
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return { min: xMin, max: xMax };
|
|
583
|
-
// Salli tyhjaa tilaa datan molemmin puolin (TradingView-tyyli):
|
|
584
|
-
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
585
|
-
const dataSpan = Math.max(1, xMax - xMin);
|
|
586
|
-
const maxPad = dataSpan * 0.75;
|
|
587
|
-
let lo = Math.max(xMin - maxPad, min);
|
|
588
|
-
let hi = Math.min(xMax + maxPad, max);
|
|
589
|
-
if (lo < xMin && hi <= lo) lo = xMin;
|
|
590
|
-
if (hi > xMax + maxPad) hi = xMax + maxPad;
|
|
591
|
-
if (hi <= lo) return { min: xMin, max: xMax };
|
|
592
|
-
return { min: lo, max: hi };
|
|
593
|
-
}
|
|
594
719
|
function bindWheelZoom(chart) {
|
|
595
720
|
chart.root.addEventListener('wheel', (e) => {
|
|
596
721
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
597
722
|
e.preventDefault();
|
|
598
723
|
const rect = chart.root.getBoundingClientRect();
|
|
599
|
-
|
|
724
|
+
// Shift+wheel scales price (cursor-anchored) anywhere over the
|
|
725
|
+
// price pane — same as wheeling over the price axis. Volume has
|
|
726
|
+
// no manual Y lock, so it keeps the timeframe zoom.
|
|
727
|
+
if (chart === priceChart && (e.shiftKey || inYAxisZone(chart, e.clientX))) {
|
|
600
728
|
const centerY = chart.posToVal(e.clientY - rect.top, 'y');
|
|
601
|
-
|
|
729
|
+
const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
730
|
+
zoomYAt(chart, centerY, delta < 0 ? 0.91 : 1.10);
|
|
602
731
|
return;
|
|
603
732
|
}
|
|
604
733
|
e.stopPropagation();
|
|
@@ -617,12 +746,132 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
617
746
|
syncXRange(center - nextSpan * ratio, center - nextSpan * ratio + nextSpan);
|
|
618
747
|
}, { passive: false });
|
|
619
748
|
}
|
|
749
|
+
function clampRange(min, max) {
|
|
750
|
+
if (!Number.isFinite(xMin) || !Number.isFinite(xMax) || xMax <= xMin) return null;
|
|
751
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
|
|
752
|
+
// Allow empty space on both sides of the data (TradingView-style):
|
|
753
|
+
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
754
|
+
// Span-locked: pans slide the window and never squash it — only a
|
|
755
|
+
// deliberate over-wide zoom-out is centered down to the limit.
|
|
756
|
+
// Plot drags therefore move but never scale; scaling lives on
|
|
757
|
+
// the axis gutters alone.
|
|
758
|
+
const dataSpan = Math.max(1, xMax - xMin);
|
|
759
|
+
const maxPad = dataSpan * 0.75;
|
|
760
|
+
const lo = xMin - maxPad;
|
|
761
|
+
const hi = xMax + maxPad;
|
|
762
|
+
const span = max - min;
|
|
763
|
+
if (span > hi - lo) {
|
|
764
|
+
const center = (min + max) / 2;
|
|
765
|
+
const half = (hi - lo) / 2;
|
|
766
|
+
return { min: center - half, max: center + half };
|
|
767
|
+
}
|
|
768
|
+
let nMin = min;
|
|
769
|
+
if (nMin < lo) nMin = lo;
|
|
770
|
+
if (nMin + span > hi) nMin = hi - span;
|
|
771
|
+
return { min: nMin, max: nMin + span };
|
|
772
|
+
}
|
|
773
|
+
function syncXRange(min, max) {
|
|
774
|
+
pendingRange = clampRange(min, max);
|
|
775
|
+
if (pendingRangeRaf) return;
|
|
776
|
+
pendingRangeRaf = requestAnimationFrame(() => {
|
|
777
|
+
const next = pendingRange;
|
|
778
|
+
pendingRange = null;
|
|
779
|
+
pendingRangeRaf = 0;
|
|
780
|
+
if (!next || charts.length === 0) return;
|
|
781
|
+
charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function bindPan(chart) {
|
|
785
|
+
let dragging = false;
|
|
786
|
+
let startClientX = 0;
|
|
787
|
+
let startClientY = 0;
|
|
788
|
+
let startMin = 0;
|
|
789
|
+
let startMax = 0;
|
|
790
|
+
let startYRange = null;
|
|
791
|
+
let panRaf = 0;
|
|
792
|
+
let pendingPan = null;
|
|
793
|
+
const applyPendingPan = () => {
|
|
794
|
+
panRaf = 0;
|
|
795
|
+
const p = pendingPan;
|
|
796
|
+
pendingPan = null;
|
|
797
|
+
if (!dragging || !p) return;
|
|
798
|
+
// Plot drag pans time and price (TradingView-style): the price
|
|
799
|
+
// move sets a manual Y range; double-click the price axis to
|
|
800
|
+
// return to autofit. Axis gutters never pan — they scale.
|
|
801
|
+
if (chart === priceChart && startYRange && Number.isFinite(p.clientY)) {
|
|
802
|
+
const vStart = chart.posToVal(p.startY - p.rectTop, 'y');
|
|
803
|
+
const vCur = chart.posToVal(p.clientY - p.rectTop, 'y');
|
|
804
|
+
if (Number.isFinite(vStart) && Number.isFinite(vCur) && vCur !== vStart) {
|
|
805
|
+
// Rigid move only: the span is locked by construction so a
|
|
806
|
+
// plot drag can never rescale — Y scaling happens solely
|
|
807
|
+
// on the price-axis gutter (wheel/drag there).
|
|
808
|
+
if (currentPriceScale === 'log') {
|
|
809
|
+
const ratio = Math.max(1e-12, vStart) / Math.max(1e-12, vCur);
|
|
810
|
+
const spanRatio = startYRange.max / startYRange.min;
|
|
811
|
+
if (Number.isFinite(ratio) && ratio > 0 && Number.isFinite(spanRatio) && spanRatio > 0) {
|
|
812
|
+
const nextMin = startYRange.min * ratio;
|
|
813
|
+
applyYRange(chart, nextMin, nextMin * spanRatio);
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
const dy = vStart - vCur;
|
|
817
|
+
const span = startYRange.max - startYRange.min;
|
|
818
|
+
if (Number.isFinite(dy) && Number.isFinite(span) && span > 0) {
|
|
819
|
+
const nextMin = startYRange.min + dy;
|
|
820
|
+
applyYRange(chart, nextMin, nextMin + span);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const delta = chart.posToVal(p.clientX - p.rectLeft, 'x') - chart.posToVal(p.startX - p.rectLeft, 'x');
|
|
826
|
+
syncXRange(startMin - delta, startMax - delta);
|
|
827
|
+
};
|
|
828
|
+
const onMove = (e) => {
|
|
829
|
+
if (!dragging) return;
|
|
830
|
+
e.preventDefault();
|
|
831
|
+
const rect = chart.root.getBoundingClientRect();
|
|
832
|
+
pendingPan = {
|
|
833
|
+
clientX: e.clientX, clientY: e.clientY,
|
|
834
|
+
startX: startClientX, startY: startClientY,
|
|
835
|
+
rectLeft: rect.left, rectTop: rect.top,
|
|
836
|
+
};
|
|
837
|
+
if (!panRaf) panRaf = requestAnimationFrame(applyPendingPan);
|
|
838
|
+
};
|
|
839
|
+
const endDrag = () => {
|
|
840
|
+
if (!dragging) return;
|
|
841
|
+
dragging = false;
|
|
842
|
+
startYRange = null;
|
|
843
|
+
pendingPan = null;
|
|
844
|
+
if (panRaf) { cancelAnimationFrame(panRaf); panRaf = 0; }
|
|
845
|
+
document.body.style.cursor = '';
|
|
846
|
+
window.removeEventListener('mousemove', onMove);
|
|
847
|
+
window.removeEventListener('mouseup', endDrag);
|
|
848
|
+
};
|
|
849
|
+
chart.root.addEventListener('mousedown', (e) => {
|
|
850
|
+
if (!e || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
851
|
+
const rect = chart.root.getBoundingClientRect();
|
|
852
|
+
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
|
|
853
|
+
if (chart === priceChart && inYAxisZone(chart, e.clientX)) return;
|
|
854
|
+
if (inXAxisZone(chart, e.clientY)) return;
|
|
855
|
+
e.preventDefault();
|
|
856
|
+
e.stopPropagation();
|
|
857
|
+
dragging = true;
|
|
858
|
+
startClientX = e.clientX;
|
|
859
|
+
startClientY = e.clientY;
|
|
860
|
+
const s = chart.scales.x || {};
|
|
861
|
+
startMin = Number.isFinite(s.min) ? s.min : currentCandles[0].time;
|
|
862
|
+
startMax = Number.isFinite(s.max) ? s.max : currentCandles[currentCandles.length - 1].time;
|
|
863
|
+
if (chart === priceChart) startYRange = currentYRange(chart);
|
|
864
|
+
document.body.style.cursor = 'grabbing';
|
|
865
|
+
window.addEventListener('mousemove', onMove);
|
|
866
|
+
window.addEventListener('mouseup', endDrag, { once: true });
|
|
867
|
+
});
|
|
868
|
+
}
|
|
620
869
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
621
|
-
// Significant-digit price formatting (
|
|
622
|
-
// DEXBot order-price style (formatCurrency)
|
|
870
|
+
// Significant-digit price formatting (4 "active" digits), matching the
|
|
871
|
+
// DEXBot order-price style (formatCurrency).
|
|
623
872
|
// Very small values get SI micro-suffixes (m/µ/n/p/...); large values
|
|
624
|
-
// get comma grouping. Examples: 34.
|
|
625
|
-
// 3014.9 -> "3,
|
|
873
|
+
// get comma grouping. Examples: 34.94 -> "34.94", 119.98 -> "120.0",
|
|
874
|
+
// 3014.9 -> "3,015", 0.00567 -> "5.670m".
|
|
626
875
|
function fmtPrice(v) {
|
|
627
876
|
if (v == null || !Number.isFinite(v)) return '-';
|
|
628
877
|
const num = Number(v);
|
|
@@ -726,6 +975,9 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
726
975
|
ticks: { stroke: '#30363d', width: 1 },
|
|
727
976
|
font: '11px Segoe UI, sans-serif',
|
|
728
977
|
values: showLabels ? (u, vals) => {
|
|
978
|
+
if (String(currentTimeframe) === '1M') {
|
|
979
|
+
return vals.map((ts) => new Date(ts * 1000).toLocaleString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' }));
|
|
980
|
+
}
|
|
729
981
|
const xScale = u.scales.x || {};
|
|
730
982
|
const spanSec = Number.isFinite(xScale.min) && Number.isFinite(xScale.max)
|
|
731
983
|
? Math.max(0, xScale.max - xScale.min)
|
|
@@ -749,6 +1001,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
749
1001
|
amaSlowPeriod: currentAmaSlowPeriod,
|
|
750
1002
|
vwapEnabled: currentVwapEnabled,
|
|
751
1003
|
vwapBars: currentVwapBars,
|
|
1004
|
+
rangeEnabled: currentRangeEnabled,
|
|
1005
|
+
rangeScaleEnabled: currentRangeScaleEnabled,
|
|
1006
|
+
rangeWidthPct: currentRangeWidthPct,
|
|
1007
|
+
rangeSpan: currentRangeSpan,
|
|
1008
|
+
ordersVisible: currentOrdersVisible,
|
|
1009
|
+
volumeVisible: currentVolumeVisible,
|
|
1010
|
+
volumeMode: currentVolumeMode,
|
|
752
1011
|
amaInitOffset: currentAmaInitOffset,
|
|
753
1012
|
priceScale: currentPriceScale,
|
|
754
1013
|
pairMode: currentPairMode,
|
|
@@ -763,6 +1022,75 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
763
1022
|
? (payload.pairLabelInverse || (payload.assetLabelB + '/' + payload.assetLabelA))
|
|
764
1023
|
: (payload.pairLabelNormal || (payload.assetLabelA + '/' + payload.assetLabelB));
|
|
765
1024
|
}
|
|
1025
|
+
// Volume units: base = displayed pair's base asset (TradingView
|
|
1026
|
+
// standard, VWMA-correct), quote = base * display close (per-candle
|
|
1027
|
+
// approximation of true quote volume). Persisted per chart in
|
|
1028
|
+
// localStorage like the AMA settings (volumeMode).
|
|
1029
|
+
function getVolumeSymbols() {
|
|
1030
|
+
const inverse = normalizePairMode(currentPairMode) === 'inverse';
|
|
1031
|
+
return {
|
|
1032
|
+
base: inverse ? payload.assetLabelB : payload.assetLabelA,
|
|
1033
|
+
quote: inverse ? payload.assetLabelA : payload.assetLabelB,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function getVolumeUnit() {
|
|
1037
|
+
if (payload.volumeIsCount) return { mode: 'count', symbol: 'feeds' };
|
|
1038
|
+
const syms = getVolumeSymbols();
|
|
1039
|
+
return currentVolumeMode === 'quote'
|
|
1040
|
+
? { mode: 'quote', symbol: syms.quote }
|
|
1041
|
+
: { mode: 'base', symbol: syms.base };
|
|
1042
|
+
}
|
|
1043
|
+
function getDisplayVolume(c) {
|
|
1044
|
+
const v = Number(c?.volume);
|
|
1045
|
+
if (!Number.isFinite(v)) return NaN;
|
|
1046
|
+
if (payload.volumeIsCount) return v;
|
|
1047
|
+
if (currentVolumeMode === 'quote') {
|
|
1048
|
+
const p = Number(c?.close);
|
|
1049
|
+
return Number.isFinite(p) && p > 0 ? v * p : NaN;
|
|
1050
|
+
}
|
|
1051
|
+
return v;
|
|
1052
|
+
}
|
|
1053
|
+
function fmtVolumeWithSym(v) {
|
|
1054
|
+
const s = fmtVolume(v);
|
|
1055
|
+
if (s == null || s === '-') return '-';
|
|
1056
|
+
const u = getVolumeUnit();
|
|
1057
|
+
return u.symbol ? s + ' ' + u.symbol : s;
|
|
1058
|
+
}
|
|
1059
|
+
function setActiveVolumeUnit() {
|
|
1060
|
+
const btn = document.getElementById('volume-unit-toggle');
|
|
1061
|
+
if (!btn) return;
|
|
1062
|
+
const u = getVolumeUnit();
|
|
1063
|
+
btn.dataset.volumeMode = u.mode;
|
|
1064
|
+
btn.textContent = u.symbol || u.mode;
|
|
1065
|
+
btn.title = payload.volumeIsCount
|
|
1066
|
+
? 'Volume: feed publish count per bucket'
|
|
1067
|
+
: (u.mode === 'quote'
|
|
1068
|
+
? 'Volume units: quote asset (' + u.symbol + ') — click for base asset'
|
|
1069
|
+
: 'Volume units: base asset (' + u.symbol + ') — click for quote asset');
|
|
1070
|
+
btn.disabled = !!payload.volumeIsCount;
|
|
1071
|
+
btn.style.opacity = payload.volumeIsCount ? '0.45' : '';
|
|
1072
|
+
btn.style.cursor = payload.volumeIsCount ? 'not-allowed' : 'pointer';
|
|
1073
|
+
const lv = document.getElementById('legend-volume');
|
|
1074
|
+
if (lv) {
|
|
1075
|
+
lv.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
1076
|
+
lv.title = payload.volumeIsCount
|
|
1077
|
+
? 'Volume: feed publish count per bucket'
|
|
1078
|
+
: 'Volume units — click to switch base/quote';
|
|
1079
|
+
}
|
|
1080
|
+
const vp = document.getElementById('vol-panel');
|
|
1081
|
+
if (vp) {
|
|
1082
|
+
vp.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
1083
|
+
vp.title = payload.volumeIsCount
|
|
1084
|
+
? 'Volume: feed publish count per bucket'
|
|
1085
|
+
: 'Volume units — click to switch base/quote';
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function toggleVolumeUnit() {
|
|
1089
|
+
if (payload.volumeIsCount) return;
|
|
1090
|
+
currentVolumeMode = currentVolumeMode === 'quote' ? 'base' : 'quote';
|
|
1091
|
+
setActiveVolumeUnit();
|
|
1092
|
+
rerender(true);
|
|
1093
|
+
}
|
|
766
1094
|
function refreshSubtitle() {
|
|
767
1095
|
const subtitleEl = document.getElementById('subtitle');
|
|
768
1096
|
if (!subtitleEl) return;
|
|
@@ -811,38 +1139,65 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
811
1139
|
seriesCache.set(key, state);
|
|
812
1140
|
return state;
|
|
813
1141
|
}
|
|
814
|
-
function aggregateCandles(rows, seconds) {
|
|
1142
|
+
function aggregateCandles(rows, seconds, label) {
|
|
1143
|
+
const monthly = String(label || '') === '1M';
|
|
815
1144
|
const bucketSec = Math.max(1, Math.round(seconds || 3600));
|
|
816
|
-
const cacheKey = currentPairMode + '|' + bucketSec;
|
|
1145
|
+
const cacheKey = currentPairMode + '|' + (monthly ? '1M' : String(bucketSec));
|
|
817
1146
|
if (aggregateCache.has(cacheKey)) return aggregateCache.get(cacheKey);
|
|
818
1147
|
const out = [];
|
|
819
1148
|
const idxs = [];
|
|
820
1149
|
let cur = null;
|
|
821
1150
|
let curBucket = null;
|
|
822
1151
|
let curIdx = -1;
|
|
1152
|
+
const finishBucket = (bucket) => {
|
|
1153
|
+
if (!bucket) return;
|
|
1154
|
+
// Feed volume is a publication rate, not trade quantity. Keep
|
|
1155
|
+
// it comparable across 1h/week/month views instead of turning
|
|
1156
|
+
// a monthly candle into a sum such as 388 publishes.
|
|
1157
|
+
if (payload.volumeIsCount && bucket.barCount > 0) {
|
|
1158
|
+
bucket.volume /= bucket.barCount;
|
|
1159
|
+
}
|
|
1160
|
+
delete bucket.barCount;
|
|
1161
|
+
out.push(bucket);
|
|
1162
|
+
};
|
|
823
1163
|
for (let i = 0; i < rows.length; i++) {
|
|
824
1164
|
const row = rows[i];
|
|
825
1165
|
const ts = Number(row.time);
|
|
826
1166
|
if (!Number.isFinite(ts)) continue;
|
|
827
|
-
|
|
1167
|
+
let bucket;
|
|
1168
|
+
if (monthly) {
|
|
1169
|
+
const d = new Date(ts * 1000);
|
|
1170
|
+
bucket = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) / 1000;
|
|
1171
|
+
} else {
|
|
1172
|
+
bucket = Math.floor(ts / bucketSec) * bucketSec;
|
|
1173
|
+
}
|
|
828
1174
|
if (!cur || bucket !== curBucket) {
|
|
829
1175
|
if (cur) {
|
|
830
|
-
|
|
1176
|
+
finishBucket(cur);
|
|
831
1177
|
idxs.push(curIdx);
|
|
832
1178
|
}
|
|
833
1179
|
curBucket = bucket;
|
|
834
1180
|
curIdx = i;
|
|
835
|
-
cur = {
|
|
1181
|
+
cur = {
|
|
1182
|
+
time: bucket,
|
|
1183
|
+
open: row.open,
|
|
1184
|
+
high: row.high,
|
|
1185
|
+
low: row.low,
|
|
1186
|
+
close: row.close,
|
|
1187
|
+
volume: row.volume || 0,
|
|
1188
|
+
barCount: 1,
|
|
1189
|
+
};
|
|
836
1190
|
} else {
|
|
837
1191
|
cur.high = Math.max(cur.high, row.high);
|
|
838
1192
|
cur.low = Math.min(cur.low, row.low);
|
|
839
1193
|
cur.close = row.close;
|
|
840
1194
|
cur.volume += row.volume || 0;
|
|
1195
|
+
cur.barCount += 1;
|
|
841
1196
|
curIdx = i;
|
|
842
1197
|
}
|
|
843
1198
|
}
|
|
844
1199
|
if (cur) {
|
|
845
|
-
|
|
1200
|
+
finishBucket(cur);
|
|
846
1201
|
idxs.push(curIdx);
|
|
847
1202
|
}
|
|
848
1203
|
const result = { candles: out, idxs };
|
|
@@ -1078,6 +1433,90 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1078
1433
|
vwapCache.set(cacheKey, out);
|
|
1079
1434
|
return out;
|
|
1080
1435
|
}
|
|
1436
|
+
// Bot-grid range band: min/max derive ONLY from the live AMA price.
|
|
1437
|
+
// All math below runs the canonical market_adapter/grid sources,
|
|
1438
|
+
// embedded verbatim (not hand-copied): computeAverageAmaSlopePct,
|
|
1439
|
+
// computeAmaSlopeClipThreshold, applyAsymmetricBounds (+ metrics/base),
|
|
1440
|
+
// applyNarrowingSideGuard, parseRelativeMultiplier. With Range Scaling
|
|
1441
|
+
// on, the clipped AMA slope tilts the span-symmetric band like the
|
|
1442
|
+
// grid build: trend side widens, opposite side tightens. Painted
|
|
1443
|
+
// red [AMA, upper], green [lower, AMA].
|
|
1444
|
+
// Never reads candles, pair-display mapping, or axis/zoom state —
|
|
1445
|
+
// inversion and timeframe sampling apply to the AMA first.
|
|
1446
|
+
${embedFunctionSources([computeAverageAmaSlopePct, computeAmaSlopeClipThreshold, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1447
|
+
function computeRangeBand(baseAma) {
|
|
1448
|
+
const n = Array.isArray(baseAma) ? baseAma.length : 0;
|
|
1449
|
+
const upper = new Array(n).fill(null);
|
|
1450
|
+
const lower = new Array(n).fill(null);
|
|
1451
|
+
const trend = new Array(n).fill(0);
|
|
1452
|
+
const slopeCfg = payload.rangeSlope || {};
|
|
1453
|
+
const gridCfg = payload.grid || null;
|
|
1454
|
+
const lookback = Math.max(1, Math.round(Number(slopeCfg.lookbackBars) || 9));
|
|
1455
|
+
const maxSlope = Number(slopeCfg.maxSlopePct) > 0 ? Number(slopeCfg.maxSlopePct) : 0.09;
|
|
1456
|
+
const neutral = Number(slopeCfg.neutralZonePct) >= 0 ? Number(slopeCfg.neutralZonePct) : 0;
|
|
1457
|
+
const maxSlopeOffset = Number(slopeCfg.maxSlopeOffset) > 0 ? Number(slopeCfg.maxSlopeOffset) : 0.5;
|
|
1458
|
+
const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
|
|
1459
|
+
? Number(gridCfg.maxAsymmetryFactor)
|
|
1460
|
+
: (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
|
|
1461
|
+
const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
|
|
1462
|
+
const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
|
|
1463
|
+
// User x-range span (slider 1.2x–2.0x, default 1.55x): the only
|
|
1464
|
+
// width input — symmetric base min = AMA/span, max = AMA*span.
|
|
1465
|
+
const span = Math.min(2, Math.max(1.2, Number(currentRangeSpan) > 0 ? Number(currentRangeSpan) : 1.55));
|
|
1466
|
+
// Canonical grid pipeline (ama_slope_model): adaptive percentile
|
|
1467
|
+
// clip over the AMA history, then offset + trend — the same numbers
|
|
1468
|
+
// the live grid build feeds into applyAsymmetricBounds.
|
|
1469
|
+
const clipPct = Number(slopeCfg.clipPercentile) >= 0 ? Number(slopeCfg.clipPercentile) : 10;
|
|
1470
|
+
const clipEr = Number.isFinite(currentAmaErPeriod) && currentAmaErPeriod > 0
|
|
1471
|
+
? Math.ceil(currentAmaErPeriod)
|
|
1472
|
+
: Math.ceil(Number(payload.amaDefaults?.erPeriod) || 781);
|
|
1473
|
+
const clipThreshold = computeAmaSlopeClipThreshold(baseAma, clipEr, lookback, clipPct);
|
|
1474
|
+
const slopeScaling = !!currentRangeScaleEnabled;
|
|
1475
|
+
if (!currentAmaEnabled || n === 0) return { upper, lower, trend };
|
|
1476
|
+
for (let i = 0; i < n; i++) {
|
|
1477
|
+
const ama = baseAma[i];
|
|
1478
|
+
const past = i - lookback >= 0 ? baseAma[i - lookback] : null;
|
|
1479
|
+
if (!Number.isFinite(ama) || ama <= 0 || !Number.isFinite(past) || past <= 0) continue;
|
|
1480
|
+
const slopePct = computeAverageAmaSlopePct(ama, past, lookback);
|
|
1481
|
+
if (slopePct == null || !Number.isFinite(slopePct)) continue;
|
|
1482
|
+
const csp = Math.max(-clipThreshold, Math.min(clipThreshold, slopePct));
|
|
1483
|
+
const dir = Math.abs(csp) <= neutral ? 0 : (csp > 0 ? 1 : -1);
|
|
1484
|
+
// Symmetric base from this bar's AMA and the user span —
|
|
1485
|
+
// no chart or config price level.
|
|
1486
|
+
let rMin = ama / span;
|
|
1487
|
+
let rMax = ama * span;
|
|
1488
|
+
if (slopeScaling && dir !== 0) {
|
|
1489
|
+
// Canonical tilt: clipped slope offset applied through
|
|
1490
|
+
// asymmetric_bounds, then the narrowing-side guard.
|
|
1491
|
+
const trendName = dir > 0 ? 'UP' : 'DOWN';
|
|
1492
|
+
const slopeOffset = clamp(csp / maxSlope, -1, 1) * maxSlopeOffset;
|
|
1493
|
+
const tilt = applyAsymmetricBounds({
|
|
1494
|
+
centerPrice: ama,
|
|
1495
|
+
minPrice: ama / span,
|
|
1496
|
+
maxPrice: ama * span,
|
|
1497
|
+
trend: trendName,
|
|
1498
|
+
slopeOffset,
|
|
1499
|
+
maxSlopeOffset,
|
|
1500
|
+
maxAsymmetryFactor: maxAsym,
|
|
1501
|
+
});
|
|
1502
|
+
const guard = applyNarrowingSideGuard({
|
|
1503
|
+
centerPrice: ama,
|
|
1504
|
+
minPrice: tilt.resolvedMinPrice,
|
|
1505
|
+
maxPrice: tilt.resolvedMaxPrice,
|
|
1506
|
+
trend: trendName,
|
|
1507
|
+
incrementPercent: inc,
|
|
1508
|
+
minScaleSlots: minSlots,
|
|
1509
|
+
});
|
|
1510
|
+
rMin = guard.minPrice;
|
|
1511
|
+
rMax = guard.maxPrice;
|
|
1512
|
+
}
|
|
1513
|
+
if (!Number.isFinite(rMin) || !Number.isFinite(rMax) || rMin <= 0 || rMax <= rMin) continue;
|
|
1514
|
+
upper[i] = rMax;
|
|
1515
|
+
lower[i] = rMin;
|
|
1516
|
+
trend[i] = dir;
|
|
1517
|
+
}
|
|
1518
|
+
return { upper, lower, trend };
|
|
1519
|
+
}
|
|
1081
1520
|
function lowerBound(arr, value) {
|
|
1082
1521
|
let lo = 0;
|
|
1083
1522
|
let hi = arr.length;
|
|
@@ -1098,6 +1537,25 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1098
1537
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1099
1538
|
let min = Infinity;
|
|
1100
1539
|
let max = -Infinity;
|
|
1540
|
+
// Range scaling active: fit the price axis to the full envelope
|
|
1541
|
+
// (lower ↔ upper through AMA) instead of the candles, so the
|
|
1542
|
+
// switch visibly adjusts the range. Falls through to the candle
|
|
1543
|
+
// fit when the band has no finite values in view.
|
|
1544
|
+
if (currentRangeEnabled && currentRangeScaleEnabled && currentAmaEnabled) {
|
|
1545
|
+
let bmin = Infinity;
|
|
1546
|
+
let bmax = -Infinity;
|
|
1547
|
+
for (let i = start; i < end; i++) {
|
|
1548
|
+
const a = currentAma[i];
|
|
1549
|
+
const up = currentRangeUpper[i];
|
|
1550
|
+
const lo = currentRangeLower[i];
|
|
1551
|
+
if (Number.isFinite(a) && a > 0) { if (a < bmin) bmin = a; if (a > bmax) bmax = a; }
|
|
1552
|
+
if (Number.isFinite(up) && up > 0) { if (up < bmin) bmin = up; if (up > bmax) bmax = up; }
|
|
1553
|
+
if (Number.isFinite(lo) && lo > 0) { if (lo < bmin) bmin = lo; if (lo > bmax) bmax = lo; }
|
|
1554
|
+
}
|
|
1555
|
+
if (Number.isFinite(bmin) && Number.isFinite(bmax) && bmin > 0 && bmax > bmin) {
|
|
1556
|
+
return [bmin * 0.97, bmax * 1.03];
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1101
1559
|
for (let i = start; i < end; i++) {
|
|
1102
1560
|
const lo = currentLow[i];
|
|
1103
1561
|
const hi = currentHigh[i];
|
|
@@ -1125,7 +1583,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1125
1583
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1126
1584
|
let max = 0;
|
|
1127
1585
|
for (let i = start; i < end; i++) {
|
|
1128
|
-
const v = currentCandles[i]
|
|
1586
|
+
const v = getDisplayVolume(currentCandles[i]);
|
|
1129
1587
|
if (Number.isFinite(v) && v > max) max = v;
|
|
1130
1588
|
}
|
|
1131
1589
|
if (!Number.isFinite(max) || max <= 0) return [0, 1];
|
|
@@ -1144,12 +1602,29 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1144
1602
|
btn.title = next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale';
|
|
1145
1603
|
btn.setAttribute('aria-label', next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale');
|
|
1146
1604
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1605
|
+
// AMA presets for the quick buttons (values from constants.ts AMAS).
|
|
1606
|
+
const AMA_PRESETS = {
|
|
1607
|
+
AMA1: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA1.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA1.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA1.slowPeriod} },
|
|
1608
|
+
AMA2: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA2.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA2.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA2.slowPeriod} },
|
|
1609
|
+
AMA3: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA3.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA3.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod} },
|
|
1610
|
+
AMA4: { erPeriod: ${MARKET_ADAPTER.AMAS.AMA4.erPeriod}, fastPeriod: ${MARKET_ADAPTER.AMAS.AMA4.fastPeriod}, slowPeriod: ${MARKET_ADAPTER.AMAS.AMA4.slowPeriod} },
|
|
1611
|
+
};
|
|
1612
|
+
function markActiveAmaPreset() {
|
|
1613
|
+
const cur = { e: Math.round(Number(currentAmaErPeriod)), f: Number(currentAmaFastPeriod), s: Number(currentAmaSlowPeriod) };
|
|
1614
|
+
document.querySelectorAll('.ama-preset-btn').forEach((b) => {
|
|
1615
|
+
const p = AMA_PRESETS[b.dataset.amaPreset];
|
|
1616
|
+
const match = p && Math.round(p.erPeriod) === cur.e && Number(p.fastPeriod) === cur.f && Number(p.slowPeriod) === cur.s;
|
|
1617
|
+
b.classList.toggle('active', !!match);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
function applyAmaPreset(key) {
|
|
1621
|
+
const p = AMA_PRESETS[key];
|
|
1622
|
+
if (!p) return;
|
|
1623
|
+
document.getElementById('ama-er').value = String(p.erPeriod);
|
|
1624
|
+
document.getElementById('ama-fast').value = String(p.fastPeriod);
|
|
1625
|
+
document.getElementById('ama-slow').value = String(p.slowPeriod);
|
|
1626
|
+
document.getElementById('ama-toggle').checked = true;
|
|
1627
|
+
syncInputs();
|
|
1153
1628
|
}
|
|
1154
1629
|
function setIndicatorSeriesVisible(seriesIndex, visible) {
|
|
1155
1630
|
if (!priceChart) return;
|
|
@@ -1174,11 +1649,22 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1174
1649
|
refreshLegend();
|
|
1175
1650
|
saveState();
|
|
1176
1651
|
}
|
|
1652
|
+
function hideRangeBandImmediate() {
|
|
1653
|
+
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1654
|
+
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1655
|
+
// The band is a draw hook with no uPlot series: nothing else
|
|
1656
|
+
// repaints on hide, so redraw explicitly (stale pixels otherwise linger).
|
|
1657
|
+
if (priceChart && typeof priceChart.redraw === 'function') priceChart.redraw();
|
|
1658
|
+
refreshLegend();
|
|
1659
|
+
saveState();
|
|
1660
|
+
}
|
|
1177
1661
|
function hideAmaSeriesImmediate() {
|
|
1178
1662
|
currentAma = new Array(currentCandles.length).fill(null);
|
|
1179
1663
|
currentAmaOff = new Array(currentCandles.length).fill(null);
|
|
1180
1664
|
currentSmaInit = new Array(currentCandles.length).fill(null);
|
|
1181
1665
|
currentSmaInitOff = new Array(currentCandles.length).fill(null);
|
|
1666
|
+
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1667
|
+
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1182
1668
|
if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
|
|
1183
1669
|
currentPriceData[6] = currentAma;
|
|
1184
1670
|
currentPriceData[8] = currentSmaInit;
|
|
@@ -1212,14 +1698,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1212
1698
|
document.getElementById('legend-delta').textContent = Number.isFinite(delta)
|
|
1213
1699
|
? ((delta >= 0 ? '+' : '') + fmtPrice(delta))
|
|
1214
1700
|
: '-';
|
|
1215
|
-
document.getElementById('legend-volume').textContent =
|
|
1216
|
-
document.getElementById('legend-scale').textContent = currentPriceScale === 'linear' ? 'Linear' : 'Log';
|
|
1701
|
+
document.getElementById('legend-volume').textContent = fmtVolumeWithSym(getDisplayVolume(c));
|
|
1217
1702
|
document.getElementById('legend-sma').textContent = Number.isFinite(currentSma[idx]) ? fmtPrice(currentSma[idx]) : (smaPending ? '...' : '-');
|
|
1218
|
-
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1219
|
-
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1220
|
-
document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
|
|
1221
|
-
document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
|
|
1222
1703
|
document.getElementById('legend-vwap').textContent = Number.isFinite(currentVwap[idx]) ? fmtPrice(currentVwap[idx]) : '-';
|
|
1704
|
+
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1705
|
+
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1223
1706
|
}
|
|
1224
1707
|
function candlePlugin() {
|
|
1225
1708
|
function drawCandles(u) {
|
|
@@ -1362,12 +1845,86 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1362
1845
|
hooks: { drawAxes: [drawVolumeBars] },
|
|
1363
1846
|
};
|
|
1364
1847
|
}
|
|
1848
|
+
function rangeBandPlugin() {
|
|
1849
|
+
function drawRangeBand(u) {
|
|
1850
|
+
if (!currentRangeEnabled || !currentAmaEnabled || !currentCandles.length) return;
|
|
1851
|
+
const xs = u.data[0];
|
|
1852
|
+
if (!Array.isArray(xs) || xs.length === 0) return;
|
|
1853
|
+
u.ctx.save();
|
|
1854
|
+
u.ctx.beginPath();
|
|
1855
|
+
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
|
1856
|
+
u.ctx.clip();
|
|
1857
|
+
// Behind candles/AMA lines (same trick as the dynamic-weight signal background).
|
|
1858
|
+
u.ctx.globalCompositeOperation = 'destination-over';
|
|
1859
|
+
const UP_FILL = 'rgba(239,83,80,0.16)';
|
|
1860
|
+
const DOWN_FILL = 'rgba(38,166,154,0.16)';
|
|
1861
|
+
// Symmetric envelope, always both sides: red [AMA, upper]
|
|
1862
|
+
// plus green [lower, AMA] along the whole AMA, tilted by the
|
|
1863
|
+
// grid's range scaling. Paint per contiguous valid segment.
|
|
1864
|
+
let segTop = [];
|
|
1865
|
+
let segBot = [];
|
|
1866
|
+
const flush = () => {
|
|
1867
|
+
if (segTop.length > 1) {
|
|
1868
|
+
u.ctx.fillStyle = UP_FILL;
|
|
1869
|
+
u.ctx.beginPath();
|
|
1870
|
+
u.ctx.moveTo(segTop[0][0], segTop[0][1]);
|
|
1871
|
+
for (let k = 1; k < segTop.length; k++) u.ctx.lineTo(segTop[k][0], segTop[k][1]);
|
|
1872
|
+
for (let k = segTop.length - 1; k >= 0; k--) u.ctx.lineTo(segTop[k][0], segTop[k][2]);
|
|
1873
|
+
u.ctx.closePath();
|
|
1874
|
+
u.ctx.fill();
|
|
1875
|
+
}
|
|
1876
|
+
if (segBot.length > 1) {
|
|
1877
|
+
u.ctx.fillStyle = DOWN_FILL;
|
|
1878
|
+
u.ctx.beginPath();
|
|
1879
|
+
u.ctx.moveTo(segBot[0][0], segBot[0][1]);
|
|
1880
|
+
for (let k = 1; k < segBot.length; k++) u.ctx.lineTo(segBot[k][0], segBot[k][1]);
|
|
1881
|
+
for (let k = segBot.length - 1; k >= 0; k--) u.ctx.lineTo(segBot[k][0], segBot[k][2]);
|
|
1882
|
+
u.ctx.closePath();
|
|
1883
|
+
u.ctx.fill();
|
|
1884
|
+
}
|
|
1885
|
+
u.ctx.strokeStyle = 'rgba(239,83,80,0.55)';
|
|
1886
|
+
u.ctx.lineWidth = 1;
|
|
1887
|
+
u.ctx.beginPath();
|
|
1888
|
+
segTop.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1889
|
+
u.ctx.stroke();
|
|
1890
|
+
u.ctx.strokeStyle = 'rgba(38,166,154,0.55)';
|
|
1891
|
+
u.ctx.beginPath();
|
|
1892
|
+
segBot.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1893
|
+
u.ctx.stroke();
|
|
1894
|
+
segTop = [];
|
|
1895
|
+
segBot = [];
|
|
1896
|
+
};
|
|
1897
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1898
|
+
const a = currentAma[i];
|
|
1899
|
+
const up = currentRangeUpper[i];
|
|
1900
|
+
const lo = currentRangeLower[i];
|
|
1901
|
+
if (!Number.isFinite(a) || a <= 0 || !Number.isFinite(up) || up <= a
|
|
1902
|
+
|| !Number.isFinite(lo) || lo <= 0 || lo >= a) {
|
|
1903
|
+
flush();
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
const x = u.valToPos(xs[i], 'x', true);
|
|
1907
|
+
const yA = u.valToPos(a, 'y', true);
|
|
1908
|
+
const yU = u.valToPos(up, 'y', true);
|
|
1909
|
+
const yL = u.valToPos(lo, 'y', true);
|
|
1910
|
+
if (!Number.isFinite(x) || !Number.isFinite(yA) || !Number.isFinite(yU) || !Number.isFinite(yL)) {
|
|
1911
|
+
flush();
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
segTop.push([x, yU, yA]);
|
|
1915
|
+
segBot.push([x, yL, yA]);
|
|
1916
|
+
}
|
|
1917
|
+
flush();
|
|
1918
|
+
u.ctx.restore();
|
|
1919
|
+
}
|
|
1920
|
+
return { hooks: { draw: [drawRangeBand] } };
|
|
1921
|
+
}
|
|
1365
1922
|
function buildData() {
|
|
1366
1923
|
const tf = timeframeMap.get(currentTimeframe) || timeframeMap.get(defaultTimeframe.label) || timeframes[0];
|
|
1367
1924
|
currentSeriesState = getSeriesState(currentPairMode);
|
|
1368
1925
|
currentSeriesCandles = currentSeriesState.candles;
|
|
1369
1926
|
currentSeriesCloseValues = currentSeriesState.closeValues;
|
|
1370
|
-
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600);
|
|
1927
|
+
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600, tf?.label || currentTimeframe);
|
|
1371
1928
|
currentCandles = aggregated.candles;
|
|
1372
1929
|
if (currentCandles.length) {
|
|
1373
1930
|
xMin = currentCandles[0].time;
|
|
@@ -1396,21 +1953,33 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1396
1953
|
currentSma = new Array(currentCandles.length).fill(null);
|
|
1397
1954
|
}
|
|
1398
1955
|
currentAma = currentAmaEnabled ? sampleSeriesByIndex(baseAma, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1956
|
+
const baseRange = computeRangeBand(baseAma);
|
|
1957
|
+
currentRangeUpper = sampleSeriesByIndex(baseRange.upper, aggregated.idxs);
|
|
1958
|
+
currentRangeLower = sampleSeriesByIndex(baseRange.lower, aggregated.idxs);
|
|
1399
1959
|
currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1400
1960
|
currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1401
1961
|
const amaCfg = currentAmaConfig();
|
|
1402
1962
|
const warmupInit = currentAmaEnabled && baseAma.length > amaCfg.erPeriod ? baseAma[amaCfg.erPeriod] : null;
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1963
|
+
// Build the init segment in the selected timeframe. Sampling the
|
|
1964
|
+
// 1h init series by each bucket's final 1h index can make it end
|
|
1965
|
+
// inside a week/month bucket, leaving a gap before the first
|
|
1966
|
+
// sampled AMA value appears.
|
|
1967
|
+
const firstAmaIndex = currentAma.findIndex((value) => Number.isFinite(value));
|
|
1968
|
+
// The first visible higher-timeframe AMA sample can already be
|
|
1969
|
+
// several 1h updates past the true warmup point. Anchor that
|
|
1970
|
+
// sample to the warmup average so the AMA starts where the init
|
|
1971
|
+
// line ends instead of appearing detached at a different value.
|
|
1972
|
+
if (warmupInit !== null && firstAmaIndex >= 0) currentAma[firstAmaIndex] = warmupInit;
|
|
1973
|
+
currentSmaInit = warmupInit !== null
|
|
1974
|
+
? currentAma.map((value, i) => i <= firstAmaIndex || (firstAmaIndex < 0 && i === currentAma.length - 1) ? warmupInit : null)
|
|
1975
|
+
: new Array(currentCandles.length).fill(null);
|
|
1407
1976
|
const offsetInit = currentAmaEnabled && currentAmaInitOffset !== 0 && baseAmaOff.length > amaCfg.erPeriod
|
|
1408
1977
|
? baseAmaOff[amaCfg.erPeriod]
|
|
1409
1978
|
: null;
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1979
|
+
const firstAmaOffIndex = currentAmaOff.findIndex((value) => Number.isFinite(value));
|
|
1980
|
+
currentSmaInitOff = offsetInit !== null
|
|
1981
|
+
? currentAmaOff.map((value, i) => i < firstAmaOffIndex || (firstAmaOffIndex < 0 && i === currentAmaOff.length - 1) ? offsetInit : null)
|
|
1982
|
+
: new Array(currentCandles.length).fill(null);
|
|
1414
1983
|
currentPriceData = [
|
|
1415
1984
|
currentCandles.map((c) => c.time),
|
|
1416
1985
|
currentOpen,
|
|
@@ -1426,7 +1995,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1426
1995
|
];
|
|
1427
1996
|
currentVolumeData = [
|
|
1428
1997
|
currentCandles.map((c) => c.time),
|
|
1429
|
-
currentCandles.map((c) => c
|
|
1998
|
+
currentCandles.map((c) => getDisplayVolume(c)),
|
|
1430
1999
|
];
|
|
1431
2000
|
return {
|
|
1432
2001
|
priceData: currentPriceData,
|
|
@@ -1443,7 +2012,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1443
2012
|
padding: [14, 8, 8, 8],
|
|
1444
2013
|
legend: { show: false },
|
|
1445
2014
|
select: { show: false },
|
|
1446
|
-
cursor: { sync: { key:
|
|
2015
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1447
2016
|
scales: {
|
|
1448
2017
|
x: { time: true },
|
|
1449
2018
|
y: {
|
|
@@ -1494,13 +2063,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1494
2063
|
},
|
|
1495
2064
|
],
|
|
1496
2065
|
hooks: {
|
|
1497
|
-
draw: [(u) => positionPriceMarker(u)],
|
|
2066
|
+
draw: [(u) => { positionPriceMarker(u); positionReserveLine(u); positionOrderLines(u); positionUpdateMarker(u, true); }],
|
|
2067
|
+
// Keep the bottom-right stat badges glued to the visible
|
|
2068
|
+
// window while zooming/panning (rAF-throttled text swap).
|
|
2069
|
+
// Refit Y to the visible window on timeframe (x) moves —
|
|
2070
|
+
// uPlot does not recompute Y on setScale('x') by itself.
|
|
2071
|
+
setScale: [(u, key) => { if (key === 'x') scheduleYRefit(); scheduleStatPanels(); }],
|
|
1498
2072
|
},
|
|
1499
2073
|
};
|
|
1500
2074
|
|
|
1501
2075
|
const plugin = candlePlugin();
|
|
1502
2076
|
plugin.opts(null, priceOpts);
|
|
1503
|
-
priceOpts.plugins = [plugin];
|
|
2077
|
+
priceOpts.plugins = [rangeBandPlugin(), plugin];
|
|
1504
2078
|
if (!priceChart) priceChart = new uPlot(priceOpts, data.priceData, priceEl);
|
|
1505
2079
|
else priceChart.setData(data.priceData, false);
|
|
1506
2080
|
|
|
@@ -1510,7 +2084,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1510
2084
|
padding: [6, 8, 8, 8],
|
|
1511
2085
|
legend: { show: false },
|
|
1512
2086
|
select: { show: false },
|
|
1513
|
-
cursor: { sync: { key:
|
|
2087
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1514
2088
|
scales: {
|
|
1515
2089
|
x: { time: true },
|
|
1516
2090
|
y: {
|
|
@@ -1531,6 +2105,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1531
2105
|
makeTimeAxis(true),
|
|
1532
2106
|
{ 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))) },
|
|
1533
2107
|
],
|
|
2108
|
+
hooks: {
|
|
2109
|
+
// x is synced from the price chart; refit volume Y to the
|
|
2110
|
+
// visible window and refresh the max badge.
|
|
2111
|
+
setScale: [(u, key) => { if (key === 'x') scheduleYRefit(); scheduleStatPanels(); }],
|
|
2112
|
+
},
|
|
1534
2113
|
};
|
|
1535
2114
|
|
|
1536
2115
|
const volumePluginInst = volumePlugin();
|
|
@@ -1543,32 +2122,33 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1543
2122
|
lastRenderedPriceScale = currentPriceScale;
|
|
1544
2123
|
return priceChart;
|
|
1545
2124
|
}
|
|
1546
|
-
// True when clientX sits over the price-axis
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
2125
|
+
// True when clientX sits over the true price-axis gutter (right of
|
|
2126
|
+
// the plot area). Keyed off uPlot's plot rect, not the container
|
|
2127
|
+
// width, so grabs on candles — even the newest ones at the plot's
|
|
2128
|
+
// right edge — always pan instead of scaling. Only the axis itself
|
|
2129
|
+
// zooms price; the rest pans time.
|
|
1551
2130
|
function inYAxisZone(chart, clientX) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2131
|
+
if (chart !== priceChart || !chart.over) return false;
|
|
2132
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2133
|
+
if (!overRect || overRect.width <= 0) return false;
|
|
2134
|
+
return clientX >= overRect.right - 2;
|
|
2135
|
+
}
|
|
2136
|
+
// True when the pointer sits over the time-axis gutter (below the
|
|
2137
|
+
// plot area) of either chart. Dragging there scales the timeframe;
|
|
2138
|
+
// drags on candles always pan instead.
|
|
2139
|
+
function inXAxisZone(chart, clientY) {
|
|
2140
|
+
if (!chart || !chart.over) return false;
|
|
2141
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2142
|
+
if (!overRect || overRect.height <= 0) return false;
|
|
2143
|
+
return clientY >= overRect.bottom - 2;
|
|
1559
2144
|
}
|
|
1560
2145
|
let priceMarkerLabel = null;
|
|
1561
|
-
let priceMarkerLine = null;
|
|
1562
2146
|
function ensurePriceMarker(u) {
|
|
1563
2147
|
if (priceMarkerLabel && priceMarkerLabel.parentNode === u.root) return;
|
|
1564
2148
|
if (priceMarkerLabel && priceMarkerLabel.parentNode) priceMarkerLabel.parentNode.removeChild(priceMarkerLabel);
|
|
1565
|
-
if (priceMarkerLine && priceMarkerLine.parentNode) priceMarkerLine.parentNode.removeChild(priceMarkerLine);
|
|
1566
2149
|
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
1567
2150
|
priceMarkerLabel = document.createElement('div');
|
|
1568
2151
|
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;';
|
|
1569
|
-
priceMarkerLine = document.createElement('div');
|
|
1570
|
-
priceMarkerLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:1px dashed currentColor;opacity:0.6;';
|
|
1571
|
-
u.root.appendChild(priceMarkerLine);
|
|
1572
2152
|
u.root.appendChild(priceMarkerLabel);
|
|
1573
2153
|
}
|
|
1574
2154
|
function positionPriceMarker(u) {
|
|
@@ -1579,7 +2159,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1579
2159
|
const price = last.close;
|
|
1580
2160
|
if (!Number.isFinite(price)) {
|
|
1581
2161
|
priceMarkerLabel.style.display = 'none';
|
|
1582
|
-
priceMarkerLine.style.display = 'none';
|
|
1583
2162
|
return;
|
|
1584
2163
|
}
|
|
1585
2164
|
const up = price >= (Number.isFinite(prev.close) ? prev.close : price);
|
|
@@ -1589,7 +2168,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1589
2168
|
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
1590
2169
|
if (sMin == null || sMax == null || sMax <= sMin) {
|
|
1591
2170
|
priceMarkerLabel.style.display = 'none';
|
|
1592
|
-
priceMarkerLine.style.display = 'none';
|
|
1593
2171
|
return;
|
|
1594
2172
|
}
|
|
1595
2173
|
let frac;
|
|
@@ -1606,7 +2184,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1606
2184
|
if (!overRect || overRect.height <= 0) return;
|
|
1607
2185
|
const plotTop = overRect.top - rootRect.top;
|
|
1608
2186
|
const yRel = (1 - frac) * overRect.height;
|
|
1609
|
-
|
|
2187
|
+
// No full-width dashed line by design (disturbs reading) — the
|
|
2188
|
+
// axis label above carries the last price on its own.
|
|
1610
2189
|
priceMarkerLabel.style.display = 'block';
|
|
1611
2190
|
priceMarkerLabel.textContent = fmtPriceLabel(price);
|
|
1612
2191
|
priceMarkerLabel.style.background = color;
|
|
@@ -1622,15 +2201,335 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1622
2201
|
priceMarkerLabel.style.right = '0px';
|
|
1623
2202
|
priceMarkerLabel.style.width = Math.max(40, rightAxisW - 4) + 'px';
|
|
1624
2203
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2204
|
+
}
|
|
2205
|
+
// ── Order overlay ──
|
|
2206
|
+
// Active grid orders as dashed levels: green = buys, red = sells.
|
|
2207
|
+
// Grid bounds span the full grid (live + virtual slots), never
|
|
2208
|
+
// calculated: reserve = lowest grid buy, ceiling = highest grid sell.
|
|
2209
|
+
// Spread = best buy/best sell gap.
|
|
2210
|
+
// Pair-aware: order prices invert (1/p) with the candles so B/A shows
|
|
2211
|
+
// the same levels in display units (side colors preserved).
|
|
2212
|
+
let reserveLine = null;
|
|
2213
|
+
let reserveLabel = null;
|
|
2214
|
+
let ceilingLine = null;
|
|
2215
|
+
let ceilingLabel = null;
|
|
2216
|
+
let spreadMidLabel = null;
|
|
2217
|
+
let orderLineDivs = [];
|
|
2218
|
+
let orderBuyLabel = null;
|
|
2219
|
+
let orderSellLabel = null;
|
|
2220
|
+
let orderPriceTags = [];
|
|
2221
|
+
let lastOverlayKey = '';
|
|
2222
|
+
function getDisplayOrders() {
|
|
2223
|
+
const rawBuys = Array.isArray(payload.orderBuys) ? payload.orderBuys.filter(Number.isFinite).filter((p) => p > 0) : [];
|
|
2224
|
+
const rawSells = Array.isArray(payload.orderSells) ? payload.orderSells.filter(Number.isFinite).filter((p) => p > 0) : [];
|
|
2225
|
+
if (normalizePairMode(currentPairMode) !== 'inverse') return { buys: rawBuys, sells: rawSells };
|
|
2226
|
+
const inv = (arr) => arr.map((p) => 1 / p).filter(Number.isFinite).filter((p) => p > 0);
|
|
2227
|
+
return { buys: inv(rawBuys), sells: inv(rawSells) };
|
|
2228
|
+
}
|
|
2229
|
+
function yForPriceCached(ys, overRect, rootRect, price) {
|
|
2230
|
+
const sMin = Number.isFinite(ys.min) ? ys.min : null;
|
|
2231
|
+
const sMax = Number.isFinite(ys.max) ? ys.max : null;
|
|
2232
|
+
if (sMin == null || sMax == null || sMax <= sMin) return null;
|
|
2233
|
+
let frac;
|
|
2234
|
+
if (currentPriceScale === 'log') {
|
|
2235
|
+
if (!(sMin > 0) || !(price > 0)) return null;
|
|
2236
|
+
const lmin = Math.log10(sMin), lmax = Math.log10(sMax);
|
|
2237
|
+
frac = (Math.log10(price) - lmin) / (lmax - lmin);
|
|
1631
2238
|
} else {
|
|
1632
|
-
|
|
2239
|
+
frac = (price - sMin) / (sMax - sMin);
|
|
1633
2240
|
}
|
|
2241
|
+
if (frac < 0 || frac > 1) return null;
|
|
2242
|
+
return (overRect.top - rootRect.top) + (1 - frac) * overRect.height;
|
|
2243
|
+
}
|
|
2244
|
+
function hideOverlayNodes() {
|
|
2245
|
+
[reserveLine, reserveLabel, ceilingLine, ceilingLabel, spreadMidLabel, orderBuyLabel, orderSellLabel].forEach((n) => { if (n) n.style.display = 'none'; });
|
|
2246
|
+
orderLineDivs.forEach((d) => { d.style.display = 'none'; });
|
|
2247
|
+
orderPriceTags.forEach((t) => { t.style.display = 'none'; });
|
|
2248
|
+
}
|
|
2249
|
+
function positionReserveLine(u) {
|
|
2250
|
+
if (!u || !u.over || !currentCandles.length) return;
|
|
2251
|
+
if (!currentOrdersVisible || (!hasOrders && !hasBounds)) { hideOverlayNodes(); return; }
|
|
2252
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2253
|
+
if (!reserveLine || reserveLine.parentNode !== u.root) {
|
|
2254
|
+
if (reserveLine && reserveLine.parentNode) reserveLine.parentNode.removeChild(reserveLine);
|
|
2255
|
+
if (reserveLabel && reserveLabel.parentNode) reserveLabel.parentNode.removeChild(reserveLabel);
|
|
2256
|
+
reserveLine = document.createElement('div');
|
|
2257
|
+
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;';
|
|
2258
|
+
reserveLabel = document.createElement('div');
|
|
2259
|
+
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;';
|
|
2260
|
+
u.root.appendChild(reserveLine);
|
|
2261
|
+
u.root.appendChild(reserveLabel);
|
|
2262
|
+
}
|
|
2263
|
+
// Grid bounds span the full DEXBot grid (live + virtual slots
|
|
2264
|
+
// in base units), never calculated: bottom = lowest grid buy,
|
|
2265
|
+
// top = highest grid sell. Reciprocals flip the ordering, so
|
|
2266
|
+
// the sides swap in B/A view. A missing side hides its line
|
|
2267
|
+
// instead of drawing an invented level.
|
|
2268
|
+
const { buys: boundBuys, sells: boundSells } = getDisplayOrders();
|
|
2269
|
+
const rawLow = Number(payload.gridBounds?.low);
|
|
2270
|
+
const rawHigh = Number(payload.gridBounds?.high);
|
|
2271
|
+
const gridLow = Number.isFinite(rawLow) && rawLow > 0 ? rawLow : null;
|
|
2272
|
+
const gridHigh = Number.isFinite(rawHigh) && rawHigh > 0 ? rawHigh : null;
|
|
2273
|
+
const inversePairs = normalizePairMode(currentPairMode) === 'inverse';
|
|
2274
|
+
const reservePrice = inversePairs
|
|
2275
|
+
? (gridHigh != null ? 1 / gridHigh : null)
|
|
2276
|
+
: gridLow;
|
|
2277
|
+
const ceilingPrice = inversePairs
|
|
2278
|
+
? (gridLow != null ? 1 / gridLow : null)
|
|
2279
|
+
: gridHigh;
|
|
2280
|
+
const ys = u.scales.y || {};
|
|
2281
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2282
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2283
|
+
const y = reservePrice != null ? yForPriceCached(ys, overRect, rootRect, reservePrice) : null;
|
|
2284
|
+
if (y == null) {
|
|
2285
|
+
reserveLine.style.display = 'none';
|
|
2286
|
+
reserveLabel.style.display = 'none';
|
|
2287
|
+
} else {
|
|
2288
|
+
reserveLine.style.display = 'block';
|
|
2289
|
+
reserveLine.style.top = y + 'px';
|
|
2290
|
+
const lastClose = currentCandles[currentCandles.length - 1].close;
|
|
2291
|
+
const dipRaw = Number.isFinite(lastClose) && lastClose > 0 ? (lastClose - reservePrice) / lastClose * 100 : NaN;
|
|
2292
|
+
const dipTxt = Number.isFinite(dipRaw) ? (dipRaw >= 0 ? '-' : '+') + Math.abs(Math.round(dipRaw)) + '%' : '-';
|
|
2293
|
+
reserveLabel.style.display = 'block';
|
|
2294
|
+
reserveLabel.textContent = 'reserve ' + fmtPriceLabel(reservePrice) + ' (' + dipTxt + ')';
|
|
2295
|
+
reserveLabel.style.left = '8px';
|
|
2296
|
+
reserveLabel.style.top = (y - 16) + 'px';
|
|
2297
|
+
}
|
|
2298
|
+
if (!ceilingLine || ceilingLine.parentNode !== u.root) {
|
|
2299
|
+
if (ceilingLine && ceilingLine.parentNode) ceilingLine.parentNode.removeChild(ceilingLine);
|
|
2300
|
+
if (ceilingLabel && ceilingLabel.parentNode) ceilingLabel.parentNode.removeChild(ceilingLabel);
|
|
2301
|
+
ceilingLine = document.createElement('div');
|
|
2302
|
+
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;';
|
|
2303
|
+
ceilingLabel = document.createElement('div');
|
|
2304
|
+
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;';
|
|
2305
|
+
u.root.appendChild(ceilingLine);
|
|
2306
|
+
u.root.appendChild(ceilingLabel);
|
|
2307
|
+
}
|
|
2308
|
+
const cY = ceilingPrice != null ? yForPriceCached(ys, overRect, rootRect, ceilingPrice) : null;
|
|
2309
|
+
if (cY == null) {
|
|
2310
|
+
ceilingLine.style.display = 'none';
|
|
2311
|
+
ceilingLabel.style.display = 'none';
|
|
2312
|
+
} else {
|
|
2313
|
+
ceilingLine.style.display = 'block';
|
|
2314
|
+
ceilingLine.style.top = cY + 'px';
|
|
2315
|
+
const lastCloseTop = currentCandles[currentCandles.length - 1].close;
|
|
2316
|
+
const upRaw = Number.isFinite(lastCloseTop) && lastCloseTop > 0 ? (ceilingPrice - lastCloseTop) / lastCloseTop * 100 : NaN;
|
|
2317
|
+
const upTxt = Number.isFinite(upRaw) ? (upRaw >= 0 ? '+' : '-') + Math.abs(Math.round(upRaw)) + '%' : '-';
|
|
2318
|
+
ceilingLabel.style.display = 'block';
|
|
2319
|
+
ceilingLabel.textContent = 'top ' + fmtPriceLabel(ceilingPrice) + ' (' + upTxt + ')';
|
|
2320
|
+
ceilingLabel.style.left = '8px';
|
|
2321
|
+
ceilingLabel.style.top = (cY + 4) + 'px';
|
|
2322
|
+
}
|
|
2323
|
+
if (!spreadMidLabel || spreadMidLabel.parentNode !== u.root) {
|
|
2324
|
+
if (spreadMidLabel && spreadMidLabel.parentNode) spreadMidLabel.parentNode.removeChild(spreadMidLabel);
|
|
2325
|
+
spreadMidLabel = document.createElement('div');
|
|
2326
|
+
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;';
|
|
2327
|
+
u.root.appendChild(spreadMidLabel);
|
|
2328
|
+
}
|
|
2329
|
+
if (boundBuys.length && boundSells.length) {
|
|
2330
|
+
const bestBuy = Math.max(...boundBuys);
|
|
2331
|
+
const bestSell = Math.min(...boundSells);
|
|
2332
|
+
const yB = yForPriceCached(ys, overRect, rootRect, bestBuy);
|
|
2333
|
+
const yS = yForPriceCached(ys, overRect, rootRect, bestSell);
|
|
2334
|
+
if (yB != null && yS != null && bestBuy > 0) {
|
|
2335
|
+
const spr = (bestSell - bestBuy) / bestBuy * 100;
|
|
2336
|
+
spreadMidLabel.style.display = 'block';
|
|
2337
|
+
spreadMidLabel.textContent = 'spread ' + spr.toFixed(2) + '%';
|
|
2338
|
+
spreadMidLabel.style.left = '8px';
|
|
2339
|
+
spreadMidLabel.style.top = ((yB + yS) / 2 - 8) + 'px';
|
|
2340
|
+
} else {
|
|
2341
|
+
spreadMidLabel.style.display = 'none';
|
|
2342
|
+
}
|
|
2343
|
+
} else if (spreadMidLabel) {
|
|
2344
|
+
spreadMidLabel.style.display = 'none';
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function positionOrderLines(u) {
|
|
2348
|
+
if (!u || !u.over) return;
|
|
2349
|
+
if (!currentOrdersVisible) {
|
|
2350
|
+
orderLineDivs.forEach((d) => { d.style.display = 'none'; });
|
|
2351
|
+
orderPriceTags.forEach((t) => { t.style.display = 'none'; });
|
|
2352
|
+
if (orderBuyLabel) orderBuyLabel.style.display = 'none';
|
|
2353
|
+
if (orderSellLabel) orderSellLabel.style.display = 'none';
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2357
|
+
const { buys, sells } = getDisplayOrders();
|
|
2358
|
+
const total = buys.length + sells.length;
|
|
2359
|
+
if (orderLineDivs.length && orderLineDivs[0].parentNode !== u.root) {
|
|
2360
|
+
orderLineDivs.forEach((d) => u.root.appendChild(d));
|
|
2361
|
+
orderPriceTags.forEach((t) => u.root.appendChild(t));
|
|
2362
|
+
lastOverlayKey = '';
|
|
2363
|
+
}
|
|
2364
|
+
while (orderLineDivs.length < total) {
|
|
2365
|
+
const d = document.createElement('div');
|
|
2366
|
+
d.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;left:0;right:0;display:none;';
|
|
2367
|
+
u.root.appendChild(d);
|
|
2368
|
+
orderLineDivs.push(d);
|
|
2369
|
+
}
|
|
2370
|
+
const ys = u.scales.y || {};
|
|
2371
|
+
const overlayKey = (ys.min || 0) + '|' + (ys.max || 0) + '|' + (u.root.clientWidth || 0) + '|' + (u.root.clientHeight || 0) + '|' + currentPriceScale + '|' + currentPairMode;
|
|
2372
|
+
if (overlayKey === lastOverlayKey) return;
|
|
2373
|
+
lastOverlayKey = overlayKey;
|
|
2374
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2375
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2376
|
+
if (!orderBuyLabel || orderBuyLabel.parentNode !== u.root) {
|
|
2377
|
+
if (orderBuyLabel && orderBuyLabel.parentNode) orderBuyLabel.parentNode.removeChild(orderBuyLabel);
|
|
2378
|
+
orderBuyLabel = document.createElement('div');
|
|
2379
|
+
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;';
|
|
2380
|
+
u.root.appendChild(orderBuyLabel);
|
|
2381
|
+
}
|
|
2382
|
+
if (!orderSellLabel || orderSellLabel.parentNode !== u.root) {
|
|
2383
|
+
if (orderSellLabel && orderSellLabel.parentNode) orderSellLabel.parentNode.removeChild(orderSellLabel);
|
|
2384
|
+
orderSellLabel = document.createElement('div');
|
|
2385
|
+
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;';
|
|
2386
|
+
u.root.appendChild(orderSellLabel);
|
|
2387
|
+
}
|
|
2388
|
+
let idx = 0;
|
|
2389
|
+
const place = (price, color) => {
|
|
2390
|
+
const d = orderLineDivs[idx++];
|
|
2391
|
+
const y = yForPriceCached(ys, overRect, rootRect, price);
|
|
2392
|
+
if (y == null) { d.style.display = 'none'; return null; }
|
|
2393
|
+
d.style.display = 'block';
|
|
2394
|
+
d.style.top = y + 'px';
|
|
2395
|
+
d.style.borderTop = '1.5px dashed ' + color;
|
|
2396
|
+
d.style.opacity = '0.55';
|
|
2397
|
+
return y;
|
|
2398
|
+
};
|
|
2399
|
+
let botBuyY = null;
|
|
2400
|
+
buys.forEach((p) => { const y = place(p, '#26a69a'); if (y != null && (botBuyY == null || y > botBuyY)) botBuyY = y; });
|
|
2401
|
+
let topSellY = null;
|
|
2402
|
+
sells.forEach((p) => { const y = place(p, '#ef5350'); if (y != null && (topSellY == null || y < topSellY)) topSellY = y; });
|
|
2403
|
+
for (; idx < orderLineDivs.length; idx++) orderLineDivs[idx].style.display = 'none';
|
|
2404
|
+
if (orderPriceTags.length && orderPriceTags[0].parentNode !== u.root) {
|
|
2405
|
+
orderPriceTags.forEach((t) => u.root.appendChild(t));
|
|
2406
|
+
}
|
|
2407
|
+
const levels = [
|
|
2408
|
+
...buys.map((p) => ({ p, c: '#26a69a', bg: 'rgba(20,30,28,0.92)' })),
|
|
2409
|
+
...sells.map((p) => ({ p, c: '#ef5350', bg: 'rgba(30,20,22,0.92)' })),
|
|
2410
|
+
].sort((a, b) => a.p - b.p);
|
|
2411
|
+
let ti = 0, lastTagY = -Infinity;
|
|
2412
|
+
const ensureTag = () => {
|
|
2413
|
+
if (ti >= orderPriceTags.length) {
|
|
2414
|
+
const t = document.createElement('div');
|
|
2415
|
+
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;';
|
|
2416
|
+
u.root.appendChild(t);
|
|
2417
|
+
orderPriceTags.push(t);
|
|
2418
|
+
}
|
|
2419
|
+
return orderPriceTags[ti++];
|
|
2420
|
+
};
|
|
2421
|
+
for (const lv of levels) {
|
|
2422
|
+
const tag = ensureTag();
|
|
2423
|
+
const y = yForPriceCached(ys, overRect, rootRect, lv.p);
|
|
2424
|
+
if (y == null || Math.abs(y - lastTagY) < 13) { tag.style.display = 'none'; continue; }
|
|
2425
|
+
tag.style.display = 'block';
|
|
2426
|
+
tag.style.top = (y - 7) + 'px';
|
|
2427
|
+
tag.style.right = '4px';
|
|
2428
|
+
tag.style.color = lv.c;
|
|
2429
|
+
tag.style.background = lv.bg;
|
|
2430
|
+
tag.style.border = '1px solid ' + lv.c;
|
|
2431
|
+
tag.textContent = fmtPriceLabel(lv.p);
|
|
2432
|
+
lastTagY = y;
|
|
2433
|
+
}
|
|
2434
|
+
for (; ti < orderPriceTags.length; ti++) orderPriceTags[ti].style.display = 'none';
|
|
2435
|
+
if (buys.length && botBuyY != null) {
|
|
2436
|
+
orderBuyLabel.style.display = 'block';
|
|
2437
|
+
orderBuyLabel.textContent = 'BUYS (' + buys.length + ') ' + fmtPriceLabel(Math.min(...buys));
|
|
2438
|
+
orderBuyLabel.style.left = '8px';
|
|
2439
|
+
orderBuyLabel.style.top = (botBuyY + 4) + 'px';
|
|
2440
|
+
} else {
|
|
2441
|
+
orderBuyLabel.style.display = 'none';
|
|
2442
|
+
}
|
|
2443
|
+
if (sells.length && topSellY != null) {
|
|
2444
|
+
orderSellLabel.style.display = 'block';
|
|
2445
|
+
orderSellLabel.textContent = 'SELLS (' + sells.length + ') ' + fmtPriceLabel(Math.max(...sells));
|
|
2446
|
+
orderSellLabel.style.left = '8px';
|
|
2447
|
+
orderSellLabel.style.top = (topSellY - 16) + 'px';
|
|
2448
|
+
} else {
|
|
2449
|
+
orderSellLabel.style.display = 'none';
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function resetOverlayNodes() {
|
|
2453
|
+
reserveLine = null;
|
|
2454
|
+
reserveLabel = null;
|
|
2455
|
+
ceilingLine = null;
|
|
2456
|
+
ceilingLabel = null;
|
|
2457
|
+
spreadMidLabel = null;
|
|
2458
|
+
orderLineDivs = [];
|
|
2459
|
+
orderBuyLabel = null;
|
|
2460
|
+
orderSellLabel = null;
|
|
2461
|
+
orderPriceTags = [];
|
|
2462
|
+
lastOverlayKey = '';
|
|
2463
|
+
}
|
|
2464
|
+
// ── Volume hover tooltip ──
|
|
2465
|
+
let volumeHoverTip = null;
|
|
2466
|
+
// ── Update marker ("updated from here"): vertical line where the
|
|
2467
|
+
// latest incremental fetch started, with a date label. Shown only
|
|
2468
|
+
// when the generator was given a marker timestamp.
|
|
2469
|
+
const UPDATE_MARKER_SEC = Number(payload.updateMarkerTsSec) > 0 ? Number(payload.updateMarkerTsSec) : null;
|
|
2470
|
+
function positionUpdateMarker(u, withLabel) {
|
|
2471
|
+
if (!UPDATE_MARKER_SEC || !u || !u.over || !currentCandles.length) return;
|
|
2472
|
+
let wrap = u.root.querySelector(':scope > .um-wrap');
|
|
2473
|
+
if (!wrap) {
|
|
2474
|
+
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
2475
|
+
wrap = document.createElement('div');
|
|
2476
|
+
wrap.style.cssText = 'position:absolute;z-index:24;pointer-events:none;';
|
|
2477
|
+
wrap.innerHTML =
|
|
2478
|
+
'<div class="um-line" style="position:absolute;top:0;height:100%;width:0;border-left:2px dashed #22d3ee;opacity:0.75;"></div>' +
|
|
2479
|
+
'<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>';
|
|
2480
|
+
u.root.appendChild(wrap);
|
|
2481
|
+
}
|
|
2482
|
+
const s = u.scales.x || {};
|
|
2483
|
+
const sMin = Number.isFinite(s.min) ? s.min : null;
|
|
2484
|
+
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
2485
|
+
if (sMin == null || sMax == null || sMax <= sMin) { wrap.style.display = 'none'; return; }
|
|
2486
|
+
const frac = (UPDATE_MARKER_SEC - sMin) / (sMax - sMin);
|
|
2487
|
+
const inView = frac >= 0 && frac <= 1;
|
|
2488
|
+
if (!inView) { wrap.style.display = 'none'; return; }
|
|
2489
|
+
const rootRect = u.root.getBoundingClientRect();
|
|
2490
|
+
const overRect = u.over.getBoundingClientRect();
|
|
2491
|
+
const xRoot = (overRect.left - rootRect.left) + frac * overRect.width;
|
|
2492
|
+
wrap.style.display = 'block';
|
|
2493
|
+
wrap.style.left = xRoot + 'px';
|
|
2494
|
+
wrap.style.top = (overRect.top - rootRect.top) + 'px';
|
|
2495
|
+
wrap.style.height = overRect.height + 'px';
|
|
2496
|
+
if (withLabel) {
|
|
2497
|
+
const tag = wrap.querySelector('.um-tag');
|
|
2498
|
+
const d = new Date(UPDATE_MARKER_SEC * 1000);
|
|
2499
|
+
const hh = String(d.getUTCHours()).padStart(2, '0');
|
|
2500
|
+
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
|
2501
|
+
const bars = Number(payload.updateMarkerNewBars) > 0 ? ' (+' + Math.round(Number(payload.updateMarkerNewBars)) + ')' : '';
|
|
2502
|
+
tag.textContent = 'update ' + d.toLocaleDateString(undefined) + ' ' + hh + ':' + mm + bars + ' →';
|
|
2503
|
+
tag.style.left = (frac > 0.8 ? -(tag.offsetWidth + 8) : 6) + 'px';
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function ensureVolumeHoverTip(u) {
|
|
2507
|
+
if (volumeHoverTip && volumeHoverTip.parentNode === u.root) return;
|
|
2508
|
+
if (volumeHoverTip && volumeHoverTip.parentNode) volumeHoverTip.parentNode.removeChild(volumeHoverTip);
|
|
2509
|
+
volumeHoverTip = document.createElement('div');
|
|
2510
|
+
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;';
|
|
2511
|
+
u.root.appendChild(volumeHoverTip);
|
|
2512
|
+
}
|
|
2513
|
+
function showVolumeHover(u) {
|
|
2514
|
+
if (!u || !u.over || !currentCandles.length) return;
|
|
2515
|
+
ensureVolumeHoverTip(u);
|
|
2516
|
+
const idx = u.cursor.idx;
|
|
2517
|
+
if (idx == null || idx < 0 || idx >= currentCandles.length) {
|
|
2518
|
+
volumeHoverTip.style.display = 'none';
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
const c = currentCandles[idx];
|
|
2522
|
+
const dv = getDisplayVolume(c);
|
|
2523
|
+
if (!Number.isFinite(dv)) {
|
|
2524
|
+
volumeHoverTip.style.display = 'none';
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
const barX = u.valToPos(u.data[0][idx], 'x', true);
|
|
2528
|
+
volumeHoverTip.style.display = 'block';
|
|
2529
|
+
volumeHoverTip.textContent = 'Vol ' + fmtVolumeWithSym(dv);
|
|
2530
|
+
volumeHoverTip.style.left = Math.max(2, barX - volumeHoverTip.offsetWidth / 2) + 'px';
|
|
2531
|
+
// Pinned to the panel top — never covers the bars
|
|
2532
|
+
volumeHoverTip.style.top = '2px';
|
|
1634
2533
|
}
|
|
1635
2534
|
function currentYRange(chart) {
|
|
1636
2535
|
const s = chart.scales.y || {};
|
|
@@ -1657,12 +2556,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1657
2556
|
priceBounds = null;
|
|
1658
2557
|
return;
|
|
1659
2558
|
}
|
|
1660
|
-
const range = max - min;
|
|
1661
2559
|
// Uloszoomauksen rajat: alin naytettava hinta voi painua viimeistaan
|
|
1662
|
-
// ~
|
|
1663
|
-
//
|
|
1664
|
-
// ei siis paase "lipsahtamaan" candleita nakyvasta
|
|
1665
|
-
|
|
2560
|
+
// ~1/2 datan minimin alapuolelle, ylin ~2x datan maksimin —
|
|
2561
|
+
// paneminen kayttaa tata 2x-bandia ja pysahtyy jaykkana sen
|
|
2562
|
+
// reunaan (ei siis paase "lipsahtamaan" candleita nakyvasta
|
|
2563
|
+
// kadottaen). Kokonaisvalille ei ole ylarajaa: pystyakselia saa
|
|
2564
|
+
// zoomata ulos vapaasti.
|
|
2565
|
+
priceBounds = { min: min, max: max, floor: min / 2, ceil: max * 2.0 };
|
|
1666
2566
|
}
|
|
1667
2567
|
function applyYRange(chart, min, max) {
|
|
1668
2568
|
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return false;
|
|
@@ -1673,18 +2573,37 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1673
2573
|
if (max <= min) return false;
|
|
1674
2574
|
}
|
|
1675
2575
|
if (priceBounds) {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2576
|
+
// Span-locked slide within the 2x zoom band: a pan that would
|
|
2577
|
+
// cross the zoom-out limits stops as a rigid block instead of
|
|
2578
|
+
// getting squashed (squashing reads as scaling halfway through
|
|
2579
|
+
// a drag). Only a span wider than the band itself snaps to the
|
|
2580
|
+
// full band. Scaling lives on the axis gutters alone.
|
|
2581
|
+
if (currentPriceScale === 'log') {
|
|
2582
|
+
const spanRatio = max / min;
|
|
2583
|
+
if (spanRatio > priceBounds.ceil / priceBounds.floor) {
|
|
2584
|
+
min = priceBounds.floor;
|
|
2585
|
+
max = priceBounds.ceil;
|
|
2586
|
+
} else {
|
|
2587
|
+
if (max > priceBounds.ceil) {
|
|
2588
|
+
const f = max / priceBounds.ceil;
|
|
2589
|
+
min /= f; max /= f;
|
|
2590
|
+
}
|
|
2591
|
+
if (min < priceBounds.floor) {
|
|
2592
|
+
const f = priceBounds.floor / min;
|
|
2593
|
+
min *= f; max *= f;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
} else {
|
|
2597
|
+
const span = max - min;
|
|
2598
|
+
if (span > priceBounds.ceil - priceBounds.floor) {
|
|
2599
|
+
min = priceBounds.floor;
|
|
2600
|
+
max = priceBounds.ceil;
|
|
2601
|
+
} else {
|
|
2602
|
+
if (max > priceBounds.ceil) { max = priceBounds.ceil; min = max - span; }
|
|
2603
|
+
if (min < priceBounds.floor) { min = priceBounds.floor; max = min + span; }
|
|
2604
|
+
}
|
|
1684
2605
|
}
|
|
1685
|
-
if (
|
|
1686
|
-
min = nMin;
|
|
1687
|
-
max = nMax;
|
|
2606
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return false;
|
|
1688
2607
|
}
|
|
1689
2608
|
manualYRange = { min: min, max: max };
|
|
1690
2609
|
chart.setScale('y', { min: min, max: max });
|
|
@@ -1743,6 +2662,56 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1743
2662
|
window.addEventListener('mouseup', endDrag, { once: true });
|
|
1744
2663
|
}, true);
|
|
1745
2664
|
}
|
|
2665
|
+
// Time-axis drag scales the timeframe around its center (mirror of
|
|
2666
|
+
// the price-axis drag): drag right to zoom out, left to zoom in.
|
|
2667
|
+
// Synced across charts via syncXRange; capture phase pre-empts plot pan.
|
|
2668
|
+
function bindXAxisDrag(chart) {
|
|
2669
|
+
let dragging = false;
|
|
2670
|
+
let startClientX = 0;
|
|
2671
|
+
let startRange = null;
|
|
2672
|
+
const onMove = (e) => {
|
|
2673
|
+
if (!dragging || !startRange) return;
|
|
2674
|
+
e.preventDefault();
|
|
2675
|
+
chart.root.style.cursor = 'ew-resize';
|
|
2676
|
+
const deltaPx = e.clientX - startClientX;
|
|
2677
|
+
if (!Number.isFinite(deltaPx) || deltaPx === 0) return;
|
|
2678
|
+
const factor = Math.exp(deltaPx / 350);
|
|
2679
|
+
const span = startRange.max - startRange.min;
|
|
2680
|
+
if (!Number.isFinite(span) || span <= 0) return;
|
|
2681
|
+
const center = (startRange.min + startRange.max) / 2;
|
|
2682
|
+
const nextSpan = span * factor;
|
|
2683
|
+
syncXRange(center - nextSpan / 2, center + nextSpan / 2);
|
|
2684
|
+
};
|
|
2685
|
+
const endDrag = () => {
|
|
2686
|
+
if (!dragging) return;
|
|
2687
|
+
dragging = false;
|
|
2688
|
+
startRange = null;
|
|
2689
|
+
chart.root.style.cursor = '';
|
|
2690
|
+
window.removeEventListener('mousemove', onMove);
|
|
2691
|
+
window.removeEventListener('mouseup', endDrag);
|
|
2692
|
+
};
|
|
2693
|
+
chart.root.addEventListener('mousedown', (e) => {
|
|
2694
|
+
if (!e || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
2695
|
+
// capture phase so we pre-empt the plot pan when in the x-zone
|
|
2696
|
+
const rect = chart.root.getBoundingClientRect();
|
|
2697
|
+
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
|
|
2698
|
+
if (!inXAxisZone(chart, e.clientY)) return;
|
|
2699
|
+
const s = chart.scales.x || {};
|
|
2700
|
+
const range = {
|
|
2701
|
+
min: Number.isFinite(s.min) ? s.min : xMin,
|
|
2702
|
+
max: Number.isFinite(s.max) ? s.max : xMax,
|
|
2703
|
+
};
|
|
2704
|
+
if (!Number.isFinite(range.min) || !Number.isFinite(range.max) || range.max <= range.min) return;
|
|
2705
|
+
e.preventDefault();
|
|
2706
|
+
e.stopPropagation();
|
|
2707
|
+
dragging = true;
|
|
2708
|
+
startClientX = e.clientX;
|
|
2709
|
+
startRange = range;
|
|
2710
|
+
chart.root.style.cursor = 'ew-resize';
|
|
2711
|
+
window.addEventListener('mousemove', onMove);
|
|
2712
|
+
window.addEventListener('mouseup', endDrag, { once: true });
|
|
2713
|
+
}, true);
|
|
2714
|
+
}
|
|
1746
2715
|
function bindYAxisReset(chart) {
|
|
1747
2716
|
chart.root.addEventListener('dblclick', (e) => {
|
|
1748
2717
|
if (chart !== priceChart) return;
|
|
@@ -1757,6 +2726,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1757
2726
|
function setControls() {
|
|
1758
2727
|
setActivePairMode(currentPairMode);
|
|
1759
2728
|
setActivePriceScale(currentPriceScale);
|
|
2729
|
+
const ordersToggle = document.getElementById('orders-toggle');
|
|
2730
|
+
if (ordersToggle) ordersToggle.checked = currentOrdersVisible;
|
|
2731
|
+
const volumeToggleEl = document.getElementById('volume-toggle');
|
|
2732
|
+
if (volumeToggleEl) volumeToggleEl.checked = currentVolumeVisible;
|
|
2733
|
+
setActiveVolumeUnit();
|
|
2734
|
+
markActiveAmaPreset();
|
|
1760
2735
|
document.getElementById('sma-toggle').checked = currentSmaEnabled;
|
|
1761
2736
|
document.getElementById('sma-period').value = String(currentSmaPeriod);
|
|
1762
2737
|
document.getElementById('ama-toggle').checked = currentAmaEnabled;
|
|
@@ -1765,6 +2740,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1765
2740
|
document.getElementById('ama-slow').value = currentAmaSlowPeriod.toFixed(1);
|
|
1766
2741
|
document.getElementById('vwap-toggle').checked = currentVwapEnabled;
|
|
1767
2742
|
document.getElementById('vwap-bars').value = String(currentVwapBars);
|
|
2743
|
+
document.getElementById('range-toggle').checked = currentRangeEnabled;
|
|
2744
|
+
document.getElementById('range-scale-toggle').checked = currentRangeScaleEnabled;
|
|
2745
|
+
document.getElementById('range-span').value = Number(currentRangeSpan).toFixed(2);
|
|
2746
|
+
document.getElementById('range-span-val').textContent = Number(currentRangeSpan).toFixed(2) + 'x';
|
|
2747
|
+
document.getElementById('range-grid-wrap').style.display = 'inline';
|
|
1768
2748
|
document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
|
|
1769
2749
|
document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
|
|
1770
2750
|
document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
|
|
@@ -1778,7 +2758,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1778
2758
|
if (needsRebuild) {
|
|
1779
2759
|
manualYRange = null;
|
|
1780
2760
|
priceMarkerLabel = null;
|
|
1781
|
-
|
|
2761
|
+
resetOverlayNodes();
|
|
2762
|
+
volumeHoverTip = null;
|
|
1782
2763
|
if (priceEl) priceEl.innerHTML = '';
|
|
1783
2764
|
if (volumeEl) volumeEl.innerHTML = '';
|
|
1784
2765
|
priceChart = null;
|
|
@@ -1809,16 +2790,53 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1809
2790
|
charts.forEach((chart) => {
|
|
1810
2791
|
bindWheelZoom(chart);
|
|
1811
2792
|
bindPan(chart);
|
|
2793
|
+
bindXAxisDrag(chart);
|
|
1812
2794
|
if (chart === priceChart) {
|
|
1813
2795
|
bindYAxisDrag(chart);
|
|
1814
2796
|
bindYAxisReset(chart);
|
|
1815
2797
|
chart.root.addEventListener('mousemove', (e) => {
|
|
1816
|
-
chart.root.style.cursor = inYAxisZone(chart, e.clientX) ? 'ns-resize' : '';
|
|
2798
|
+
chart.root.style.cursor = inYAxisZone(chart, e.clientX) ? 'ns-resize' : (inXAxisZone(chart, e.clientY) ? 'ew-resize' : '');
|
|
2799
|
+
});
|
|
2800
|
+
// Cursor price tag: floating label next to the mouse
|
|
2801
|
+
// showing the price under the cursor (price chart only).
|
|
2802
|
+
let cursorPriceTag = null;
|
|
2803
|
+
const ensureCursorPriceTag = () => {
|
|
2804
|
+
if (!cursorPriceTag || cursorPriceTag.parentNode !== chart.root) {
|
|
2805
|
+
if (cursorPriceTag && cursorPriceTag.parentNode) cursorPriceTag.parentNode.removeChild(cursorPriceTag);
|
|
2806
|
+
cursorPriceTag = document.createElement('div');
|
|
2807
|
+
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;';
|
|
2808
|
+
chart.root.appendChild(cursorPriceTag);
|
|
2809
|
+
}
|
|
2810
|
+
return cursorPriceTag;
|
|
2811
|
+
};
|
|
2812
|
+
chart.over.addEventListener('mousemove', (e) => {
|
|
2813
|
+
const tag = ensureCursorPriceTag();
|
|
2814
|
+
const rect = chart.root.getBoundingClientRect();
|
|
2815
|
+
const overRect = chart.over.getBoundingClientRect();
|
|
2816
|
+
const price = chart.posToVal(e.clientY - overRect.top, 'y');
|
|
2817
|
+
if (!Number.isFinite(price)) { tag.style.display = 'none'; return; }
|
|
2818
|
+
tag.style.display = 'block';
|
|
2819
|
+
tag.textContent = fmtPriceLabel(price);
|
|
2820
|
+
let x = e.clientX - rect.left + 14;
|
|
2821
|
+
const y = e.clientY - rect.top - 8;
|
|
2822
|
+
const maxX = rect.width - tag.offsetWidth - 6;
|
|
2823
|
+
if (x > maxX) x = e.clientX - rect.left - tag.offsetWidth - 14;
|
|
2824
|
+
tag.style.left = Math.max(2, x) + 'px';
|
|
2825
|
+
tag.style.top = Math.max(2, y) + 'px';
|
|
2826
|
+
});
|
|
2827
|
+
chart.over.addEventListener('mouseleave', () => {
|
|
2828
|
+
if (cursorPriceTag) cursorPriceTag.style.display = 'none';
|
|
1817
2829
|
});
|
|
1818
2830
|
}
|
|
1819
2831
|
chart.root.addEventListener('mousemove', () => {
|
|
1820
2832
|
if (chart.cursor.idx != null) updateLegend(chart.cursor.idx);
|
|
1821
2833
|
});
|
|
2834
|
+
if (chart === volumeChart) {
|
|
2835
|
+
chart.over.addEventListener('mousemove', () => showVolumeHover(chart));
|
|
2836
|
+
chart.over.addEventListener('mouseleave', () => {
|
|
2837
|
+
if (volumeHoverTip) volumeHoverTip.style.display = 'none';
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
1822
2840
|
chart.root.addEventListener('mouseleave', refreshLegend);
|
|
1823
2841
|
chart.root.addEventListener('mouseenter', () => chart.root.classList.add('is-hovered'));
|
|
1824
2842
|
chart.root.addEventListener('mouseleave', () => chart.root.classList.remove('is-hovered'));
|
|
@@ -1826,16 +2844,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1826
2844
|
chartEventsBound = true;
|
|
1827
2845
|
}
|
|
1828
2846
|
if (oldRange && Number.isFinite(oldRange.min) && Number.isFinite(oldRange.max)) {
|
|
1829
|
-
const next =
|
|
2847
|
+
const next = clampRange(oldRange.min, oldRange.max);
|
|
1830
2848
|
if (next) charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
1831
2849
|
}
|
|
1832
2850
|
refreshLegend();
|
|
2851
|
+
renderMarketPanel();
|
|
2852
|
+
renderRangePanel();
|
|
2853
|
+
renderVolumePanel();
|
|
1833
2854
|
saveState();
|
|
1834
2855
|
}
|
|
1835
2856
|
|
|
1836
2857
|
setControls();
|
|
1837
2858
|
rerender(false);
|
|
1838
|
-
|
|
1839
2859
|
document.querySelectorAll('.time-btn').forEach((btn) => {
|
|
1840
2860
|
btn.addEventListener('click', () => {
|
|
1841
2861
|
if (btn.disabled) return;
|
|
@@ -1846,7 +2866,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1846
2866
|
rerender(true);
|
|
1847
2867
|
});
|
|
1848
2868
|
});
|
|
1849
|
-
|
|
1850
2869
|
const scaleToggle = document.getElementById('scale-toggle');
|
|
1851
2870
|
if (scaleToggle) {
|
|
1852
2871
|
scaleToggle.addEventListener('click', () => {
|
|
@@ -1860,10 +2879,62 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1860
2879
|
pairToggle.addEventListener('click', () => {
|
|
1861
2880
|
currentPairMode = currentPairMode === 'inverse' ? 'normal' : 'inverse';
|
|
1862
2881
|
manualYRange = null;
|
|
2882
|
+
lastOverlayKey = '';
|
|
1863
2883
|
setControls();
|
|
1864
2884
|
rerender(true);
|
|
2885
|
+
padXRight();
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
const ordersToggle = document.getElementById('orders-toggle');
|
|
2889
|
+
if (ordersToggle) {
|
|
2890
|
+
ordersToggle.addEventListener('change', () => {
|
|
2891
|
+
currentOrdersVisible = ordersToggle.checked;
|
|
2892
|
+
lastOverlayKey = '';
|
|
2893
|
+
if (!currentOrdersVisible && priceChart) hideOverlayNodes();
|
|
2894
|
+
setControls();
|
|
2895
|
+
saveState();
|
|
2896
|
+
if (priceChart) {
|
|
2897
|
+
positionReserveLine(priceChart);
|
|
2898
|
+
positionOrderLines(priceChart);
|
|
2899
|
+
}
|
|
2900
|
+
renderMarketPanel();
|
|
1865
2901
|
});
|
|
1866
2902
|
}
|
|
2903
|
+
// Volume toggle: hide the volume chart so flex yields the space to
|
|
2904
|
+
// the price chart (larger). No rerender, resize only.
|
|
2905
|
+
const applyVolumeVisibility = () => {
|
|
2906
|
+
if (!volumeEl || !priceChart) return;
|
|
2907
|
+
volumeEl.style.display = currentVolumeVisible ? '' : 'none';
|
|
2908
|
+
requestAnimationFrame(() => {
|
|
2909
|
+
if (!priceChart) return;
|
|
2910
|
+
priceChart.setSize({ width: priceEl.clientWidth, height: priceEl.clientHeight });
|
|
2911
|
+
if (currentVolumeVisible && volumeChart && volumeEl.clientWidth > 0) {
|
|
2912
|
+
volumeChart.setSize({ width: volumeEl.clientWidth, height: volumeEl.clientHeight });
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
};
|
|
2916
|
+
const volumeToggleInit = document.getElementById('volume-toggle');
|
|
2917
|
+
if (volumeToggleInit) {
|
|
2918
|
+
volumeToggleInit.checked = currentVolumeVisible;
|
|
2919
|
+
volumeToggleInit.addEventListener('change', () => {
|
|
2920
|
+
currentVolumeVisible = volumeToggleInit.checked;
|
|
2921
|
+
saveState();
|
|
2922
|
+
applyVolumeVisibility();
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
const volumeUnitToggle = document.getElementById('volume-unit-toggle');
|
|
2926
|
+
if (volumeUnitToggle) {
|
|
2927
|
+
volumeUnitToggle.addEventListener('click', () => toggleVolumeUnit());
|
|
2928
|
+
}
|
|
2929
|
+
const legendVolume = document.getElementById('legend-volume');
|
|
2930
|
+
if (legendVolume) {
|
|
2931
|
+
legendVolume.addEventListener('click', () => toggleVolumeUnit());
|
|
2932
|
+
}
|
|
2933
|
+
// Initial state on load (previously stored in localStorage)
|
|
2934
|
+
applyVolumeVisibility();
|
|
2935
|
+
document.querySelectorAll('.ama-preset-btn').forEach((b) => {
|
|
2936
|
+
b.addEventListener('click', () => applyAmaPreset(b.dataset.amaPreset));
|
|
2937
|
+
});
|
|
1867
2938
|
|
|
1868
2939
|
const syncInputs = () => {
|
|
1869
2940
|
currentSmaEnabled = document.getElementById('sma-toggle').checked;
|
|
@@ -1879,7 +2950,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1879
2950
|
document.getElementById('ama-init-offset-val').textContent = currentAmaInitOffset + '%';
|
|
1880
2951
|
currentVwapEnabled = document.getElementById('vwap-toggle').checked;
|
|
1881
2952
|
currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
|
|
2953
|
+
currentRangeEnabled = document.getElementById('range-toggle').checked;
|
|
2954
|
+
currentRangeScaleEnabled = document.getElementById('range-scale-toggle').checked;
|
|
2955
|
+
currentRangeSpan = Math.min(2, Math.max(1.2, Math.round((Number(document.getElementById('range-span').value) || 1.55) * 20) / 20));
|
|
2956
|
+
const ordersEl = document.getElementById('orders-toggle');
|
|
2957
|
+
if (ordersEl) currentOrdersVisible = ordersEl.checked;
|
|
1882
2958
|
setControls();
|
|
2959
|
+
markActiveAmaPreset();
|
|
1883
2960
|
rerender(false);
|
|
1884
2961
|
};
|
|
1885
2962
|
|
|
@@ -1917,6 +2994,25 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1917
2994
|
rerender(false);
|
|
1918
2995
|
});
|
|
1919
2996
|
document.getElementById('ama-init-offset-toggle').addEventListener('change', syncInputs);
|
|
2997
|
+
document.getElementById('range-toggle').addEventListener('change', () => {
|
|
2998
|
+
const nextEnabled = document.getElementById('range-toggle').checked;
|
|
2999
|
+
if (currentRangeEnabled === nextEnabled) return;
|
|
3000
|
+
currentRangeEnabled = nextEnabled;
|
|
3001
|
+
setControls();
|
|
3002
|
+
if (!currentRangeEnabled) {
|
|
3003
|
+
hideRangeBandImmediate();
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
rerender(false);
|
|
3007
|
+
});
|
|
3008
|
+
document.getElementById('range-scale-toggle').addEventListener('change', () => {
|
|
3009
|
+
const nextEnabled = document.getElementById('range-scale-toggle').checked;
|
|
3010
|
+
if (currentRangeScaleEnabled === nextEnabled) return;
|
|
3011
|
+
currentRangeScaleEnabled = nextEnabled;
|
|
3012
|
+
manualYRange = null;
|
|
3013
|
+
setControls();
|
|
3014
|
+
rerender(false);
|
|
3015
|
+
});
|
|
1920
3016
|
['sma-period', 'ama-er', 'ama-fast', 'ama-slow', 'vwap-bars'].forEach((id) => {
|
|
1921
3017
|
document.getElementById(id).addEventListener('change', syncInputs);
|
|
1922
3018
|
document.getElementById(id).addEventListener('blur', syncInputs);
|
|
@@ -1960,7 +3056,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1960
3056
|
makeStepper('ama-er', 1, 1, 0);
|
|
1961
3057
|
makeStepper('ama-fast', 0.1, 0.1, 1);
|
|
1962
3058
|
makeStepper('ama-slow', 1, 0.1, 1);
|
|
1963
|
-
document.getElementById('ama-reset').addEventListener('click', resetAmaDefaults);
|
|
1964
3059
|
document.getElementById('ama-init-offset').addEventListener('input', () => {
|
|
1965
3060
|
const input = document.getElementById('ama-init-offset');
|
|
1966
3061
|
const snapped = snapInitOffset(input.value);
|
|
@@ -1968,15 +3063,187 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1968
3063
|
document.getElementById('ama-init-offset-val').textContent = snapped + '%';
|
|
1969
3064
|
syncInputs();
|
|
1970
3065
|
});
|
|
3066
|
+
document.getElementById('range-span').addEventListener('input', () => {
|
|
3067
|
+
const input = document.getElementById('range-span');
|
|
3068
|
+
document.getElementById('range-span-val').textContent = Number(input.value).toFixed(2) + 'x';
|
|
3069
|
+
syncInputs();
|
|
3070
|
+
});
|
|
3071
|
+
|
|
3072
|
+
// Room to the right past the last bar (~12%): for the market panel
|
|
3073
|
+
// and future candles. Applied at init and on pair flip; user
|
|
3074
|
+
// zoom/pan never trigger it, so it does not fight the user zoom.
|
|
3075
|
+
function padXRight() {
|
|
3076
|
+
if (!priceChart || !currentCandles.length) return;
|
|
3077
|
+
const first = currentCandles[0]?.time;
|
|
3078
|
+
const last = currentCandles[currentCandles.length - 1]?.time;
|
|
3079
|
+
if (!Number.isFinite(first) || !Number.isFinite(last) || last <= first) return;
|
|
3080
|
+
const span = last - first;
|
|
3081
|
+
syncXRange(first - span * 0.02, last + span * 0.12);
|
|
3082
|
+
}
|
|
3083
|
+
// Market panel top-right: SELL / Market / BUY with distance-to-market
|
|
3084
|
+
// %. Pair-aware (same display levels as the overlay). Static per
|
|
3085
|
+
// generation, so drawn once (no draw hook); refreshed on pair flip
|
|
3086
|
+
// via rerender. Hidden while the Orders overlay is unchecked — the
|
|
3087
|
+
// BUY/SELL rows are overlay data, so they go away with it.
|
|
3088
|
+
function renderMarketPanel() {
|
|
3089
|
+
if (!priceChart) return;
|
|
3090
|
+
let panel = document.getElementById('mkt-panel');
|
|
3091
|
+
if (!panel) {
|
|
3092
|
+
panel = document.createElement('div');
|
|
3093
|
+
panel.id = 'mkt-panel';
|
|
3094
|
+
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;';
|
|
3095
|
+
priceChart.root.appendChild(panel);
|
|
3096
|
+
}
|
|
3097
|
+
if (!currentOrdersVisible && (hasOrders || hasBounds)) { panel.style.display = 'none'; return; }
|
|
3098
|
+
panel.style.display = 'block';
|
|
3099
|
+
const last = currentCandles[currentCandles.length - 1];
|
|
3100
|
+
const mkt = last ? last.close : NaN;
|
|
3101
|
+
const { buys, sells } = getDisplayOrders();
|
|
3102
|
+
let html = '';
|
|
3103
|
+
if (sells.length && Number.isFinite(mkt)) {
|
|
3104
|
+
const s = Math.min(...sells);
|
|
3105
|
+
const p = (s - mkt) / mkt * 100;
|
|
3106
|
+
html += '<div style="color:#ef5350">SELL ' + fmtPriceLabel(s) + ' ' + (p >= 0 ? '+' : '') + p.toFixed(1) + '%</div>';
|
|
3107
|
+
}
|
|
3108
|
+
html += '<div style="color:#e8eef5">Market ' + (Number.isFinite(mkt) ? fmtPriceLabel(mkt) : '-') + '</div>';
|
|
3109
|
+
if (buys.length && Number.isFinite(mkt)) {
|
|
3110
|
+
const b = Math.max(...buys);
|
|
3111
|
+
const p = (b - mkt) / mkt * 100;
|
|
3112
|
+
html += '<div style="color:#26a69a">BUY ' + fmtPriceLabel(b) + ' ' + (p >= 0 ? '+' : '') + p.toFixed(1) + '%</div>';
|
|
3113
|
+
}
|
|
3114
|
+
panel.innerHTML = html;
|
|
3115
|
+
}
|
|
3116
|
+
// Range panel bottom-right: visible-window candle High (red) /
|
|
3117
|
+
// Low (green) — sell-side red on top, buy-side green below, mirroring
|
|
3118
|
+
// the top-right market badge. Volume max lives in its own badge on
|
|
3119
|
+
// the volume chart below. Slightly smaller type than the top-right market badge.
|
|
3120
|
+
// Pair-aware via currentCandles (already inverted in B/A view).
|
|
3121
|
+
// Refreshed on rerender and on x zoom/pan through the setScale hooks.
|
|
3122
|
+
function visibleCandleStats() {
|
|
3123
|
+
if (!currentCandles.length) return null;
|
|
3124
|
+
let minX = NaN;
|
|
3125
|
+
let maxX = NaN;
|
|
3126
|
+
if (priceChart) {
|
|
3127
|
+
const xs = priceChart.scales.x || {};
|
|
3128
|
+
if (Number.isFinite(xs.min)) minX = xs.min;
|
|
3129
|
+
if (Number.isFinite(xs.max)) maxX = xs.max;
|
|
3130
|
+
}
|
|
3131
|
+
if (!Number.isFinite(minX)) minX = currentCandles[0].time;
|
|
3132
|
+
if (!Number.isFinite(maxX)) maxX = currentCandles[currentCandles.length - 1].time;
|
|
3133
|
+
const times = currentCandles.map((c) => c.time);
|
|
3134
|
+
const start = Math.max(0, lowerBound(times, minX) - 1);
|
|
3135
|
+
const end = Math.min(times.length, lowerBound(times, maxX) + 2);
|
|
3136
|
+
let hi = -Infinity;
|
|
3137
|
+
let lo = Infinity;
|
|
3138
|
+
let vol = 0;
|
|
3139
|
+
for (let i = start; i < end; i++) {
|
|
3140
|
+
const c = currentCandles[i];
|
|
3141
|
+
if (!c) continue;
|
|
3142
|
+
if (Number.isFinite(c.high) && c.high > hi) hi = c.high;
|
|
3143
|
+
if (Number.isFinite(c.low) && c.low < lo) lo = c.low;
|
|
3144
|
+
const dv = getDisplayVolume(c);
|
|
3145
|
+
if (Number.isFinite(dv) && dv > vol) vol = dv;
|
|
3146
|
+
}
|
|
3147
|
+
if (!Number.isFinite(hi) || !Number.isFinite(lo)) return null;
|
|
3148
|
+
return { hi, lo, vol };
|
|
3149
|
+
}
|
|
3150
|
+
function renderRangePanel() {
|
|
3151
|
+
if (!priceChart) return;
|
|
3152
|
+
let panel = document.getElementById('range-panel');
|
|
3153
|
+
if (!panel) {
|
|
3154
|
+
panel = document.createElement('div');
|
|
3155
|
+
panel.id = 'range-panel';
|
|
3156
|
+
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;';
|
|
3157
|
+
priceChart.root.appendChild(panel);
|
|
3158
|
+
}
|
|
3159
|
+
const stats = visibleCandleStats();
|
|
3160
|
+
if (!stats) { panel.style.display = 'none'; return; }
|
|
3161
|
+
panel.style.display = 'block';
|
|
3162
|
+
panel.innerHTML = '<div style="color:#ef5350">H ' + fmtPriceLabel(stats.hi) + '</div>'
|
|
3163
|
+
+ '<div style="color:#26a69a">L ' + fmtPriceLabel(stats.lo) + '</div>';
|
|
3164
|
+
}
|
|
3165
|
+
// Volume badge top-right on the volume chart: max volume of the
|
|
3166
|
+
// visible window only. Same small type as the range panel. Lives on
|
|
3167
|
+
// the volume root, so hiding the volume chart hides the badge too.
|
|
3168
|
+
function renderVolumePanel() {
|
|
3169
|
+
if (!volumeChart) return;
|
|
3170
|
+
let panel = document.getElementById('vol-panel');
|
|
3171
|
+
if (!panel) {
|
|
3172
|
+
panel = document.createElement('div');
|
|
3173
|
+
panel.id = 'vol-panel';
|
|
3174
|
+
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;';
|
|
3175
|
+
panel.addEventListener('click', () => toggleVolumeUnit());
|
|
3176
|
+
volumeChart.root.appendChild(panel);
|
|
3177
|
+
}
|
|
3178
|
+
panel.style.cursor = payload.volumeIsCount ? 'default' : 'pointer';
|
|
3179
|
+
panel.title = payload.volumeIsCount
|
|
3180
|
+
? 'Volume: feed publish count per bucket'
|
|
3181
|
+
: 'Volume units — click to switch base/quote';
|
|
3182
|
+
const stats = visibleCandleStats();
|
|
3183
|
+
if (!stats) { panel.style.display = 'none'; return; }
|
|
3184
|
+
panel.style.display = 'block';
|
|
3185
|
+
panel.innerHTML = '<div style="color:#e8eef5">max ' + fmtVolumeWithSym(stats.vol) + '</div>';
|
|
3186
|
+
}
|
|
3187
|
+
function scheduleStatPanels() {
|
|
3188
|
+
if (rangePanelRaf) return;
|
|
3189
|
+
rangePanelRaf = requestAnimationFrame(() => {
|
|
3190
|
+
rangePanelRaf = 0;
|
|
3191
|
+
renderRangePanel();
|
|
3192
|
+
renderVolumePanel();
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
// uPlot does not recompute an auto Y scale when X moves via
|
|
3196
|
+
// setScale('x') — the scale range fns only run on init/data
|
|
3197
|
+
// change. Refit Y to the visible window after every timeframe
|
|
3198
|
+
// move so candles/volume follow the pan/zoom instead of getting
|
|
3199
|
+
// stuck outside the view. A user-locked price range (axis
|
|
3200
|
+
// wheel/drag → manualYRange) is respected; double-click the
|
|
3201
|
+
// price axis to return to autofit. y-keyed setScale calls never
|
|
3202
|
+
// schedule a refit, so this cannot loop with itself.
|
|
3203
|
+
function yRangeDirty(u, vis) {
|
|
3204
|
+
const s = u.scales.y || {};
|
|
3205
|
+
if (!Number.isFinite(s.min) || !Number.isFinite(s.max)) return true;
|
|
3206
|
+
const span = Math.max(1e-12, Math.abs(vis[1] - vis[0]));
|
|
3207
|
+
return Math.abs(s.min - vis[0]) / span > 1e-9 || Math.abs(s.max - vis[1]) / span > 1e-9;
|
|
3208
|
+
}
|
|
3209
|
+
function scheduleYRefit() {
|
|
3210
|
+
if (yRefitRaf) return;
|
|
3211
|
+
yRefitRaf = requestAnimationFrame(() => {
|
|
3212
|
+
yRefitRaf = 0;
|
|
3213
|
+
if (priceChart && !manualYRange) {
|
|
3214
|
+
const vis = visiblePriceRange(priceChart);
|
|
3215
|
+
if (vis && yRangeDirty(priceChart, vis)) priceChart.setScale('y', { min: vis[0], max: vis[1] });
|
|
3216
|
+
}
|
|
3217
|
+
if (volumeChart) {
|
|
3218
|
+
const vvis = visibleVolumeRange(volumeChart);
|
|
3219
|
+
if (vvis && yRangeDirty(volumeChart, vvis)) volumeChart.setScale('y', { min: vvis[0], max: vvis[1] });
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
padXRight();
|
|
3224
|
+
renderMarketPanel();
|
|
3225
|
+
renderRangePanel();
|
|
3226
|
+
renderVolumePanel();
|
|
1971
3227
|
|
|
1972
3228
|
window.addEventListener('resize', () => {
|
|
1973
3229
|
if (!charts.length) return;
|
|
1974
3230
|
charts.forEach((chart) => {
|
|
1975
3231
|
const el = chart === priceChart ? priceEl : volumeEl;
|
|
3232
|
+
if (el.clientWidth <= 0 || el.clientHeight <= 0) return;
|
|
1976
3233
|
chart.setSize({ width: el.clientWidth, height: el.clientHeight });
|
|
1977
3234
|
});
|
|
1978
3235
|
});
|
|
1979
3236
|
|
|
3237
|
+
window.addEventListener('keydown', (e) => {
|
|
3238
|
+
const tag = (document.activeElement && document.activeElement.tagName) || '';
|
|
3239
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
3240
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '0' && charts.length) {
|
|
3241
|
+
const first = currentCandles[0]?.time;
|
|
3242
|
+
const last = currentCandles[currentCandles.length - 1]?.time;
|
|
3243
|
+
if (Number.isFinite(first) && Number.isFinite(last)) syncXRange(first, last);
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
|
|
1980
3247
|
${zoomResetScript()}
|
|
1981
3248
|
})();
|
|
1982
3249
|
</script>
|
|
@@ -1984,5 +3251,5 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1984
3251
|
</html>`;
|
|
1985
3252
|
}
|
|
1986
3253
|
|
|
1987
|
-
export { generateHTML, normalizeCandle, loadMarketProfiles }
|
|
3254
|
+
export { generateHTML, normalizeCandle, loadMarketProfiles, resolveChartStorageKey, sanitizeStorageComponent, TRADINGVIEW_PREFS_KEY_PREFIX, TRADINGVIEW_SYNC_KEY }
|
|
1988
3255
|
|