博客 / 詳情

返回

從一個開發工程師的角度,聊聊“什麼是 K 線”

先把話挑明:K 線不是“畫出來”的,而是“算出來”的。它是對一段時間內價格與成交的壓縮表示:Open、High、Low、Close、Volume(常加 Turnover、交易筆數),按某種“窗口”聚合而成。聽起來像個普通的聚合任務,但真要把它在交易系統裏做穩、做快、做準,會踩很多坑。

K 線到底指什麼

  • 時間維度:常見有 1s、1m、5m、15m、1h、日/周/月 K。不是自然時間,而是“交易所時間”。比如 A 股有午休,美股有夏令時,國內期貨有夜盤。
  • 事件來源:一般從成交(Trade)事件聚合,也有用最新成交價驅動的 Quote K(更接近行情視角)。有的市場會有撤銷/更正(Cancel/Correct)事件。
  • 數值字段:價格精度、最小變動價位、合約乘數、成交量單位(股/手/張)、貨幣與匯率,這些元數據必須進來一起算。
  • 類型變體:時間 K(最常見)、成交量 K(每 X 手出一根)、價格區間 K(Range Bar)、平均 K(Heikin Ashi/VWAP 相關)。業務要説清楚要哪種。

如果要在生產系統裏做,這些是會“咬你”的點

1) 時間邊界和交易日曆

  • 不同市場的開閉市/午休/夜盤/節假日/臨時停牌,導致“窗口”不是整齊的自然時間。
  • 夏令時切換會出現 61 分鐘或 59 分鐘的“鍾”,不要按本地時區算,要用交易所時區。
  • 周/月 K 的邊界不是自然周/月,遇到長假要正確收口。

解決思路

  • 維護“交易日曆服務”,給出某標的在某天的有效交易片段,預先切好每個週期的窗口。
  • 一律用 java.time 下的 ZonedDateTime 並固定為交易所時區,禁止用系統默認時區。
  • 對跨日/夜盤的窗口,用 sessionId(如 20250809-Night)做鍵的一部分。

2) 數據順序、重複與遲到

  • 實時流裏常見亂序、重複、延遲到達。極端情況下還會收到撤銷/更正(對某筆成交)。
  • 分區策略不當會把同一標的打到不同分區,順序直接沒了。

解決思路

  • 流分區按 symbol(甚至 symbol+venue)哈希,保證單分區內順序;同一 symbol 聚合必須單線程。
  • 設置可接受的最大亂序窗口(比如 3 秒),窗口內遲到更新允許回補,窗口外生成“更正事件”異步修正歷史。
  • 去重用“交易所側唯一鍵”(tradeId/matchNumber),每個活躍窗口掛一個 LRU Set 或 BloomFilter,窗口關了再銷燬。
  • 對 Cancel/Correct,保留事件日誌或增量計數,支持對受影響窗口重算。

3) 無成交時如何“出 K”

  • 這一分鐘沒人成交,要不要出一根 K?出的話 open/high/low/close 是啥?
  • 國內股票常見規則是用上一成交價填充 O=H=L=C,Volume=0;有的業務要求“不出空 K”。

解決思路

  • 把規則提前固化到配置裏,按交易所/品種/週期做矩陣開關。
  • 即使不出空 K,窗口邊界也要推動 close 更新(否則下游指標會串)。

4) 價格精度與溢出

  • BigDecimal 慢,double 有誤差,long 容易溢出,turnover(成交額)乘合約乘數後尤甚。
  • 匯率轉換會引入更多精度與溢出風險。

解決思路

  • 價格、數量都用“整數化”的 long 存(如價格按最小价位或 1e4 縮放),只在邊界 I/O 處做格式化。
  • 成交額用 128 位(Java 裏 BigDecimal,但複用對象與避免頻繁創建),或分幣種分桶存 long 後離線彙總。
  • 提前加載 instrument 元數據:tickSizepriceScalelotmultipliercurrency

5) 聚合策略與層級

  • 從 tick 直接聚到所有周期 vs 先聚 1 分鐘再二次聚合到 5 分鐘/15 分鐘?
  • 直接從 tick 聚到所有周期,CPU 壓力大;二次聚合可能引入邊界誤差(跨窗口的高低點)。

解決思路

  • 統一以“最小基礎週期”(一般 1S 或 1M)做底座,其他週期用基礎週期二次聚合,且二次聚合以“max(high)min(low)、開=首根 open、收=末根 close、量/額累加”的嚴格規則,避免誤差。
  • 僅對需要的週期開啓實時聚合,其他由查詢側即時拼裝。

6) 修正與復權

  • 日線以上要處理分紅、配股、拆合股,對歷史 K 進行前/後復權。
  • 期貨連續合約、主力切換,怎麼拼接不會斷層。

解決思路

  • 復權係數維護為“日期->factor”的時間序列,原始 K 只存不復權,查詢時按前/後復權在線轉換:adjPrice = raw * factor(t) / factor(now)
  • 連續合約策略分主力/指數/近月滾動,邊界日給出映射表,生成“連續映射事件”,驅動重算或查詢期融合。

7) 性能與 GC

  • 高頻市場每秒幾十萬條 tick,分鐘邊界瞬時抖動明顯。
  • 大量對象創建會引發 GC 抖動,延遲尾部很難看。

