mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 12:00:59 +08:00
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:
@@ -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))
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user