|
3 | 3 | // @namespace binance.orderbook.trade |
4 | 4 | // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E |
5 | 5 | // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E |
6 | | -// @version 2.7.177 |
| 6 | +// @version 2.7.178 |
7 | 7 | // @author jackhai9 |
8 | 8 | // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 |
9 | 9 | // @match https://www.binance.com/*/futures/* |
|
2186 | 2186 | } |
2187 | 2187 | return matches.length === 1 ? matches[0] : null; |
2188 | 2188 | } |
| 2189 | + function createTradeInputResolver(ownerDocument, { |
| 2190 | + initialRoot = null, |
| 2191 | + panelId, |
| 2192 | + isVisibleElement |
| 2193 | + }) { |
| 2194 | + if (!ownerDocument?.querySelectorAll || typeof isVisibleElement !== "function") { |
| 2195 | + throw new Error("交易表单解析依赖异常"); |
| 2196 | + } |
| 2197 | + let cachedRoot = initialRoot; |
| 2198 | + const resolveInputs = (options = null) => { |
| 2199 | + const requirePrice = options?.requirePrice !== false; |
| 2200 | + if (cachedRoot?.isConnected) { |
| 2201 | + const cachedInputs = findActiveTradeInputs(cachedRoot, { |
| 2202 | + panelId, |
| 2203 | + isVisibleElement, |
| 2204 | + requirePrice |
| 2205 | + }); |
| 2206 | + if (cachedInputs) { |
| 2207 | + cachedRoot = cachedInputs.root; |
| 2208 | + return cachedInputs; |
| 2209 | + } |
| 2210 | + return null; |
| 2211 | + } |
| 2212 | + cachedRoot = null; |
| 2213 | + const discoveredInputs = findActiveTradeInputs(ownerDocument, { |
| 2214 | + panelId, |
| 2215 | + isVisibleElement, |
| 2216 | + requirePrice |
| 2217 | + }); |
| 2218 | + if (discoveredInputs) cachedRoot = discoveredInputs.root; |
| 2219 | + return discoveredInputs; |
| 2220 | + }; |
| 2221 | + return resolveInputs; |
| 2222 | + } |
2189 | 2223 | function createBoundedInputWriter({ writeValue, maxWriteAttempts }) { |
2190 | 2224 | if (typeof writeValue !== "function") { |
2191 | 2225 | throw new Error("输入框写入依赖异常"); |
|
2804 | 2838 | throw new Error("图表保存接口未能恢复"); |
2805 | 2839 | } |
2806 | 2840 | } |
| 2841 | + function createTradingViewRemoveSaveBurstController(api, { |
| 2842 | + settleQuietMs = 120, |
| 2843 | + maxWaitMs = 400, |
| 2844 | + setTimeoutFn = setTimeout, |
| 2845 | + clearTimeoutFn = clearTimeout |
| 2846 | + } = {}) { |
| 2847 | + validateTradingViewApi(api); |
| 2848 | + if (!Number.isFinite(settleQuietMs) || settleQuietMs <= 0) { |
| 2849 | + throw new Error("图表保存合并静默时间无效"); |
| 2850 | + } |
| 2851 | + if (!Number.isFinite(maxWaitMs) || maxWaitMs < settleQuietMs) { |
| 2852 | + throw new Error("图表保存合并最长等待时间无效"); |
| 2853 | + } |
| 2854 | + const sessionSaveChart = api.saveChart; |
| 2855 | + let activeBurst = null; |
| 2856 | + let removeEventCount = 0; |
| 2857 | + let saveRequestCount = 0; |
| 2858 | + let fullSaveCount = 0; |
| 2859 | + let stopped = false; |
| 2860 | + const clearBurstTimers = (burst) => { |
| 2861 | + if (burst.settleTimer !== null) clearTimeoutFn(burst.settleTimer); |
| 2862 | + if (burst.maxWaitTimer !== null) clearTimeoutFn(burst.maxWaitTimer); |
| 2863 | + burst.settleTimer = null; |
| 2864 | + burst.maxWaitTimer = null; |
| 2865 | + }; |
| 2866 | + const flushActiveBurst = () => { |
| 2867 | + const burst = activeBurst; |
| 2868 | + if (!burst) return void 0; |
| 2869 | + clearBurstTimers(burst); |
| 2870 | + activeBurst = null; |
| 2871 | + restoreSaveChartMethod( |
| 2872 | + api, |
| 2873 | + burst.wrapper, |
| 2874 | + burst.originalSaveChart, |
| 2875 | + burst.originalDescriptor |
| 2876 | + ); |
| 2877 | + if (!burst.pendingSave) return void 0; |
| 2878 | + fullSaveCount += 1; |
| 2879 | + return burst.originalSaveChart.apply( |
| 2880 | + burst.pendingSave.thisValue, |
| 2881 | + burst.pendingSave.args |
| 2882 | + ); |
| 2883 | + }; |
| 2884 | + const scheduleBurstSettle = (burst) => { |
| 2885 | + if (burst.settleTimer !== null) clearTimeoutFn(burst.settleTimer); |
| 2886 | + burst.settleTimer = setTimeoutFn(flushActiveBurst, settleQuietMs); |
| 2887 | + }; |
| 2888 | + const startBurst = () => { |
| 2889 | + if (activeBurst || api.saveChart !== sessionSaveChart) return activeBurst; |
| 2890 | + const originalSaveChart = api.saveChart; |
| 2891 | + const originalDescriptor = Object.getOwnPropertyDescriptor(api, "saveChart"); |
| 2892 | + const burst = { |
| 2893 | + maxWaitTimer: null, |
| 2894 | + originalDescriptor, |
| 2895 | + originalSaveChart, |
| 2896 | + pendingSave: null, |
| 2897 | + settleTimer: null, |
| 2898 | + wrapper: null |
| 2899 | + }; |
| 2900 | + burst.wrapper = function removeSaveBurstWrapper(...args) { |
| 2901 | + saveRequestCount += 1; |
| 2902 | + burst.pendingSave = { thisValue: this, args }; |
| 2903 | + return void 0; |
| 2904 | + }; |
| 2905 | + api.saveChart = burst.wrapper; |
| 2906 | + if (api.saveChart !== burst.wrapper) { |
| 2907 | + throw new Error("图表保存接口无法启用删除事件合并"); |
| 2908 | + } |
| 2909 | + activeBurst = burst; |
| 2910 | + burst.maxWaitTimer = setTimeoutFn(flushActiveBurst, maxWaitMs); |
| 2911 | + return burst; |
| 2912 | + }; |
| 2913 | + const handleDrawingEvent = (_drawingId, eventType) => { |
| 2914 | + if (stopped || eventType !== "remove") return; |
| 2915 | + removeEventCount += 1; |
| 2916 | + const burst = startBurst(); |
| 2917 | + if (burst) scheduleBurstSettle(burst); |
| 2918 | + }; |
| 2919 | + api.subscribe("drawing_event", handleDrawingEvent); |
| 2920 | + return { |
| 2921 | + flush: flushActiveBurst, |
| 2922 | + stop() { |
| 2923 | + if (stopped) throw new Error("图表删除事件保存合并器已停止"); |
| 2924 | + stopped = true; |
| 2925 | + api.unsubscribe("drawing_event", handleDrawingEvent); |
| 2926 | + flushActiveBurst(); |
| 2927 | + return { fullSaveCount, removeEventCount, saveRequestCount }; |
| 2928 | + } |
| 2929 | + }; |
| 2930 | + } |
2807 | 2931 | async function coalesceTradingViewDrawingSaves(api, action, { |
2808 | 2932 | eventDiscoveryTimeoutMs = 800, |
2809 | 2933 | settleQuietMs = 50, |
|
2924 | 3048 | } |
2925 | 3049 | } |
2926 | 3050 |
|
| 3051 | + // src/binance-orderbook-trade/dom/tradingview-target.js |
| 3052 | + var CHART_ROOT_SELECTOR = ".chart-widget-root"; |
| 3053 | + function hasVisibleBox(element) { |
| 3054 | + if (!element?.getClientRects().length) return false; |
| 3055 | + const rect = element.getBoundingClientRect(); |
| 3056 | + return rect.width > 0 && rect.height > 0; |
| 3057 | + } |
| 3058 | + function findBinanceTradingViewTarget(document2) { |
| 3059 | + const chartRoots = Array.from(document2.querySelectorAll(CHART_ROOT_SELECTOR)).filter(hasVisibleBox); |
| 3060 | + if (!chartRoots.length) return null; |
| 3061 | + if (chartRoots.length > 1) { |
| 3062 | + throw new Error(`可见图表区域数量异常:${chartRoots.length}`); |
| 3063 | + } |
| 3064 | + const chartRoot = chartRoots[0]; |
| 3065 | + const tradingViewApis = Array.from(chartRoot.querySelectorAll("iframe")).map((frame) => frame.contentWindow?.tradingViewApi).filter(Boolean); |
| 3066 | + if (!tradingViewApis.length) return null; |
| 3067 | + if (tradingViewApis.length > 1) { |
| 3068 | + throw new Error(`图表接口数量异常:${tradingViewApis.length}`); |
| 3069 | + } |
| 3070 | + return { chartRoot, tradingViewApi: tradingViewApis[0] }; |
| 3071 | + } |
| 3072 | + |
2927 | 3073 | // src/binance-orderbook-trade/core/cancel-dialog-decision.js |
2928 | 3074 | function resolveCancelDialogDecision({ |
2929 | 3075 | seenDialog, |
|
3113 | 3259 | return cells.length >= MIN_OPEN_ORDER_COLUMNS ? cells : []; |
3114 | 3260 | } |
3115 | 3261 |
|
3116 | | - // src/binance-orderbook-trade/dom/tradingview-target.js |
3117 | | - var CHART_ROOT_SELECTOR = ".chart-widget-root"; |
3118 | | - function hasVisibleBox(element) { |
3119 | | - if (!element?.getClientRects().length) return false; |
3120 | | - const rect = element.getBoundingClientRect(); |
3121 | | - return rect.width > 0 && rect.height > 0; |
3122 | | - } |
3123 | | - function findBinanceTradingViewTarget(document2) { |
3124 | | - const chartRoots = Array.from(document2.querySelectorAll(CHART_ROOT_SELECTOR)).filter(hasVisibleBox); |
3125 | | - if (!chartRoots.length) return null; |
3126 | | - if (chartRoots.length > 1) { |
3127 | | - throw new Error(`可见图表区域数量异常:${chartRoots.length}`); |
3128 | | - } |
3129 | | - const chartRoot = chartRoots[0]; |
3130 | | - const tradingViewApis = Array.from(chartRoot.querySelectorAll("iframe")).map((frame) => frame.contentWindow?.tradingViewApi).filter(Boolean); |
3131 | | - if (!tradingViewApis.length) return null; |
3132 | | - if (tradingViewApis.length > 1) { |
3133 | | - throw new Error(`图表接口数量异常:${tradingViewApis.length}`); |
3134 | | - } |
3135 | | - return { chartRoot, tradingViewApi: tradingViewApis[0] }; |
3136 | | - } |
3137 | | - |
3138 | 3262 | // src/binance-orderbook-trade/dom/chart-orders.js |
3139 | 3263 | var ACTIVE_POPOVER_SELECTOR = ".bn-bubble.active"; |
3140 | 3264 | var OPEN_ORDERS_LABEL_PATTERN = /^(?:当前委托|Open Orders)$/i; |
|
3604 | 3728 | const CANCEL_NO_ORDERS_FEEDBACK_MS = 600; |
3605 | 3729 | const CHART_ORDERS_MENU_TIMEOUT_MS = 1800; |
3606 | 3730 | const CHART_ORDERS_MENU_POLL_MS = 50; |
| 3731 | + const CONTINUOUS_CHART_REMOVE_SAVE_QUIET_MS = 120; |
| 3732 | + const CONTINUOUS_CHART_REMOVE_SAVE_MAX_WAIT_MS = 400; |
3607 | 3733 | const LADDER_MAKER_BUFFER_LEVELS = 1; |
3608 | 3734 | const LADDER_REPRICE_PAUSE_EVERY_ATTEMPTS = 5; |
3609 | 3735 | const LADDER_REPRICE_PAUSE_MS = 3e3; |
|
3702 | 3828 | let singleOrderTask = null; |
3703 | 3829 | let ladderAbortController = null; |
3704 | 3830 | let continuousLadderAbortController = null; |
| 3831 | + let continuousChartSaveCoalescer = null; |
3705 | 3832 | let activeLadderActionType = null; |
3706 | 3833 | let activeContinuousLadderActionType = null; |
3707 | 3834 | let activeContinuousLadderProgress = null; |
|
5801 | 5928 | ); |
5802 | 5929 | } |
5803 | 5930 | const observationRoot = inputs.root; |
| 5931 | + const resolveSynchronizedTradeInputs = createTradeInputResolver(document, { |
| 5932 | + initialRoot: observationRoot, |
| 5933 | + panelId: PANEL_ID, |
| 5934 | + isVisibleElement |
| 5935 | + }); |
5804 | 5936 | const readTradeState = createTradeInputStateReader({ |
5805 | | - resolveInputs: findTradeInputs, |
| 5937 | + resolveInputs: resolveSynchronizedTradeInputs, |
5806 | 5938 | expectedPrice, |
5807 | 5939 | expectedQty, |
5808 | 5940 | includePrice: true, |
|
6500 | 6632 | formatContinuousLadderProgress(label, phase, progress, titleReason) |
6501 | 6633 | ); |
6502 | 6634 | } |
| 6635 | + function startContinuousChartSaveCoalescing() { |
| 6636 | + try { |
| 6637 | + const target = findBinanceTradingViewTarget(document); |
| 6638 | + if (!target) return null; |
| 6639 | + return createTradingViewRemoveSaveBurstController(target.tradingViewApi, { |
| 6640 | + settleQuietMs: CONTINUOUS_CHART_REMOVE_SAVE_QUIET_MS, |
| 6641 | + maxWaitMs: CONTINUOUS_CHART_REMOVE_SAVE_MAX_WAIT_MS |
| 6642 | + }); |
| 6643 | + } catch (error) { |
| 6644 | + warn("未启用连续交易图表保存合并:", error?.message || error); |
| 6645 | + return null; |
| 6646 | + } |
| 6647 | + } |
| 6648 | + function stopContinuousChartSaveCoalescing(coalescer) { |
| 6649 | + if (!coalescer) return; |
| 6650 | + try { |
| 6651 | + const result = coalescer.stop(); |
| 6652 | + log("连续交易图表保存已合并", result); |
| 6653 | + } catch (error) { |
| 6654 | + err("连续交易图表保存合并清理失败:", error); |
| 6655 | + } finally { |
| 6656 | + if (continuousChartSaveCoalescer === coalescer) { |
| 6657 | + continuousChartSaveCoalescer = null; |
| 6658 | + } |
| 6659 | + } |
| 6660 | + } |
6503 | 6661 | async function startContinuousLadder(actionType) { |
6504 | 6662 | const spec = getLadderActionSpec2(actionType); |
6505 | 6663 | if (!spec || spec.mode !== "CLOSE") return startLadder(actionType); |
|
6516 | 6674 | const abortController = new AbortController(); |
6517 | 6675 | const continuousProgress = createContinuousLadderProgress(); |
6518 | 6676 | const positionCheckState = { checkedAt: Date.now(), retryAt: 0 }; |
| 6677 | + const chartSaveCoalescer = startContinuousChartSaveCoalescing(); |
6519 | 6678 | continuousLadderAbortController = abortController; |
| 6679 | + continuousChartSaveCoalescer = chartSaveCoalescer; |
6520 | 6680 | activeContinuousLadderActionType = actionType; |
6521 | 6681 | activeContinuousLadderProgress = continuousProgress; |
6522 | 6682 | const executionTask = (async () => { |
|
6616 | 6776 | ); |
6617 | 6777 | return { status: "failed", error: e }; |
6618 | 6778 | }).finally(() => { |
| 6779 | + stopContinuousChartSaveCoalescing(chartSaveCoalescer); |
6619 | 6780 | if (continuousLadderAbortController === abortController) { |
6620 | 6781 | continuousLadderAbortController = null; |
6621 | 6782 | } |
|
10483 | 10644 | scheduleChartOrdersRecovery(); |
10484 | 10645 | document.addEventListener("visibilitychange", () => { |
10485 | 10646 | if (document.hidden) { |
| 10647 | + try { |
| 10648 | + continuousChartSaveCoalescer?.flush(); |
| 10649 | + } catch (error) { |
| 10650 | + err("页面隐藏前图表保存刷新失败:", error); |
| 10651 | + } |
10486 | 10652 | stopTradingTimers(); |
10487 | 10653 | stopRouteWatcher(); |
10488 | 10654 | return; |
|
0 commit comments