Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions scripts/binance-orderbook-trade.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @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
// @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
// @version 2.7.182
// @version 2.7.183
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -3408,6 +3408,7 @@
}
};
var MAX_BUFFERED_UPDATES = 500;
var DEPTH_PROFILE_LEVELS_PER_SIDE = 1e3;
function assertSymbol(value) {
if (typeof value !== "string" || !/^[A-Z0-9_]+$/.test(value)) {
throw new Error("Invalid depth profile symbol");
Expand Down Expand Up @@ -3565,7 +3566,9 @@
return { ...level, cumulative };
});
}
function buildDepthProfile(book, { levelsPerSide = 60 } = {}) {
function buildDepthProfile(book, {
levelsPerSide = DEPTH_PROFILE_LEVELS_PER_SIDE
} = {}) {
if (!book.ready) throw new Error("Depth profile book is not ready");
if (!Number.isInteger(levelsPerSide) || levelsPerSide <= 0) {
throw new Error("Invalid depth profile level count");
Expand Down Expand Up @@ -3596,7 +3599,6 @@
}

// src/binance-orderbook-trade/core/depth-profile-session.js
var DEPTH_SNAPSHOT_LIMIT = 1e3;
var MAX_RESYNCS_PER_CONNECTION = 3;
var MAX_RECONNECT_ATTEMPTS = 5;
var RESYNC_DELAY_MS = 1e3;
Expand Down Expand Up @@ -3633,7 +3635,7 @@
let reconnectAttempts = 0;
let resyncAttempts = 0;
let book = createDepthProfileBook(symbol);
const snapshotUrl = `https://fapi.binance.com/fapi/v1/depth?symbol=${symbol}&limit=${DEPTH_SNAPSHOT_LIMIT}`;
const snapshotUrl = `https://fapi.binance.com/fapi/v1/depth?symbol=${symbol}&limit=${DEPTH_PROFILE_LEVELS_PER_SIDE}`;
const streamUrl = `wss://fstream.binance.com/public/ws/${symbol.toLowerCase()}@depth@100ms`;
function emitStatus(status, detail = "") {
if (!active) return;
Expand Down Expand Up @@ -3998,21 +4000,35 @@
if (root.style.top !== top) root.style.top = top;
if (root.style.height !== height) root.style.height = height;
}
function mapVisibleLevels(levels, geometry) {
return levels.map((level) => ({
...level,
y: geometry.priceToCoordinate(level.price)
})).filter((level) => Number.isFinite(level.y));
function bucketVisibleLevels(levels, geometry) {
const lastRow = Math.max(0, Math.ceil(geometry.height) - 1);
const buckets = /* @__PURE__ */ new Map();
for (const level of levels) {
const coordinate = geometry.priceToCoordinate(level.price);
if (!Number.isFinite(coordinate)) continue;
const y = Math.max(0, Math.min(lastRow, Math.round(coordinate)));
const current = buckets.get(y);
if (!current || level.cumulative > current.cumulative) {
buckets.set(y, { ...level, y });
}
}
return [...buckets.values()].sort((left, right) => left.y - right.y);
}
function drawSide(context, levels, profile, width, height, color) {
function drawSide(context, levels, maxVisibleCumulative, width, color) {
if (!levels.length) return;
const levelHeight = Math.max(1, Math.min(5, height / (levels.length * 2.4)));
context.fillStyle = color;
for (const level of levels) {
const barWidth = level.cumulative / profile.maxCumulative * width;
context.fillRect(width - barWidth, level.y - levelHeight / 2, barWidth, levelHeight);
const barWidth = level.cumulative / maxVisibleCumulative * width;
context.fillRect(width - barWidth, level.y, barWidth, 1);
}
}
function getMaxVisibleCumulative(...levelGroups) {
let maximum = 0;
for (const levels of levelGroups) {
for (const level of levels) maximum = Math.max(maximum, level.cumulative);
}
return maximum;
}
function renderDepthProfile(root, profile, geometry, currentPrice) {
setDepthProfileGeometry(root, geometry);
const canvas = root.querySelector(".jh-depth-profile-canvas");
Expand All @@ -4027,10 +4043,13 @@
if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
context.clearRect(0, 0, rect.width, rect.height);
const asks = mapVisibleLevels(profile.asks, geometry);
const bids = mapVisibleLevels(profile.bids, geometry);
drawSide(context, asks, profile, rect.width, rect.height, "rgba(246, 70, 93, .30)");
drawSide(context, bids, profile, rect.width, rect.height, "rgba(14, 203, 129, .30)");
const asks = bucketVisibleLevels(profile.asks, geometry);
const bids = bucketVisibleLevels(profile.bids, geometry);
const maxVisibleCumulative = getMaxVisibleCumulative(asks, bids);
if (maxVisibleCumulative > 0) {
drawSide(context, asks, maxVisibleCumulative, rect.width, "rgba(246, 70, 93, .30)");
drawSide(context, bids, maxVisibleCumulative, rect.width, "rgba(14, 203, 129, .30)");
}
const currentPriceY = geometry.priceToCoordinate(currentPrice);
if (!Number.isFinite(currentPriceY)) return true;
context.save();
Expand Down
5 changes: 4 additions & 1 deletion src/binance-orderbook-trade/core/depth-profile-book.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class DepthProfileSequenceError extends Error {
}

const MAX_BUFFERED_UPDATES = 500;
export const DEPTH_PROFILE_LEVELS_PER_SIDE = 1000;

function assertSymbol(value) {
if (typeof value !== 'string' || !/^[A-Z0-9_]+$/.test(value)) {
Expand Down Expand Up @@ -187,7 +188,9 @@ function addCumulativeQuantity(levels) {
});
}

export function buildDepthProfile(book, { levelsPerSide = 60 } = {}) {
export function buildDepthProfile(book, {
levelsPerSide = DEPTH_PROFILE_LEVELS_PER_SIDE,
} = {}) {
if (!book.ready) throw new Error('Depth profile book is not ready');
if (!Number.isInteger(levelsPerSide) || levelsPerSide <= 0) {
throw new Error('Invalid depth profile level count');
Expand Down
4 changes: 2 additions & 2 deletions src/binance-orderbook-trade/core/depth-profile-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import {
applyDepthProfileSnapshot,
buildDepthProfile,
createDepthProfileBook,
DEPTH_PROFILE_LEVELS_PER_SIDE,
DepthProfileSequenceError,
pushDepthProfileUpdate,
} from './depth-profile-book.js';

const DEPTH_SNAPSHOT_LIMIT = 1000;
const MAX_RESYNCS_PER_CONNECTION = 3;
const MAX_RECONNECT_ATTEMPTS = 5;
const RESYNC_DELAY_MS = 1000;
Expand Down Expand Up @@ -47,7 +47,7 @@ export function createDepthProfileSession(options) {
let resyncAttempts = 0;
let book = createDepthProfileBook(symbol);

const snapshotUrl = `https://fapi.binance.com/fapi/v1/depth?symbol=${symbol}&limit=${DEPTH_SNAPSHOT_LIMIT}`;
const snapshotUrl = `https://fapi.binance.com/fapi/v1/depth?symbol=${symbol}&limit=${DEPTH_PROFILE_LEVELS_PER_SIDE}`;
const streamUrl = `wss://fstream.binance.com/public/ws/${symbol.toLowerCase()}@depth@100ms`;

function emitStatus(status, detail = '') {
Expand Down
46 changes: 33 additions & 13 deletions src/binance-orderbook-trade/dom/depth-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,23 +243,40 @@ export function setDepthProfileGeometry(root, geometry) {
if (root.style.height !== height) root.style.height = height;
}

function mapVisibleLevels(levels, geometry) {
return levels.map((level) => ({
...level,
y: geometry.priceToCoordinate(level.price),
})).filter((level) => Number.isFinite(level.y));
function bucketVisibleLevels(levels, geometry) {
const lastRow = Math.max(0, Math.ceil(geometry.height) - 1);
const buckets = new Map();
for (const level of levels) {
const coordinate = geometry.priceToCoordinate(level.price);
if (!Number.isFinite(coordinate)) continue;
const y = Math.max(0, Math.min(lastRow, Math.round(coordinate)));
const current = buckets.get(y);
// Several exchange prices can map to one chart pixel. The outermost level has the
// largest cumulative quantity and preserves the complete depth represented by that row.
if (!current || level.cumulative > current.cumulative) {
buckets.set(y, { ...level, y });
}
}
return [...buckets.values()].sort((left, right) => left.y - right.y);
}

function drawSide(context, levels, profile, width, height, color) {
function drawSide(context, levels, maxVisibleCumulative, width, color) {
if (!levels.length) return;
const levelHeight = Math.max(1, Math.min(5, height / (levels.length * 2.4)));
context.fillStyle = color;
for (const level of levels) {
const barWidth = (level.cumulative / profile.maxCumulative) * width;
context.fillRect(width - barWidth, level.y - levelHeight / 2, barWidth, levelHeight);
const barWidth = (level.cumulative / maxVisibleCumulative) * width;
context.fillRect(width - barWidth, level.y, barWidth, 1);
}
}

function getMaxVisibleCumulative(...levelGroups) {
let maximum = 0;
for (const levels of levelGroups) {
for (const level of levels) maximum = Math.max(maximum, level.cumulative);
}
return maximum;
}

export function renderDepthProfile(root, profile, geometry, currentPrice) {
setDepthProfileGeometry(root, geometry);
const canvas = root.querySelector('.jh-depth-profile-canvas');
Expand All @@ -275,10 +292,13 @@ export function renderDepthProfile(root, profile, geometry, currentPrice) {

context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
context.clearRect(0, 0, rect.width, rect.height);
const asks = mapVisibleLevels(profile.asks, geometry);
const bids = mapVisibleLevels(profile.bids, geometry);
drawSide(context, asks, profile, rect.width, rect.height, 'rgba(246, 70, 93, .30)');
drawSide(context, bids, profile, rect.width, rect.height, 'rgba(14, 203, 129, .30)');
const asks = bucketVisibleLevels(profile.asks, geometry);
const bids = bucketVisibleLevels(profile.bids, geometry);
const maxVisibleCumulative = getMaxVisibleCumulative(asks, bids);
if (maxVisibleCumulative > 0) {
drawSide(context, asks, maxVisibleCumulative, rect.width, 'rgba(246, 70, 93, .30)');
drawSide(context, bids, maxVisibleCumulative, rect.width, 'rgba(14, 203, 129, .30)');
}

const currentPriceY = geometry.priceToCoordinate(currentPrice);
if (!Number.isFinite(currentPriceY)) return true;
Expand Down
2 changes: 1 addition & 1 deletion src/binance-orderbook-trade/index.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @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
// @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
// @version 2.7.182
// @version 2.7.183
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down
48 changes: 47 additions & 1 deletion test/dom/binance-orderbook-trade/depth-profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ test('draws bid and ask bars plus the latest-trade divider', () => {
assert.equal(calls.filter((call) => Array.isArray(call) && call[0] === 'fillRect').length, 2);
assert.deepEqual(
calls.filter((call) => Array.isArray(call) && call[0] === 'fillRect').map((call) => call[2]),
[77.5, 177.5],
[80, 180],
);
assert.deepEqual(calls.find((call) => Array.isArray(call) && call[0] === 'moveTo'), ['moveTo', 0, 140.5]);
assert.equal(calls.includes('stroke'), true);
Expand All @@ -245,6 +245,52 @@ test('draws bid and ask bars plus the latest-trade divider', () => {
assert.deepEqual(calls, ['save', 'setTransform', 'clearRect', 'restore']);
});

test('draws one bar per visible CSS pixel row and scales width to visible depth', () => {
const dom = createChartDom();
const { document } = dom.window;
const { host } = findDepthProfileHost(document);
const root = ensureDepthProfileView(document, host, { onToggle: () => {} });
const fillRects = [];
const canvas = root.querySelector('canvas');
canvas.getContext = () => ({
beginPath: () => {},
clearRect: () => {},
fillRect: (...args) => fillRects.push(args),
lineTo: () => {},
moveTo: () => {},
restore: () => {},
save: () => {},
setLineDash: () => {},
setTransform: () => {},
stroke: () => {},
});

const geometry = {
top: 0,
height: 240,
priceToCoordinate: (price) => ({
99: 180.4,
101: 80.2,
102: 80.4,
}[price] ?? null),
};
renderDepthProfile(root, {
maxCumulative: 1_000,
bids: [{ price: 99, cumulative: 4 }],
asks: [
{ price: 101, cumulative: 3 },
{ price: 102, cumulative: 8 },
{ price: 110, cumulative: 1_000 },
],
}, geometry, 100);

assert.equal(fillRects.length, 2);
assert.deepEqual(fillRects.map(([, y, , height]) => [y, height]), [[80, 1], [180, 1]]);
assert.equal(fillRects[0][0], 0);
assert.equal(fillRects[0][2], canvas.getBoundingClientRect().width);
assert.equal(fillRects[1][2], canvas.getBoundingClientRect().width / 2);
});

test('updates root geometry without rewriting unchanged styles', () => {
const dom = createChartDom();
const { document } = dom.window;
Expand Down
18 changes: 18 additions & 0 deletions test/unit/binance-orderbook-trade/depth-profile-book.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
applyDepthProfileSnapshot,
buildDepthProfile,
createDepthProfileBook,
DEPTH_PROFILE_LEVELS_PER_SIDE,
DepthProfileSequenceError,
pushDepthProfileUpdate,
} from '../../../src/binance-orderbook-trade/core/depth-profile-book.js';
Expand Down Expand Up @@ -120,3 +121,20 @@ test('builds a symmetric vertical price range with cumulative depth', () => {
assert.deepEqual(profile.asks.map((level) => level.cumulative), [4, 10, 20]);
assert.equal(profile.maxCumulative, 20);
});

test('builds every level available from the 1000-level snapshot by default', () => {
const book = createDepthProfileBook('BTCUSDT');
book.ready = true;
for (let index = 0; index < DEPTH_PROFILE_LEVELS_PER_SIDE + 1; index += 1) {
book.bids.set(String(100 - index / 1000), '1');
book.asks.set(String(101 + index / 1000), '1');
}

const profile = buildDepthProfile(book);

assert.equal(DEPTH_PROFILE_LEVELS_PER_SIDE, 1000);
assert.equal(profile.bids.length, 1000);
assert.equal(profile.asks.length, 1000);
assert.equal(profile.bids.at(-1).cumulative, 1000);
assert.equal(profile.asks.at(-1).cumulative, 1000);
});
Loading