解決思路

  • 單 symbol 單線程聚合,事件循環用 Disruptor 或 Chronicle Queue/RingBuffer,減少鎖。
  • 對象池化,Candle、事件包裝重用;primitive 集合(fastutil/HPPC),儘量零裝箱。
  • 延遲敏感路徑避免 BigDecimal 運算,批量落盤,內存對齊。
  • 分時“削峯”:分鐘收口延後幾百毫秒出 K(業務可接受範圍內),換吞吐量。

8) 存儲與接口

  • 歷史查詢高 QPS,實時訂閲低延遲,冷熱數據分層。
  • 一致性:用户拉歷史與訂閲實時,不能看到“撕裂”的最後一根。

解決思路

  • 實時內存狀態 + 週期性落盤歷史庫(如 ClickHouse/QuestDB/TimescaleDB/Parquet)。
  • 歷史 REST 拉取採用“快照點”概念,快照後的增量通過 WebSocket 推送,給最後一根帶版本號或校驗碼。
  • 分區按交易日/標的,壓縮列式存儲,支持前綴查詢。

一個精簡版的聚合器結構(刪繁就簡)

數據模型long 代表已整數化的價格與量):

  • CandleKey: symbol, interval, sessionId, windowStart
  • CandleState: open, high, low, close, volume, turnover, tradeCount, version

核心流程

  • 事件進入(按 symbol 分區、單線程消費)
  • 根據交易日曆找到它屬於哪個窗口
  • 如窗口不存在則創建並初始化 open/high/low/close
  • 應用成交事件更新高低收、量額
  • 檢查窗口是否到期,到期則封口輸出並落盤;同時滾動到下一窗口
  • 遲到事件:若還在“可回補期”,直接更新狀態並廣播修正;否則記錄更正任務

非常小的一段 Java 偽代碼(僅示意,真實實現要更多邊界判斷)

class Candle {

long open = Long.MIN\_VALUE;

long high = Long.MIN\_VALUE;

long low = Long.MAX\_VALUE;

long close = Long.MIN\_VALUE;

long volume;

long turnover;

int trades;

void applyTrade(long px, long qty) {
if (open == Long.MIN_VALUE) open = px;
if (px > high) high = px;
if (px < low) low = px;
close = px;
volume += qty;
turnover += px * qty; // 注意溢出與縮放
trades++;
}

boolean isEmpty() {
return open == Long.MIN_VALUE;
}

}

class Aggregator {

final Map map = new HashMap<>();

void onTrade(Trade t) {
CandleKey key = calendar.locateWindow(t.symbol, t.exchangeTime, t.session);
Candle c = map.computeIfAbsent(key, k -> new Candle());
if (t.isCancelOrCorrect()) {
// 查找原事件並回滾/重算(需要事件日誌)
return;
}
c.applyTrade(t.price, t.qty);
}

void onTickBoundary(Instant now) {
// 找到到期的窗口,封口輸出,落盤並清理
}
}

實現時別忘了這些“坑口提示”

  • 集合競價:開盤/收盤集合競價形成的價量要算進對應窗口,尤其是收盤價定義要跟業務確認(最後成交價 vs 收盤價)。
  • 交易所回補:鏈路斷開後的回補數據順序可能與實時不同,回放時要沿用同一聚合邏輯,且要可冪等。
  • 標的元數據變更:停復牌、最小變動價位與合約乘數變化(再遇見就是真實世界),要有生效時間點。
  • 跨市場合並:港股/美股幣種不同,turnover 彙總別悄悄相加。
  • 壓力測試:用歷史 tick 回放到 Kafka,按真實峯值放大 2 倍,觀察分鐘邊界延遲尾部和丟包率。
  • 監控:窗口實時數量、亂序比率、遲到修正次數、分鐘邊界 99.9 延遲、落盤滯後、GC 暫停、每標的事件速率。

我會怎麼落地

  • 入口:Kafka/NATS/ChronicleQueue,按 symbol 分區,消費端單線程聚合。
  • 時間:交易日曆服務(內置規則 + 可熱更新),所有時間用交易所時區。
  • 聚合:1 秒或 1 分鐘作為基礎週期,其他週期二次聚合。遲到容忍窗口 3 秒,超時轉“修正任務隊列”。
  • 去重:每窗口維護 LRU tradeId 集合;全局 Bloom 限制內存。
  • 存儲:實時 Redis/內存快照,分鐘/日線落 ClickHouse;寫前批量,按 symbol+date 分區;歷史修正以 upsert。
  • 對外:REST 歷史 + WebSocket 訂閲;訂閲流包含“完整 K”“修正 K”兩類事件;最後一根攜帶 version。
  • 性能:Disruptor 事件環,預分配對象;fastutil LongOpenHashSet 做去重;少用 BigDecimal,必要處彙總線程集中轉換。
  • 測試:重放歷史包,屬性測試校驗 O/H/L/C 不變量,邊界日(節假日、夏令時、夜盤)專項用例。

寫在最後

K 線是“低級需求,高級實現”。從產品視角它只是幾根柱子,從工程視角它是時間、數據質量、併發與業務規則的交叉地帶。真正難的不是把 open/high/low/close 算出來,而是:

  • 任意時刻説得清“為什麼是這個值”
  • 在異常和修正下仍然可追溯、可重放、可冪等
  • 在高峯時段穩穩地按時吐出每一根

如果你正準備做這件事,先把時區、交易日曆、事件順序、去重與修正理順,再談性能優化;這樣第二天早上看監控的時候,心裏會更踏實。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.