dexbot 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +4 -3
- package/analysis/README.md +1 -2
- package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
- package/analysis/resolve_source.ts +9 -2
- package/analysis/tradingview/README.md +29 -11
- package/analysis/tradingview/analyze_tradingview.ts +92 -3
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
- package/analysis/trend_detection/package.json +1 -1
- package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
- package/claw/modules/dexbot_profiles.ts +22 -2
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/claw/tests/test_dexbot_profiles.ts +65 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +6 -2
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.js +3 -3
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/candle_utils.d.ts +6 -0
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +6 -0
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +101 -19
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +2 -0
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +31 -4
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +0 -6
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
- package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
- package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +10 -5
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
- package/dist/market_adapter/inputs/window_cache.js +500 -0
- package/dist/market_adapter/inputs/window_cache.js.map +1 -0
- package/dist/market_adapter/interval_utils.d.ts +8 -1
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +14 -1
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -0
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +21 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +25 -4
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +67 -4
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +3 -1
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +13 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +2 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +122 -28
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +59 -8
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +11 -0
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +68 -4
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +8 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +57 -2
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +157 -28
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +135 -5
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +554 -46
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +13 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +15 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +36 -7
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/fund_registry.d.ts +1 -1
- package/dist/modules/fund_registry.js +1 -1
- package/dist/modules/node_connect_policy.d.ts +52 -0
- package/dist/modules/node_connect_policy.d.ts.map +1 -0
- package/dist/modules/node_connect_policy.js +35 -0
- package/dist/modules/node_connect_policy.js.map +1 -0
- package/dist/modules/node_manager.d.ts +32 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +76 -9
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +30 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +176 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -0
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +143 -15
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +5 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +182 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +64 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +231 -14
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -0
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +79 -7
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +16 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +11 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +24 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +258 -13
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +662 -12
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +36 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +102 -6
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +6 -2
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +13 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +20 -0
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +16 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +31 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/pm2.js +7 -7
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +13 -3
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/dist/scripts/analyze-orders.js +2 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.js +0 -1
- package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/tv.d.ts +24 -2
- package/dist/scripts/tv.d.ts.map +1 -1
- package/dist/scripts/tv.js +190 -65
- package/dist/scripts/tv.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -2
- package/docs/COW_INVARIANTS.md +18 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +8 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
- package/docs/GRID_RECALCULATION.md +5 -6
- package/docs/GRID_RECONCILE.md +12 -2
- package/docs/MPA_CREDIT_USAGE.md +2 -2
- package/docs/README.md +5 -6
- package/docs/architecture.md +0 -58
- package/docs/developer_guide.md +2 -1
- package/docs/docker.md +1 -1
- package/market_adapter/README.md +4 -0
- package/modules/README.md +1 -1
- package/package.json +1 -1
- package/scripts/README.md +5 -3
- package/tests/README.md +2 -2
|
@@ -11,7 +11,7 @@ import { getStorage } from '../../modules/storage/index.js';
|
|
|
11
11
|
const { readJSON } = getStorage();
|
|
12
12
|
import { getErrorMessage } from '../../modules/utils/errors.js';
|
|
13
13
|
import { parseRelativeMultiplier } from '../../modules/order/utils/math.js';
|
|
14
|
-
import { computeAverageAmaSlopePct } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
14
|
+
import { computeAverageAmaSlopePct, computeAmaSlopeClipThreshold } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
|
|
15
15
|
import {
|
|
16
16
|
resolveBaseBounds,
|
|
17
17
|
computeAsymmetricBoundsMetrics,
|
|
@@ -88,6 +88,27 @@ function resolveAmaDefaults({ meta, data, marketProfiles }: any = {}) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
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
|
+
|
|
91
112
|
function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
92
113
|
const rawCandles = Array.isArray(data.candles) ? data.candles : [];
|
|
93
114
|
const candles = rawCandles.map(normalizeCandle).filter(Boolean);
|
|
@@ -103,6 +124,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
103
124
|
{ label: '4h', seconds: 14400 },
|
|
104
125
|
{ label: '1d', seconds: 86400 },
|
|
105
126
|
{ label: '1w', seconds: 604800 },
|
|
127
|
+
{ label: '1M', seconds: 2592000, calendar: 'month' },
|
|
106
128
|
].map((item: any) => ({ ...item, enabled: item.seconds >= baseIntervalSeconds }));
|
|
107
129
|
|
|
108
130
|
const defaultTimeframe = timeframes.find((item: any) => item.label === data.defaultTimeframe && item.enabled)
|
|
@@ -118,18 +140,19 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
118
140
|
maxSlopeOffset: MARKET_ADAPTER.DYNAMIC_WEIGHT_ASYMMETRIC_OFFSET_CLAMP,
|
|
119
141
|
maxAsymmetryFactor: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR,
|
|
120
142
|
minScaleSlots: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS,
|
|
143
|
+
clipPercentile: MARKET_ADAPTER.DYNAMIC_WEIGHT_CLIP_PERCENTILE,
|
|
121
144
|
};
|
|
122
145
|
function defaultRangeSpan(input: { rangeSpan?: unknown; grid?: { minPrice?: unknown; maxPrice?: unknown } | null }): number {
|
|
123
146
|
const cli = Number(input.rangeSpan);
|
|
124
|
-
if (Number.isFinite(cli)) return Math.min(2, Math.max(1.
|
|
147
|
+
if (Number.isFinite(cli)) return Math.min(2, Math.max(1.2, cli));
|
|
125
148
|
const down = parseRelativeMultiplier(input.grid?.minPrice);
|
|
126
149
|
const up = parseRelativeMultiplier(input.grid?.maxPrice);
|
|
127
|
-
if (down != null && down > 1 && up != null && up > 1) return Math.min(2, Math.max(1.
|
|
150
|
+
if (down != null && down > 1 && up != null && up > 1) return Math.min(2, Math.max(1.2, (down + up) / 2));
|
|
128
151
|
const vd = Number(input.grid?.minPrice);
|
|
129
152
|
const vu = Number(input.grid?.maxPrice);
|
|
130
153
|
if (Number.isFinite(vd) && Number.isFinite(vu) && vd > 0 && vu > vd) {
|
|
131
154
|
const ref = Math.sqrt(vd * vu);
|
|
132
|
-
return Math.min(2, Math.max(1.
|
|
155
|
+
return Math.min(2, Math.max(1.2, (ref / vd + vu / ref) / 2));
|
|
133
156
|
}
|
|
134
157
|
return 1.55;
|
|
135
158
|
}
|
|
@@ -141,7 +164,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
141
164
|
vwapEnabled: data.vwapEnabled === true,
|
|
142
165
|
vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
|
|
143
166
|
priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
|
|
144
|
-
rangeEnabled: data.rangeEnabled
|
|
167
|
+
rangeEnabled: data.rangeEnabled === true,
|
|
145
168
|
rangeScaleEnabled: data.rangeScaleEnabled === true,
|
|
146
169
|
rangeWidthPct: Number.isFinite(Number(data.rangeWidthPct)) && Number(data.rangeWidthPct) > 0
|
|
147
170
|
? Number(data.rangeWidthPct)
|
|
@@ -167,8 +190,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
167
190
|
? `${Math.round(baseIntervalSeconds / 86400)}d`
|
|
168
191
|
: `${Math.round(baseIntervalSeconds / 3600)}h`;
|
|
169
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);
|
|
170
196
|
|
|
171
197
|
const payload = {
|
|
198
|
+
storageKey,
|
|
199
|
+
syncKey: TRADINGVIEW_SYNC_KEY,
|
|
172
200
|
candles,
|
|
173
201
|
timeframes,
|
|
174
202
|
defaultTimeframe: defaultTimeframe.label,
|
|
@@ -184,6 +212,14 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
184
212
|
rangeWidthPct: defaults.rangeWidthPct,
|
|
185
213
|
rangeSpan: defaults.rangeSpan,
|
|
186
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,
|
|
187
223
|
rangeSlope,
|
|
188
224
|
defaultPairMode,
|
|
189
225
|
assetLabelA,
|
|
@@ -193,6 +229,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
193
229
|
poolLabel,
|
|
194
230
|
intervalLabel,
|
|
195
231
|
amaDefaultsSource: marketProfiles ? 'market_profiles' : 'constants',
|
|
232
|
+
volumeIsCount: !!((meta as any)?.feed),
|
|
196
233
|
};
|
|
197
234
|
|
|
198
235
|
return `<!doctype html>
|
|
@@ -232,6 +269,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
232
269
|
.toolbar-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
|
|
233
270
|
.group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
|
|
234
271
|
.group:first-child { padding-left: 0; border-left: 0; }
|
|
272
|
+
.primary-toolbar-row .group { padding-left: 0; border-left: 0; }
|
|
235
273
|
:root {
|
|
236
274
|
--control-height: 36px;
|
|
237
275
|
}
|
|
@@ -254,6 +292,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
254
292
|
background: linear-gradient(180deg, rgba(35,53,79,0.95), rgba(22,32,47,0.98));
|
|
255
293
|
}
|
|
256
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
|
+
}
|
|
257
300
|
.indicator {
|
|
258
301
|
display: inline-flex;
|
|
259
302
|
align-items: center;
|
|
@@ -439,11 +482,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
439
482
|
background: rgba(10,14,20,0.72);
|
|
440
483
|
backdrop-filter: blur(10px);
|
|
441
484
|
}
|
|
442
|
-
.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; }
|
|
443
486
|
.legend-line + .legend-line { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.06); }
|
|
444
487
|
.legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
445
488
|
.legend-dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
|
|
446
|
-
.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; }
|
|
447
490
|
.legend-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #eef4fb; font-weight: 700; }
|
|
448
491
|
.status { display: inline-flex; align-items: center; gap: 8px; font-size: 10px; color: #8290a2; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
449
492
|
.pill { border: 1px solid rgba(255,255,255,0.06); border-radius: 999px; padding: 4px 8px; background: rgba(255,255,255,0.03); }
|
|
@@ -466,7 +509,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
466
509
|
<div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
|
|
467
510
|
</div>
|
|
468
511
|
<div id="toolbar">
|
|
469
|
-
<div class="toolbar-row">
|
|
512
|
+
<div class="toolbar-row primary-toolbar-row">
|
|
470
513
|
<div class="group">
|
|
471
514
|
<div class="indicator">
|
|
472
515
|
<span class="tag">pair</span>
|
|
@@ -487,7 +530,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
487
530
|
<div class="group">
|
|
488
531
|
<div class="indicator">
|
|
489
532
|
<label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
|
|
490
|
-
<span class="tag">period</span>
|
|
491
533
|
<input type="text" inputmode="numeric" id="sma-period" value="${defaults.smaPeriod}">
|
|
492
534
|
<span class="step-stack">
|
|
493
535
|
<button type="button" class="step-btn" id="sma-period-inc">▲</button>
|
|
@@ -496,13 +538,14 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
496
538
|
</div>
|
|
497
539
|
<div class="indicator">
|
|
498
540
|
<label><input type="checkbox" id="vwap-toggle"${defaults.vwapEnabled ? ' checked' : ''}> VWMA</label>
|
|
499
|
-
<span class="tag">bars</span>
|
|
500
541
|
<input type="text" inputmode="numeric" id="vwap-bars" value="${defaults.vwapBars}">
|
|
501
542
|
<span class="step-stack">
|
|
502
543
|
<button type="button" class="step-btn" id="vwap-bars-inc">▲</button>
|
|
503
544
|
<button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
|
|
504
545
|
</span>
|
|
505
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>' : '')}
|
|
506
549
|
</div>
|
|
507
550
|
</div>
|
|
508
551
|
<div class="toolbar-row">
|
|
@@ -527,20 +570,22 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
527
570
|
<button type="button" class="step-btn" id="ama-slow-inc">▲</button>
|
|
528
571
|
<button type="button" class="step-btn" id="ama-slow-dec">▼</button>
|
|
529
572
|
</span>
|
|
530
|
-
<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>
|
|
531
577
|
</div>
|
|
532
|
-
<div class="indicator" title="Range min/max built only from the live AMA price (
|
|
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">
|
|
533
579
|
<label><input type="checkbox" id="range-toggle"${defaults.rangeEnabled ? ' checked' : ''}> Range</label>
|
|
534
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>
|
|
535
|
-
<span id="range-grid-wrap" style="display
|
|
536
|
-
<span
|
|
537
|
-
<input type="range" id="range-span" min="1.25" max="2" step="0.05" value="${defaults.rangeSpan.toFixed(2)}" style="width:90px;vertical-align:middle">
|
|
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">
|
|
538
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>
|
|
539
584
|
</span>
|
|
540
585
|
</div>
|
|
541
586
|
<div class="indicator">
|
|
542
587
|
<label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
|
|
543
|
-
<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>
|
|
544
589
|
<span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
|
|
545
590
|
</div>
|
|
546
591
|
</div>
|
|
@@ -553,21 +598,16 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
553
598
|
<span class="legend-item"><span class="legend-label">Time</span> <span class="legend-value" id="legend-time">-</span></span>
|
|
554
599
|
<span class="legend-item"><span class="legend-label">C</span> <span class="legend-value" id="legend-close">-</span></span>
|
|
555
600
|
<span class="legend-item"><span class="legend-label">Delta</span> <span class="legend-value" id="legend-delta">-</span></span>
|
|
556
|
-
<span class="legend-item"><span class="legend-label">Vol</span> <span class="legend-value" id="legend-volume">-</span></span>
|
|
557
|
-
<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>
|
|
558
602
|
</div>
|
|
559
603
|
<div class="legend-line">
|
|
560
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>
|
|
561
|
-
<span class="legend-item"><span class="legend-dot" style="background:#2dd4bf"></span><span class="legend-label">AMA</span> <span class="legend-value" id="legend-ama">-</span></span>
|
|
562
|
-
<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>
|
|
563
|
-
<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>
|
|
564
|
-
<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>
|
|
565
|
-
|
|
566
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>
|
|
567
|
-
<span class="legend-item"><span class="legend-dot"
|
|
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>
|
|
568
608
|
</div>
|
|
569
609
|
</div>
|
|
570
|
-
<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>
|
|
571
611
|
<div id="volume-chart"></div>
|
|
572
612
|
</div>
|
|
573
613
|
</div>
|
|
@@ -582,7 +622,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
582
622
|
const baseCloseValues = baseCandles.map((c) => c.close);
|
|
583
623
|
const timeframes = Array.isArray(payload.timeframes) ? payload.timeframes.slice() : [];
|
|
584
624
|
const timeframeMap = new Map(timeframes.map((item) => [item.label, item]));
|
|
585
|
-
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';
|
|
586
631
|
|
|
587
632
|
const state = loadState();
|
|
588
633
|
let currentTimeframe = state.timeframe || payload.defaultTimeframe || '1h';
|
|
@@ -606,8 +651,19 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
606
651
|
? Number(state.rangeWidthPct)
|
|
607
652
|
: (Number.isFinite(Number(payload.rangeWidthPct)) && Number(payload.rangeWidthPct) > 0 ? Number(payload.rangeWidthPct) : 2);
|
|
608
653
|
let currentRangeSpan = Number.isFinite(state.rangeSpan) && Number(state.rangeSpan) > 0
|
|
609
|
-
? Math.min(2, Math.max(1.
|
|
610
|
-
: (Number.isFinite(Number(payload.rangeSpan)) && Number(payload.rangeSpan) > 0 ? Math.min(2, Math.max(1.
|
|
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;
|
|
611
667
|
let currentAmaInitOffsetEnabled = false;
|
|
612
668
|
let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
|
|
613
669
|
let currentPriceScale = state.priceScale || payload.priceScale || 'log';
|
|
@@ -631,7 +687,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
631
687
|
let currentAmaOff = [];
|
|
632
688
|
let currentRangeUpper = [];
|
|
633
689
|
let currentRangeLower = [];
|
|
634
|
-
let currentRangeTrend = [];
|
|
635
690
|
let priceChart = null;
|
|
636
691
|
let volumeChart = null;
|
|
637
692
|
let lastRenderedPriceScale = null;
|
|
@@ -641,6 +696,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
641
696
|
let lastCandleKey = null;
|
|
642
697
|
let pendingRange = null;
|
|
643
698
|
let pendingRangeRaf = 0;
|
|
699
|
+
let rangePanelRaf = 0;
|
|
700
|
+
let yRefitRaf = 0;
|
|
644
701
|
let xMin = 0;
|
|
645
702
|
let xMax = 0;
|
|
646
703
|
let smaWorker = null;
|
|
@@ -659,27 +716,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
659
716
|
// TradingView-exporter interaction overrides (PR #3 / #11 feature):
|
|
660
717
|
// redeclare after UPLOT_SHARED_SCRIPT so these hoisted declarations win
|
|
661
718
|
// for this page only, without mutating the shared stack used by other charts.
|
|
662
|
-
function clampXRange(min, max) {
|
|
663
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return { min: xMin, max: xMax };
|
|
664
|
-
// Salli tyhjaa tilaa datan molemmin puolin (TradingView-tyyli):
|
|
665
|
-
// enintaan 75 % datan pituudesta reunapuskurina kummallekin puolelle.
|
|
666
|
-
const dataSpan = Math.max(1, xMax - xMin);
|
|
667
|
-
const maxPad = dataSpan * 0.75;
|
|
668
|
-
let lo = Math.max(xMin - maxPad, min);
|
|
669
|
-
let hi = Math.min(xMax + maxPad, max);
|
|
670
|
-
if (lo < xMin && hi <= lo) lo = xMin;
|
|
671
|
-
if (hi > xMax + maxPad) hi = xMax + maxPad;
|
|
672
|
-
if (hi <= lo) return { min: xMin, max: xMax };
|
|
673
|
-
return { min: lo, max: hi };
|
|
674
|
-
}
|
|
675
719
|
function bindWheelZoom(chart) {
|
|
676
720
|
chart.root.addEventListener('wheel', (e) => {
|
|
677
721
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
678
722
|
e.preventDefault();
|
|
679
723
|
const rect = chart.root.getBoundingClientRect();
|
|
680
|
-
|
|
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))) {
|
|
681
728
|
const centerY = chart.posToVal(e.clientY - rect.top, 'y');
|
|
682
|
-
|
|
729
|
+
const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
730
|
+
zoomYAt(chart, centerY, delta < 0 ? 0.91 : 1.10);
|
|
683
731
|
return;
|
|
684
732
|
}
|
|
685
733
|
e.stopPropagation();
|
|
@@ -698,12 +746,132 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
698
746
|
syncXRange(center - nextSpan * ratio, center - nextSpan * ratio + nextSpan);
|
|
699
747
|
}, { passive: false });
|
|
700
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
|
+
}
|
|
701
869
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
702
|
-
// Significant-digit price formatting (
|
|
703
|
-
// DEXBot order-price style (formatCurrency)
|
|
870
|
+
// Significant-digit price formatting (4 "active" digits), matching the
|
|
871
|
+
// DEXBot order-price style (formatCurrency).
|
|
704
872
|
// Very small values get SI micro-suffixes (m/µ/n/p/...); large values
|
|
705
|
-
// get comma grouping. Examples: 34.
|
|
706
|
-
// 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".
|
|
707
875
|
function fmtPrice(v) {
|
|
708
876
|
if (v == null || !Number.isFinite(v)) return '-';
|
|
709
877
|
const num = Number(v);
|
|
@@ -807,6 +975,9 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
807
975
|
ticks: { stroke: '#30363d', width: 1 },
|
|
808
976
|
font: '11px Segoe UI, sans-serif',
|
|
809
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
|
+
}
|
|
810
981
|
const xScale = u.scales.x || {};
|
|
811
982
|
const spanSec = Number.isFinite(xScale.min) && Number.isFinite(xScale.max)
|
|
812
983
|
? Math.max(0, xScale.max - xScale.min)
|
|
@@ -834,6 +1005,9 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
834
1005
|
rangeScaleEnabled: currentRangeScaleEnabled,
|
|
835
1006
|
rangeWidthPct: currentRangeWidthPct,
|
|
836
1007
|
rangeSpan: currentRangeSpan,
|
|
1008
|
+
ordersVisible: currentOrdersVisible,
|
|
1009
|
+
volumeVisible: currentVolumeVisible,
|
|
1010
|
+
volumeMode: currentVolumeMode,
|
|
837
1011
|
amaInitOffset: currentAmaInitOffset,
|
|
838
1012
|
priceScale: currentPriceScale,
|
|
839
1013
|
pairMode: currentPairMode,
|
|
@@ -848,6 +1022,75 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
848
1022
|
? (payload.pairLabelInverse || (payload.assetLabelB + '/' + payload.assetLabelA))
|
|
849
1023
|
: (payload.pairLabelNormal || (payload.assetLabelA + '/' + payload.assetLabelB));
|
|
850
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
|
+
}
|
|
851
1094
|
function refreshSubtitle() {
|
|
852
1095
|
const subtitleEl = document.getElementById('subtitle');
|
|
853
1096
|
if (!subtitleEl) return;
|
|
@@ -896,38 +1139,65 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
896
1139
|
seriesCache.set(key, state);
|
|
897
1140
|
return state;
|
|
898
1141
|
}
|
|
899
|
-
function aggregateCandles(rows, seconds) {
|
|
1142
|
+
function aggregateCandles(rows, seconds, label) {
|
|
1143
|
+
const monthly = String(label || '') === '1M';
|
|
900
1144
|
const bucketSec = Math.max(1, Math.round(seconds || 3600));
|
|
901
|
-
const cacheKey = currentPairMode + '|' + bucketSec;
|
|
1145
|
+
const cacheKey = currentPairMode + '|' + (monthly ? '1M' : String(bucketSec));
|
|
902
1146
|
if (aggregateCache.has(cacheKey)) return aggregateCache.get(cacheKey);
|
|
903
1147
|
const out = [];
|
|
904
1148
|
const idxs = [];
|
|
905
1149
|
let cur = null;
|
|
906
1150
|
let curBucket = null;
|
|
907
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
|
+
};
|
|
908
1163
|
for (let i = 0; i < rows.length; i++) {
|
|
909
1164
|
const row = rows[i];
|
|
910
1165
|
const ts = Number(row.time);
|
|
911
1166
|
if (!Number.isFinite(ts)) continue;
|
|
912
|
-
|
|
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
|
+
}
|
|
913
1174
|
if (!cur || bucket !== curBucket) {
|
|
914
1175
|
if (cur) {
|
|
915
|
-
|
|
1176
|
+
finishBucket(cur);
|
|
916
1177
|
idxs.push(curIdx);
|
|
917
1178
|
}
|
|
918
1179
|
curBucket = bucket;
|
|
919
1180
|
curIdx = i;
|
|
920
|
-
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
|
+
};
|
|
921
1190
|
} else {
|
|
922
1191
|
cur.high = Math.max(cur.high, row.high);
|
|
923
1192
|
cur.low = Math.min(cur.low, row.low);
|
|
924
1193
|
cur.close = row.close;
|
|
925
1194
|
cur.volume += row.volume || 0;
|
|
1195
|
+
cur.barCount += 1;
|
|
926
1196
|
curIdx = i;
|
|
927
1197
|
}
|
|
928
1198
|
}
|
|
929
1199
|
if (cur) {
|
|
930
|
-
|
|
1200
|
+
finishBucket(cur);
|
|
931
1201
|
idxs.push(curIdx);
|
|
932
1202
|
}
|
|
933
1203
|
const result = { candles: out, idxs };
|
|
@@ -1166,14 +1436,14 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1166
1436
|
// Bot-grid range band: min/max derive ONLY from the live AMA price.
|
|
1167
1437
|
// All math below runs the canonical market_adapter/grid sources,
|
|
1168
1438
|
// embedded verbatim (not hand-copied): computeAverageAmaSlopePct,
|
|
1169
|
-
// applyAsymmetricBounds (+ metrics/base),
|
|
1170
|
-
// parseRelativeMultiplier. With Range Scaling
|
|
1171
|
-
// the
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
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].
|
|
1174
1444
|
// Never reads candles, pair-display mapping, or axis/zoom state —
|
|
1175
1445
|
// inversion and timeframe sampling apply to the AMA first.
|
|
1176
|
-
${embedFunctionSources([computeAverageAmaSlopePct, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1446
|
+
${embedFunctionSources([computeAverageAmaSlopePct, computeAmaSlopeClipThreshold, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
|
|
1177
1447
|
function computeRangeBand(baseAma) {
|
|
1178
1448
|
const n = Array.isArray(baseAma) ? baseAma.length : 0;
|
|
1179
1449
|
const upper = new Array(n).fill(null);
|
|
@@ -1188,19 +1458,19 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1188
1458
|
const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
|
|
1189
1459
|
? Number(gridCfg.maxAsymmetryFactor)
|
|
1190
1460
|
: (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
|
|
1191
|
-
const widthPct = Number(currentRangeWidthPct) > 0 ? Number(currentRangeWidthPct) : 2;
|
|
1192
1461
|
const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
|
|
1193
1462
|
const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
|
|
1194
|
-
// User x-range span (slider, default
|
|
1195
|
-
// width input — min = AMA/span, max = AMA*span.
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
(Number
|
|
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);
|
|
1204
1474
|
const slopeScaling = !!currentRangeScaleEnabled;
|
|
1205
1475
|
if (!currentAmaEnabled || n === 0) return { upper, lower, trend };
|
|
1206
1476
|
for (let i = 0; i < n; i++) {
|
|
@@ -1209,29 +1479,17 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1209
1479
|
if (!Number.isFinite(ama) || ama <= 0 || !Number.isFinite(past) || past <= 0) continue;
|
|
1210
1480
|
const slopePct = computeAverageAmaSlopePct(ama, past, lookback);
|
|
1211
1481
|
if (slopePct == null || !Number.isFinite(slopePct)) continue;
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
if (!hasGridShape) {
|
|
1215
|
-
// Width% envelope: symmetric around AMA; scaling sizes it
|
|
1216
|
-
// by slope strength ("real range"), otherwise full width.
|
|
1217
|
-
const w = slopeScaling ? strength * widthPct : widthPct;
|
|
1218
|
-
if (!(w > 0)) continue;
|
|
1219
|
-
const half = ama * w / 100;
|
|
1220
|
-
if (!Number.isFinite(half) || half <= 0) continue;
|
|
1221
|
-
upper[i] = ama + half;
|
|
1222
|
-
lower[i] = Math.max(ama - half, ama * 1e-6);
|
|
1223
|
-
trend[i] = dir;
|
|
1224
|
-
continue;
|
|
1225
|
-
}
|
|
1482
|
+
const csp = Math.max(-clipThreshold, Math.min(clipThreshold, slopePct));
|
|
1483
|
+
const dir = Math.abs(csp) <= neutral ? 0 : (csp > 0 ? 1 : -1);
|
|
1226
1484
|
// Symmetric base from this bar's AMA and the user span —
|
|
1227
1485
|
// no chart or config price level.
|
|
1228
1486
|
let rMin = ama / span;
|
|
1229
1487
|
let rMax = ama * span;
|
|
1230
1488
|
if (slopeScaling && dir !== 0) {
|
|
1231
|
-
// Canonical tilt:
|
|
1232
|
-
//
|
|
1489
|
+
// Canonical tilt: clipped slope offset applied through
|
|
1490
|
+
// asymmetric_bounds, then the narrowing-side guard.
|
|
1233
1491
|
const trendName = dir > 0 ? 'UP' : 'DOWN';
|
|
1234
|
-
const slopeOffset = clamp(
|
|
1492
|
+
const slopeOffset = clamp(csp / maxSlope, -1, 1) * maxSlopeOffset;
|
|
1235
1493
|
const tilt = applyAsymmetricBounds({
|
|
1236
1494
|
centerPrice: ama,
|
|
1237
1495
|
minPrice: ama / span,
|
|
@@ -1259,10 +1517,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1259
1517
|
}
|
|
1260
1518
|
return { upper, lower, trend };
|
|
1261
1519
|
}
|
|
1262
|
-
function sampleTrendByIndex(series, idxs) {
|
|
1263
|
-
if (!Array.isArray(series) || !Array.isArray(idxs)) return [];
|
|
1264
|
-
return idxs.map((idx) => (Number.isFinite(idx) && series[idx] != null ? series[idx] : 0));
|
|
1265
|
-
}
|
|
1266
1520
|
function lowerBound(arr, value) {
|
|
1267
1521
|
let lo = 0;
|
|
1268
1522
|
let hi = arr.length;
|
|
@@ -1329,7 +1583,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1329
1583
|
let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
|
|
1330
1584
|
let max = 0;
|
|
1331
1585
|
for (let i = start; i < end; i++) {
|
|
1332
|
-
const v = currentCandles[i]
|
|
1586
|
+
const v = getDisplayVolume(currentCandles[i]);
|
|
1333
1587
|
if (Number.isFinite(v) && v > max) max = v;
|
|
1334
1588
|
}
|
|
1335
1589
|
if (!Number.isFinite(max) || max <= 0) return [0, 1];
|
|
@@ -1348,12 +1602,29 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1348
1602
|
btn.title = next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale';
|
|
1349
1603
|
btn.setAttribute('aria-label', next === 'linear' ? 'Toggle to log scale' : 'Toggle to linear scale');
|
|
1350
1604
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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();
|
|
1357
1628
|
}
|
|
1358
1629
|
function setIndicatorSeriesVisible(seriesIndex, visible) {
|
|
1359
1630
|
if (!priceChart) return;
|
|
@@ -1381,7 +1652,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1381
1652
|
function hideRangeBandImmediate() {
|
|
1382
1653
|
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1383
1654
|
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1384
|
-
currentRangeTrend = new Array(currentCandles.length).fill(0);
|
|
1385
1655
|
// The band is a draw hook with no uPlot series: nothing else
|
|
1386
1656
|
// repaints on hide, so redraw explicitly (stale pixels otherwise linger).
|
|
1387
1657
|
if (priceChart && typeof priceChart.redraw === 'function') priceChart.redraw();
|
|
@@ -1395,7 +1665,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1395
1665
|
currentSmaInitOff = new Array(currentCandles.length).fill(null);
|
|
1396
1666
|
currentRangeUpper = new Array(currentCandles.length).fill(null);
|
|
1397
1667
|
currentRangeLower = new Array(currentCandles.length).fill(null);
|
|
1398
|
-
currentRangeTrend = new Array(currentCandles.length).fill(0);
|
|
1399
1668
|
if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
|
|
1400
1669
|
currentPriceData[6] = currentAma;
|
|
1401
1670
|
currentPriceData[8] = currentSmaInit;
|
|
@@ -1429,26 +1698,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1429
1698
|
document.getElementById('legend-delta').textContent = Number.isFinite(delta)
|
|
1430
1699
|
? ((delta >= 0 ? '+' : '') + fmtPrice(delta))
|
|
1431
1700
|
: '-';
|
|
1432
|
-
document.getElementById('legend-volume').textContent =
|
|
1433
|
-
document.getElementById('legend-scale').textContent = currentPriceScale === 'linear' ? 'Linear' : 'Log';
|
|
1701
|
+
document.getElementById('legend-volume').textContent = fmtVolumeWithSym(getDisplayVolume(c));
|
|
1434
1702
|
document.getElementById('legend-sma').textContent = Number.isFinite(currentSma[idx]) ? fmtPrice(currentSma[idx]) : (smaPending ? '...' : '-');
|
|
1435
|
-
document.getElementById('legend-ama').textContent = Number.isFinite(currentAma[idx]) ? fmtPrice(currentAma[idx]) : '-';
|
|
1436
|
-
document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
|
|
1437
|
-
document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
|
|
1438
|
-
document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
|
|
1439
|
-
const rangeDir = currentRangeTrend[idx];
|
|
1440
|
-
const rangeDot = document.getElementById('legend-range-dot');
|
|
1441
|
-
const rangeVal = document.getElementById('legend-range');
|
|
1442
|
-
if (rangeDot) rangeDot.style.background = rangeDir === 1 ? '#26a69a' : (rangeDir === -1 ? '#ef5350' : '#8b949e');
|
|
1443
|
-
if (rangeVal) {
|
|
1444
|
-
const ru = currentRangeUpper[idx];
|
|
1445
|
-
const rl = currentRangeLower[idx];
|
|
1446
|
-
const ama = currentAma[idx];
|
|
1447
|
-
if (currentRangeEnabled && currentAmaEnabled && Number.isFinite(ru) && ru > ama && Number.isFinite(rl) && rl > 0 && rl < ama && ama > 0) {
|
|
1448
|
-
rangeVal.textContent = '+' + ((ru - ama) / ama * 100).toFixed(1) + '% / -' + ((ama - rl) / ama * 100).toFixed(1) + '%';
|
|
1449
|
-
} else rangeVal.textContent = '-';
|
|
1450
|
-
}
|
|
1451
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]) : '-';
|
|
1452
1706
|
}
|
|
1453
1707
|
function candlePlugin() {
|
|
1454
1708
|
function drawCandles(u) {
|
|
@@ -1602,10 +1856,10 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1602
1856
|
u.ctx.clip();
|
|
1603
1857
|
// Behind candles/AMA lines (same trick as the dynamic-weight signal background).
|
|
1604
1858
|
u.ctx.globalCompositeOperation = 'destination-over';
|
|
1605
|
-
const UP_FILL = 'rgba(
|
|
1606
|
-
const DOWN_FILL = 'rgba(
|
|
1607
|
-
// Symmetric envelope, always both sides:
|
|
1608
|
-
// plus
|
|
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
|
|
1609
1863
|
// grid's range scaling. Paint per contiguous valid segment.
|
|
1610
1864
|
let segTop = [];
|
|
1611
1865
|
let segBot = [];
|
|
@@ -1628,12 +1882,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1628
1882
|
u.ctx.closePath();
|
|
1629
1883
|
u.ctx.fill();
|
|
1630
1884
|
}
|
|
1631
|
-
u.ctx.strokeStyle = 'rgba(
|
|
1885
|
+
u.ctx.strokeStyle = 'rgba(239,83,80,0.55)';
|
|
1632
1886
|
u.ctx.lineWidth = 1;
|
|
1633
1887
|
u.ctx.beginPath();
|
|
1634
1888
|
segTop.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1635
1889
|
u.ctx.stroke();
|
|
1636
|
-
u.ctx.strokeStyle = 'rgba(
|
|
1890
|
+
u.ctx.strokeStyle = 'rgba(38,166,154,0.55)';
|
|
1637
1891
|
u.ctx.beginPath();
|
|
1638
1892
|
segBot.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
|
|
1639
1893
|
u.ctx.stroke();
|
|
@@ -1670,7 +1924,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1670
1924
|
currentSeriesState = getSeriesState(currentPairMode);
|
|
1671
1925
|
currentSeriesCandles = currentSeriesState.candles;
|
|
1672
1926
|
currentSeriesCloseValues = currentSeriesState.closeValues;
|
|
1673
|
-
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600);
|
|
1927
|
+
const aggregated = aggregateCandles(currentSeriesCandles, tf?.seconds || 3600, tf?.label || currentTimeframe);
|
|
1674
1928
|
currentCandles = aggregated.candles;
|
|
1675
1929
|
if (currentCandles.length) {
|
|
1676
1930
|
xMin = currentCandles[0].time;
|
|
@@ -1702,22 +1956,30 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1702
1956
|
const baseRange = computeRangeBand(baseAma);
|
|
1703
1957
|
currentRangeUpper = sampleSeriesByIndex(baseRange.upper, aggregated.idxs);
|
|
1704
1958
|
currentRangeLower = sampleSeriesByIndex(baseRange.lower, aggregated.idxs);
|
|
1705
|
-
currentRangeTrend = sampleTrendByIndex(baseRange.trend, aggregated.idxs);
|
|
1706
1959
|
currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1707
1960
|
currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
|
|
1708
1961
|
const amaCfg = currentAmaConfig();
|
|
1709
1962
|
const warmupInit = currentAmaEnabled && baseAma.length > amaCfg.erPeriod ? baseAma[amaCfg.erPeriod] : null;
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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);
|
|
1714
1976
|
const offsetInit = currentAmaEnabled && currentAmaInitOffset !== 0 && baseAmaOff.length > amaCfg.erPeriod
|
|
1715
1977
|
? baseAmaOff[amaCfg.erPeriod]
|
|
1716
1978
|
: null;
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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);
|
|
1721
1983
|
currentPriceData = [
|
|
1722
1984
|
currentCandles.map((c) => c.time),
|
|
1723
1985
|
currentOpen,
|
|
@@ -1733,7 +1995,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1733
1995
|
];
|
|
1734
1996
|
currentVolumeData = [
|
|
1735
1997
|
currentCandles.map((c) => c.time),
|
|
1736
|
-
currentCandles.map((c) => c
|
|
1998
|
+
currentCandles.map((c) => getDisplayVolume(c)),
|
|
1737
1999
|
];
|
|
1738
2000
|
return {
|
|
1739
2001
|
priceData: currentPriceData,
|
|
@@ -1750,7 +2012,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1750
2012
|
padding: [14, 8, 8, 8],
|
|
1751
2013
|
legend: { show: false },
|
|
1752
2014
|
select: { show: false },
|
|
1753
|
-
cursor: { sync: { key:
|
|
2015
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1754
2016
|
scales: {
|
|
1755
2017
|
x: { time: true },
|
|
1756
2018
|
y: {
|
|
@@ -1801,7 +2063,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1801
2063
|
},
|
|
1802
2064
|
],
|
|
1803
2065
|
hooks: {
|
|
1804
|
-
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(); }],
|
|
1805
2072
|
},
|
|
1806
2073
|
};
|
|
1807
2074
|
|
|
@@ -1817,7 +2084,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1817
2084
|
padding: [6, 8, 8, 8],
|
|
1818
2085
|
legend: { show: false },
|
|
1819
2086
|
select: { show: false },
|
|
1820
|
-
cursor: { sync: { key:
|
|
2087
|
+
cursor: { sync: { key: SYNC_KEY, setSeries: false, scales: ['x', null] }, drag: { x: false, y: false, setScale: false }, focus: { prox: 20 } },
|
|
1821
2088
|
scales: {
|
|
1822
2089
|
x: { time: true },
|
|
1823
2090
|
y: {
|
|
@@ -1838,6 +2105,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1838
2105
|
makeTimeAxis(true),
|
|
1839
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))) },
|
|
1840
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
|
+
},
|
|
1841
2113
|
};
|
|
1842
2114
|
|
|
1843
2115
|
const volumePluginInst = volumePlugin();
|
|
@@ -1850,32 +2122,33 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1850
2122
|
lastRenderedPriceScale = currentPriceScale;
|
|
1851
2123
|
return priceChart;
|
|
1852
2124
|
}
|
|
1853
|
-
// True when clientX sits over the price-axis
|
|
1854
|
-
//
|
|
1855
|
-
//
|
|
1856
|
-
//
|
|
1857
|
-
//
|
|
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.
|
|
1858
2130
|
function inYAxisZone(chart, clientX) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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;
|
|
1866
2144
|
}
|
|
1867
2145
|
let priceMarkerLabel = null;
|
|
1868
|
-
let priceMarkerLine = null;
|
|
1869
2146
|
function ensurePriceMarker(u) {
|
|
1870
2147
|
if (priceMarkerLabel && priceMarkerLabel.parentNode === u.root) return;
|
|
1871
2148
|
if (priceMarkerLabel && priceMarkerLabel.parentNode) priceMarkerLabel.parentNode.removeChild(priceMarkerLabel);
|
|
1872
|
-
if (priceMarkerLine && priceMarkerLine.parentNode) priceMarkerLine.parentNode.removeChild(priceMarkerLine);
|
|
1873
2149
|
try { if (getComputedStyle(u.root).position === 'static') u.root.style.position = 'relative'; } catch (e) {}
|
|
1874
2150
|
priceMarkerLabel = document.createElement('div');
|
|
1875
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;';
|
|
1876
|
-
priceMarkerLine = document.createElement('div');
|
|
1877
|
-
priceMarkerLine.style.cssText = 'position:absolute;z-index:1;pointer-events:none;height:0;border-top:1px dashed currentColor;opacity:0.6;';
|
|
1878
|
-
u.root.appendChild(priceMarkerLine);
|
|
1879
2152
|
u.root.appendChild(priceMarkerLabel);
|
|
1880
2153
|
}
|
|
1881
2154
|
function positionPriceMarker(u) {
|
|
@@ -1886,7 +2159,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1886
2159
|
const price = last.close;
|
|
1887
2160
|
if (!Number.isFinite(price)) {
|
|
1888
2161
|
priceMarkerLabel.style.display = 'none';
|
|
1889
|
-
priceMarkerLine.style.display = 'none';
|
|
1890
2162
|
return;
|
|
1891
2163
|
}
|
|
1892
2164
|
const up = price >= (Number.isFinite(prev.close) ? prev.close : price);
|
|
@@ -1896,7 +2168,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1896
2168
|
const sMax = Number.isFinite(s.max) ? s.max : null;
|
|
1897
2169
|
if (sMin == null || sMax == null || sMax <= sMin) {
|
|
1898
2170
|
priceMarkerLabel.style.display = 'none';
|
|
1899
|
-
priceMarkerLine.style.display = 'none';
|
|
1900
2171
|
return;
|
|
1901
2172
|
}
|
|
1902
2173
|
let frac;
|
|
@@ -1913,7 +2184,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1913
2184
|
if (!overRect || overRect.height <= 0) return;
|
|
1914
2185
|
const plotTop = overRect.top - rootRect.top;
|
|
1915
2186
|
const yRel = (1 - frac) * overRect.height;
|
|
1916
|
-
|
|
2187
|
+
// No full-width dashed line by design (disturbs reading) — the
|
|
2188
|
+
// axis label above carries the last price on its own.
|
|
1917
2189
|
priceMarkerLabel.style.display = 'block';
|
|
1918
2190
|
priceMarkerLabel.textContent = fmtPriceLabel(price);
|
|
1919
2191
|
priceMarkerLabel.style.background = color;
|
|
@@ -1929,15 +2201,335 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1929
2201
|
priceMarkerLabel.style.right = '0px';
|
|
1930
2202
|
priceMarkerLabel.style.width = Math.max(40, rightAxisW - 4) + 'px';
|
|
1931
2203
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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);
|
|
2238
|
+
} else {
|
|
2239
|
+
frac = (price - sMin) / (sMax - sMin);
|
|
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';
|
|
1938
2448
|
} else {
|
|
1939
|
-
|
|
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;
|
|
1940
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';
|
|
1941
2533
|
}
|
|
1942
2534
|
function currentYRange(chart) {
|
|
1943
2535
|
const s = chart.scales.y || {};
|
|
@@ -1965,10 +2557,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1965
2557
|
return;
|
|
1966
2558
|
}
|
|
1967
2559
|
// Uloszoomauksen rajat: alin naytettava hinta voi painua viimeistaan
|
|
1968
|
-
// ~
|
|
1969
|
-
//
|
|
1970
|
-
//
|
|
1971
|
-
|
|
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 };
|
|
1972
2566
|
}
|
|
1973
2567
|
function applyYRange(chart, min, max) {
|
|
1974
2568
|
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return false;
|
|
@@ -1979,11 +2573,37 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
1979
2573
|
if (max <= min) return false;
|
|
1980
2574
|
}
|
|
1981
2575
|
if (priceBounds) {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return false;
|
|
1987
2607
|
}
|
|
1988
2608
|
manualYRange = { min: min, max: max };
|
|
1989
2609
|
chart.setScale('y', { min: min, max: max });
|
|
@@ -2042,6 +2662,56 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2042
2662
|
window.addEventListener('mouseup', endDrag, { once: true });
|
|
2043
2663
|
}, true);
|
|
2044
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
|
+
}
|
|
2045
2715
|
function bindYAxisReset(chart) {
|
|
2046
2716
|
chart.root.addEventListener('dblclick', (e) => {
|
|
2047
2717
|
if (chart !== priceChart) return;
|
|
@@ -2056,6 +2726,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2056
2726
|
function setControls() {
|
|
2057
2727
|
setActivePairMode(currentPairMode);
|
|
2058
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();
|
|
2059
2735
|
document.getElementById('sma-toggle').checked = currentSmaEnabled;
|
|
2060
2736
|
document.getElementById('sma-period').value = String(currentSmaPeriod);
|
|
2061
2737
|
document.getElementById('ama-toggle').checked = currentAmaEnabled;
|
|
@@ -2068,7 +2744,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2068
2744
|
document.getElementById('range-scale-toggle').checked = currentRangeScaleEnabled;
|
|
2069
2745
|
document.getElementById('range-span').value = Number(currentRangeSpan).toFixed(2);
|
|
2070
2746
|
document.getElementById('range-span-val').textContent = Number(currentRangeSpan).toFixed(2) + 'x';
|
|
2071
|
-
document.getElementById('range-grid-wrap').style.display =
|
|
2747
|
+
document.getElementById('range-grid-wrap').style.display = 'inline';
|
|
2072
2748
|
document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
|
|
2073
2749
|
document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
|
|
2074
2750
|
document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
|
|
@@ -2082,7 +2758,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2082
2758
|
if (needsRebuild) {
|
|
2083
2759
|
manualYRange = null;
|
|
2084
2760
|
priceMarkerLabel = null;
|
|
2085
|
-
|
|
2761
|
+
resetOverlayNodes();
|
|
2762
|
+
volumeHoverTip = null;
|
|
2086
2763
|
if (priceEl) priceEl.innerHTML = '';
|
|
2087
2764
|
if (volumeEl) volumeEl.innerHTML = '';
|
|
2088
2765
|
priceChart = null;
|
|
@@ -2113,16 +2790,53 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2113
2790
|
charts.forEach((chart) => {
|
|
2114
2791
|
bindWheelZoom(chart);
|
|
2115
2792
|
bindPan(chart);
|
|
2793
|
+
bindXAxisDrag(chart);
|
|
2116
2794
|
if (chart === priceChart) {
|
|
2117
2795
|
bindYAxisDrag(chart);
|
|
2118
2796
|
bindYAxisReset(chart);
|
|
2119
2797
|
chart.root.addEventListener('mousemove', (e) => {
|
|
2120
|
-
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';
|
|
2121
2829
|
});
|
|
2122
2830
|
}
|
|
2123
2831
|
chart.root.addEventListener('mousemove', () => {
|
|
2124
2832
|
if (chart.cursor.idx != null) updateLegend(chart.cursor.idx);
|
|
2125
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
|
+
}
|
|
2126
2840
|
chart.root.addEventListener('mouseleave', refreshLegend);
|
|
2127
2841
|
chart.root.addEventListener('mouseenter', () => chart.root.classList.add('is-hovered'));
|
|
2128
2842
|
chart.root.addEventListener('mouseleave', () => chart.root.classList.remove('is-hovered'));
|
|
@@ -2130,10 +2844,13 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2130
2844
|
chartEventsBound = true;
|
|
2131
2845
|
}
|
|
2132
2846
|
if (oldRange && Number.isFinite(oldRange.min) && Number.isFinite(oldRange.max)) {
|
|
2133
|
-
const next =
|
|
2847
|
+
const next = clampRange(oldRange.min, oldRange.max);
|
|
2134
2848
|
if (next) charts.forEach((chart) => chart.batch(() => chart.setScale('x', next)));
|
|
2135
2849
|
}
|
|
2136
2850
|
refreshLegend();
|
|
2851
|
+
renderMarketPanel();
|
|
2852
|
+
renderRangePanel();
|
|
2853
|
+
renderVolumePanel();
|
|
2137
2854
|
saveState();
|
|
2138
2855
|
}
|
|
2139
2856
|
|
|
@@ -2162,10 +2879,62 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2162
2879
|
pairToggle.addEventListener('click', () => {
|
|
2163
2880
|
currentPairMode = currentPairMode === 'inverse' ? 'normal' : 'inverse';
|
|
2164
2881
|
manualYRange = null;
|
|
2882
|
+
lastOverlayKey = '';
|
|
2165
2883
|
setControls();
|
|
2166
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();
|
|
2167
2901
|
});
|
|
2168
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
|
+
});
|
|
2169
2938
|
|
|
2170
2939
|
const syncInputs = () => {
|
|
2171
2940
|
currentSmaEnabled = document.getElementById('sma-toggle').checked;
|
|
@@ -2183,8 +2952,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2183
2952
|
currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
|
|
2184
2953
|
currentRangeEnabled = document.getElementById('range-toggle').checked;
|
|
2185
2954
|
currentRangeScaleEnabled = document.getElementById('range-scale-toggle').checked;
|
|
2186
|
-
currentRangeSpan = Math.min(2, Math.max(1.
|
|
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;
|
|
2187
2958
|
setControls();
|
|
2959
|
+
markActiveAmaPreset();
|
|
2188
2960
|
rerender(false);
|
|
2189
2961
|
};
|
|
2190
2962
|
|
|
@@ -2284,7 +3056,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2284
3056
|
makeStepper('ama-er', 1, 1, 0);
|
|
2285
3057
|
makeStepper('ama-fast', 0.1, 0.1, 1);
|
|
2286
3058
|
makeStepper('ama-slow', 1, 0.1, 1);
|
|
2287
|
-
document.getElementById('ama-reset').addEventListener('click', resetAmaDefaults);
|
|
2288
3059
|
document.getElementById('ama-init-offset').addEventListener('input', () => {
|
|
2289
3060
|
const input = document.getElementById('ama-init-offset');
|
|
2290
3061
|
const snapped = snapInitOffset(input.value);
|
|
@@ -2298,14 +3069,181 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2298
3069
|
syncInputs();
|
|
2299
3070
|
});
|
|
2300
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();
|
|
3227
|
+
|
|
2301
3228
|
window.addEventListener('resize', () => {
|
|
2302
3229
|
if (!charts.length) return;
|
|
2303
3230
|
charts.forEach((chart) => {
|
|
2304
3231
|
const el = chart === priceChart ? priceEl : volumeEl;
|
|
3232
|
+
if (el.clientWidth <= 0 || el.clientHeight <= 0) return;
|
|
2305
3233
|
chart.setSize({ width: el.clientWidth, height: el.clientHeight });
|
|
2306
3234
|
});
|
|
2307
3235
|
});
|
|
2308
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
|
+
|
|
2309
3247
|
${zoomResetScript()}
|
|
2310
3248
|
})();
|
|
2311
3249
|
</script>
|
|
@@ -2313,5 +3251,5 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
|
|
|
2313
3251
|
</html>`;
|
|
2314
3252
|
}
|
|
2315
3253
|
|
|
2316
|
-
export { generateHTML, normalizeCandle, loadMarketProfiles }
|
|
3254
|
+
export { generateHTML, normalizeCandle, loadMarketProfiles, resolveChartStorageKey, sanitizeStorageComponent, TRADINGVIEW_PREFS_KEY_PREFIX, TRADINGVIEW_SYNC_KEY }
|
|
2317
3255
|
|