fix: align order markers to kline candles using binary search

- Use binary search to find exact kline candle for each order
- Sort markers by time as required by lightweight-charts
- Filter orders outside kline data range
- Based on lightweight-charts issue #1182 recommendation
This commit is contained in:
tinkle-community
2025-12-27 19:38:01 +08:00
parent 8fb0d2e7e9
commit e204707845
2 changed files with 105 additions and 43 deletions

View File

@@ -446,37 +446,70 @@ export function AdvancedChart({
if (orders.length > 0) {
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders')
// 过滤掉无效时间戳的订单小于2024年的时间戳
const minValidTimestamp = new Date('2024-01-01').getTime() / 1000
const validOrders = orders.filter(order => {
if (order.time < minValidTimestamp) {
console.warn('[AdvancedChart] ⚠️ Skipping order with invalid timestamp:', order.time, '(', new Date(order.time * 1000).toISOString(), ')')
return false
// 提取 K 线时间数组(已排序
const klineTimes = klineData.map((k: any) => k.time as number)
const klineMinTime = klineTimes[0] || 0
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
// 二分查找:找到订单时间所属的 K 线蜡烛
// 返回 time <= orderTime 的最大 K 线时间
const findCandleTime = (orderTime: number): number | null => {
if (orderTime < klineMinTime || orderTime > klineMaxTime) {
return null // 超出范围
}
return true
})
console.log('[AdvancedChart] Valid orders:', validOrders.length, 'out of', orders.length)
let left = 0
let right = klineTimes.length - 1
while (left < right) {
const mid = Math.ceil((left + right + 1) / 2)
if (klineTimes[mid] <= orderTime) {
left = mid
} else {
right = mid - 1
}
}
return klineTimes[left]
}
// 过滤并对齐订单到 K 线时间
const markers: Array<{
time: Time
position: 'belowBar'
color: string
shape: 'circle'
text: string
size: number
}> = []
orders.forEach(order => {
// 使用二分查找找到对应的 K 线蜡烛时间
const candleTime = findCandleTime(order.time)
if (candleTime === null) {
console.warn('[AdvancedChart] ⚠️ Skipping order outside kline range:',
order.time, '(', new Date(order.time * 1000).toISOString(), ')')
return
}
const markers = validOrders.map(order => {
// 直接使用 rawSide 字段判断买卖(更准确)
// rawSide = 'buy' → 绿色 B
// rawSide = 'sell' → 红色 S
const isBuy = order.rawSide === 'buy'
const marker = {
time: order.time as Time,
markers.push({
time: candleTime as Time,
position: 'belowBar' as const,
color: isBuy ? '#0ECB81' : '#F6465D', // BUY绿色, SELL红色
shape: 'circle' as const, // 使用圆形作为背景
text: isBuy ? 'B' : 'S', // 显示 B 或 S
size: 1, // 稍微大一点以显示文字
}
console.log('[AdvancedChart] ✅ Created marker:', marker.text, 'for', order.rawSide, 'at', new Date(order.time * 1000).toISOString())
return marker
color: isBuy ? '#0ECB81' : '#F6465D',
shape: 'circle' as const,
text: isBuy ? 'B' : 'S',
size: 1,
})
})
// 按时间排序lightweight-charts 要求标记按时间顺序)
markers.sort((a, b) => (a.time as number) - (b.time as number))
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length)
console.log('[AdvancedChart] Setting', markers.length, 'markers on candlestick series')
console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))

View File

@@ -311,6 +311,29 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
candlestickSeriesRef.current.setData(klineData)
// 构建 K 线时间集合,用于快速查找
const klineTimeSet = new Set(klineData.map(k => k.time as number))
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
// 计算时间周期的秒数
const getIntervalSeconds = (interval: string): number => {
const match = interval.match(/(\d+)([smhd])/)
if (!match) return 60 // 默认1分钟
const [, num, unit] = match
const n = parseInt(num)
switch (unit) {
case 's': return n
case 'm': return n * 60
case 'h': return n * 3600
case 'd': return n * 86400
default: return 60
}
}
const intervalSeconds = getIntervalSeconds(interval)
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
// 2. 获取订单数据并添加标记
if (traderID) {
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
@@ -321,36 +344,42 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] No orders to display')
}
// 过滤掉无效时间戳的订单小于2024年的时间戳
const minValidTimestamp = new Date('2024-01-01').getTime() / 1000
const validOrders = orders.filter(order => {
if (order.time < minValidTimestamp) {
console.warn('[ChartWithOrders] ⚠️ Skipping order with invalid timestamp:', order.time, '(', new Date(order.time * 1000).toISOString(), ')')
return false
// 转换订单为图表标记,并对齐到 K 线时间
const markers: Array<{
time: Time
position: 'belowBar'
color: string
shape: 'circle'
text: string
price: number
size: number
}> = []
orders.forEach((order) => {
// 将订单时间对齐到 K 线周期(向下取整)
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
// 检查对齐后的时间是否在 K 线数据中存在
if (!klineTimeSet.has(alignedTime)) {
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')')
return
}
return true
})
console.log('[ChartWithOrders] Valid orders:', validOrders.length, 'out of', orders.length)
// 转换订单为图表标记 - 简洁版:只用 B/S
const markers = validOrders.map((order) => {
// 使用 side 字段判断买卖(更准确)
// side = BUY → 绿色 B
// side = SELL → 红色 S
const isBuy = order.side === 'BUY'
return {
time: order.time as Time,
markers.push({
time: alignedTime as Time,
position: 'belowBar' as const,
color: isBuy ? '#0ECB81' : '#F6465D',
shape: 'circle' as const,
text: isBuy ? 'B' : 'S',
price: order.price,
size: 1,
}
})
})
console.log('[ChartWithOrders] Valid markers (with matching klines):', markers.length, 'out of', orders.length)
console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
try {