אנחנו צוות של מהנדסי Web3 עם ניסיון של 10+ שנים בפיתוח תשתיות קריפטו. יישמנו למעלה מ-20 פרויקטים עבור בורסות וחברות מסחר, כולל מערכות אחסון של ספר הזמנות המטפלות בעד 5000 עדכונים בשנייה. בבורסות קריפטו, ספר ההזמנות הוא אחד ממקורות הנתונים האינטנסיביים ביותר. סוחרים דורשים זמן אחזור נמוך, בעוד שאנליסטים דורשים היסטוריה מלאה. אחסון לא נכון מוביל לעלויות תשתית עצומות.
נתוני ספר ההזמנות הם האינפורמטיביים ביותר אך גם המאתגרים ביותר לאחסון. ספר מלא של BTC/USDT בבינאנס מכיל 5000 רמות בכל צד, מתעדכן 5–10 פעמים בשנייה, ומייצר מאות מגה-בייט לשעה. עם גישה נאיבית (אחסון כל תמונת מצב), הנפח מגיע ל-100 ג'יגה-בייט ביום עבור סמל בודד. מערכת נכונה מאזנת בין שלמות הנתונים לבין אילוצים מעשיים. הפתרון שלנו משלב תמונות מצב מלאות ודלתות (diff), ומשיג דחיסה של פי 50 ללא אובדן רזולוציה.
באיזה פורמט אחסון של ספר הזמנות לבחור?
לפני תכנון האחסון, חשוב להבין אילו נתונים באמת נדרשים. הטבלה שלהלן משווה בין הפורמטים העיקריים.
| סוג נתונים | גודל (לעדכון) | תדירות כתיבה | מקרה שימוש |
|---|---|---|---|
| תמונת מצב מלאה | 8–15 ק"ב | פעם בדקה | שחזור מצב, גיבויים |
| תמונת עומק (20 רמות) | 200–500 בתים | 1–5 בשנייה | אסטרטגיות מסחר, ויזואליזציה |
| דלתא של ספר הזמנות | 150–300 בתים | כל עדכון | רזולוציה ברמת שנייה בין תמונות מצב |
| מחיר אמצע + מרווח | 40 בתים | כל עדכון | ניתוח ארוך טווח, ניטור |
בפועל, מערכות מאחסנות שילוב: תמונות מצב מלאות לשחזור ודלתות לדיוק היסטורי.
פורמט אחסון: קידוד דלתא
קידוד דלתא הוא קריטי להקטנת הנפח. במקום לשמור את הספר המלא, אנו שומרים רק שינויים יחסית למצב הקודם.
Snapshot @ T=0:
bids: [(43250.0, 1.5), (43249.5, 2.0), (43249.0, 0.8)]
asks: [(43251.0, 1.2), (43251.5, 3.0), (43252.0, 0.5)]
Diff @ T=1 (только изменения):
bids_updated: [(43250.0, 2.1)] # объём изменился
bids_removed: [(43249.5, 0)] # уровень исчез
bids_added: [(43248.5, 1.0)] # новый уровень
asks_updated: []
asks_removed: []
asks_added: [(43251.75, 0.3)]
תמונת מצב מלאה: ~8 ק"ב. דלתא: ~200 בתים. ב-5 עדכונים בשנייה ותמונת מצב כל 60 שניות — 300 דלתות + תמונת מצב אחת = ~60 ק"ב לדקה במקום 3 מ"ב לדקה. רווח: פי 50.
למה ClickHouse היא הבחירה האופטימלית?
אנו משתמשים ב-ClickHouse עם סריאליזציה מותאמת אישית. אחסון עמודי ותמיכה במערכים של tuples אידיאליים למבנה ספר ההזמנות. דחיסת ZSTD מקטינה עוד יותר את הנפח. לפי תיעוד ClickHouse, אחסון עמודי ו-ZSTD יכולים לדחוס נתונים מספריים פי 2-3 ביעילות רבה יותר מאשר LZ4.
CREATE TABLE orderbook_snapshots (
exchange LowCardinality(String),
symbol LowCardinality(String),
snapshot_time DateTime64(3, 'UTC'),
depth UInt16,
bids Array(Tuple(Decimal(24,8), Decimal(24,8))),
asks Array(Tuple(Decimal(24,8), Decimal(24,8)))
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(snapshot_time))
ORDER BY (exchange, symbol, snapshot_time);
CREATE TABLE orderbook_diffs (
exchange LowCardinality(String),
symbol LowCardinality(String),
diff_time DateTime64(3, 'UTC'),
first_update_id UInt64,
last_update_id UInt64,
bids_changes Array(Tuple(Decimal(24,8), Decimal(24,8))),
asks_changes Array(Tuple(Decimal(24,8), Decimal(24,8)))
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(diff_time))
ORDER BY (exchange, symbol, diff_time);
CREATE TABLE orderbook_metrics (
exchange LowCardinality(String),
symbol LowCardinality(String),
ts DateTime64(3, 'UTC'),
mid_price Decimal(24,8),
spread Decimal(24,8),
spread_bps Decimal(10,4),
bid_1 Decimal(24,8),
ask_1 Decimal(24,8),
bid_vol_10 Decimal(24,8),
ask_vol_10 Decimal(24,8),
imbalance Decimal(10,6)
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(ts))
ORDER BY (exchange, symbol, ts)
SETTINGS default_codec = ZSTD(3); שחזור מצב ספר ההזמנות
הפעולה המרכזית היא שחזור הספר בנקודת זמן שרירותית. זה מיושם על ידי החלת דלתות ברצף מתמונת המצב האחרונה.
class OrderBookReplay:
def __init__(self, storage: OrderBookStorage):
self.storage = storage
async def reconstruct_at(self, exchange: str, symbol: str, target_ts: int) -> OrderBook:
snapshot = await self.storage.get_last_snapshot_before(exchange, symbol, target_ts)
if not snapshot:
raise ValueError("No snapshot available before target timestamp")
diffs = await self.storage.get_diffs(exchange, symbol, from_ts=snapshot.timestamp, to_ts=target_ts)
book = OrderBook.from_snapshot(snapshot)
for diff in diffs:
book.apply_diff(diff)
return book
class OrderBook:
def apply_diff(self, diff: OrderBookDiff):
for price, qty in diff.bids_changes:
if qty == 0:
self.bids.pop(price, None)
else:
self.bids[price] = qty
for price, qty in diff.asks_changes:
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
חשוב להחיל דלתות בסדר ולאמת באמצעות update_id — בבינאנס, לכל דלתא יש lastUpdateId, והבאה חייבת להתחיל עם lastUpdateId+1. פער פירושו נתונים חסרים.
דחיסה ואופטימיזציה
לפני כתיבה ל-ClickHouse, אנו מיישמים:
- קידוד דלתא למחירים: אחסון ההפרש מההצעה/הדרישה הטובה ביותר בנקודות בסיס (bps). מספרים שלמים נדחסים טוב יותר.
- סריאליזציה בינארית: Protocol Buffers או MessagePack במקום JSON. רווח של פי 3–5 בגודל ובמהירות.
- דחיסת ClickHouse: אלגוריתם ZSTD(3) עבור נתוני Decimal ו-Float — יעיל ב-20% יותר מ-LZ4 ברירת המחדל.
קליטת זרם
צינור הקליטה פועל במקביל: תמונות מצב כל 60 שניות, דלתות במאגר ונשמרות בקבוצות של 100.
class OrderBookIngester:
SNAPSHOT_INTERVAL = 60
DIFF_BATCH_SIZE = 100
def __init__(self, storage):
self.storage = storage
self.diff_buffer = []
self.last_snapshot_time = 0
async def on_orderbook_update(self, book: OrderBook, diff: OrderBookDiff):
now = time.time()
if now - self.last_snapshot_time >= self.SNAPSHOT_INTERVAL:
await self.storage.save_snapshot(book.to_snapshot())
self.last_snapshot_time = now
self.diff_buffer.append(diff)
if len(self.diff_buffer) >= self.DIFF_BATCH_SIZE:
await self.storage.save_diffs(self.diff_buffer)
self.diff_buffer.clear()
שאילתות אנליטיות
לאחר צבירת נתונים, ניתוח הופך לאפשרי. לדוגמה, מרווח ממוצע לפי שעה או מתאם בין חוסר איזון לתנועת מחיר.
-- Средний спред BTC/USDT по часам за выбранный месяц
SELECT toStartOfHour(ts) AS hour,
avg(spread_bps) AS avg_spread_bps,
avg(imbalance) AS avg_imbalance
FROM orderbook_metrics
WHERE exchange = 'binance'
AND symbol = 'BTC/USDT'
AND ts BETWEEN '2024-01-01' AND '2024-02-01'
GROUP BY hour
ORDER BY hour;
-- Корреляция imbalance с последующим движением цены
WITH book AS (
SELECT ts, imbalance, mid_price
FROM orderbook_metrics
WHERE exchange = 'binance'
AND symbol = 'BTC/USDT'
),
future AS (
SELECT b.ts,
b.imbalance,
(f.mid_price - b.mid_price) / b.mid_price * 10000 AS fwd_return_bps
FROM book b
ASOF JOIN book f
ON b.symbol = f.symbol
AND f.ts BETWEEN b.ts + INTERVAL 1 MINUTE AND b.ts + INTERVAL 2 MINUTE
)
SELECT round(imbalance, 1) AS imbalance_bucket,
avg(fwd_return_bps) AS avg_1min_return_bps,
count() AS count
FROM future
GROUP BY imbalance_bucket
ORDER BY imbalance_bucket; ניטור ואיכות נתונים
קריטי לעקוב אחר פערים ברצפי דלתא. מערכת האימות משווה את lastUpdateId של כל דלתא עם firstUpdateId של הבאה ומתריעה על פערים. פער בין תמונות מצב הופך שחזור לבלתי אפשרי.
מדדים לניטור: תדירות כתיבת תמונות מצב לפי סמל, זמן אחזור מחותמת זמן של הבורסה לכתיבת ClickHouse, גודל מאגר דלתא, אחוז עדכונים שהוחמצו.
רשימת בדיקה לאיכות נתונים
- ודא רצף של update_id בדלתות - ודא שמרווח תמונות המצב אינו עולה על 60 שניות - נטר זמן אחזור כתיבה (צריך להיות < 1 שנייה) - שחזר מעת לעת ספר של סמל בדיקה והשווה עם תמונת המצב העדכניתתהליך
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח דרישות | 2-3 ימים | מפרט טכני, אב טיפוס סכמה |
| עיצוב סכמה | 3-5 ימים | דיאגרמת ER, בחירת כלים |
| יישום צינור | 5-10 ימים | קליטה עובדת, בדיקות |
| פיתוח API | 3-5 ימים | תיעוד, דוגמאות שאילתות |
| ניטור וניפוי באגים | 2-3 ימים | לוחות מחוונים, התראות |
| תיעוד והדרכה | 1-2 ימים | README, הוראות |
ציר זמן משוער: 2 עד 4 שבועות תלוי במורכבות. העלות מחושבת באופן אישי לאחר סקירת המשימה.
מה כלול
- עיצוב סכמת אחסון מותאמת לעומס שלך (תדירות עדכונים, מספר סמלים, דרישות זמן אחזור).
- יישום צינור קליטה ב-Python עם אינטגרציה של WebSocket או REST API.
- פיתוח API לגישה לנתונים היסטוריים (שחזור ספר, שליפת דלתות, אגרגטים).
- תיעוד לשחזור ושאילתות אנליטיות.
- הדרכת צוות.
- חודש תמיכה לאחר ההשקה.
אם אתה מעוניין באופטימיזציה של אחסון נתוני בורסה, צור קשר להערכה מקדימה. פנה אלינו להערכת הפרויקט שלך. הזמן מערכת אחסון ספר הזמנות סוהרת וקבל ייעוץ על ארכיטקטורה ולוחות זמנים.







