first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from packages.db.schema import init_db
|
||||
from packages.db.repository import Repository
|
||||
|
||||
__all__ = ["init_db", "Repository"]
|
||||
@@ -0,0 +1,275 @@
|
||||
"""数据访问。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from packages.db.schema import init_db
|
||||
|
||||
|
||||
def _now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptionQuoteRow:
|
||||
ts_ms: int
|
||||
exchange: str
|
||||
underlying: str
|
||||
inst_id: str
|
||||
expiry_ymd: str
|
||||
strike: float
|
||||
side: str
|
||||
index_px: float
|
||||
ask: float | None
|
||||
bid: float | None
|
||||
ask_sz: float | None
|
||||
bid_sz: float | None
|
||||
leverage: float | None
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, db_path: str | Path) -> None:
|
||||
self.db_path = Path(db_path)
|
||||
self.conn = init_db(self.db_path)
|
||||
|
||||
def close(self) -> None:
|
||||
self.conn.close()
|
||||
|
||||
def insert_option_quote(self, row: OptionQuoteRow) -> int:
|
||||
cur = self.conn.execute(
|
||||
"""
|
||||
INSERT INTO option_quotes (
|
||||
ts_ms, exchange, underlying, inst_id, expiry_ymd, strike, side,
|
||||
index_px, ask, bid, ask_sz, bid_sz, leverage, created_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
row.ts_ms,
|
||||
row.exchange,
|
||||
row.underlying,
|
||||
row.inst_id,
|
||||
row.expiry_ymd,
|
||||
row.strike,
|
||||
row.side,
|
||||
row.index_px,
|
||||
row.ask,
|
||||
row.bid,
|
||||
row.ask_sz,
|
||||
row.bid_sz,
|
||||
row.leverage,
|
||||
_now_ms(),
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
|
||||
def insert_index_tick(
|
||||
self,
|
||||
*,
|
||||
ts_ms: int,
|
||||
exchange: str,
|
||||
underlying: str,
|
||||
index_px: float,
|
||||
) -> int:
|
||||
cur = self.conn.execute(
|
||||
"""
|
||||
INSERT INTO index_ticks (ts_ms, exchange, underlying, index_px, created_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(ts_ms, exchange, underlying, index_px, _now_ms()),
|
||||
)
|
||||
self.conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
|
||||
def upsert_heartbeat(
|
||||
self,
|
||||
*,
|
||||
ok: bool,
|
||||
error: str | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
now = _now_ms()
|
||||
row = self.conn.execute(
|
||||
"SELECT consecutive_failures FROM collector_heartbeat WHERE id = 1"
|
||||
).fetchone()
|
||||
fails = int(row["consecutive_failures"] if row else 0)
|
||||
if ok:
|
||||
fails = 0
|
||||
self.conn.execute(
|
||||
"""
|
||||
UPDATE collector_heartbeat
|
||||
SET last_ok_ts_ms = ?, last_error = NULL, consecutive_failures = 0,
|
||||
meta_json = COALESCE(?, meta_json)
|
||||
WHERE id = 1
|
||||
""",
|
||||
(now, json.dumps(meta, ensure_ascii=False) if meta else None),
|
||||
)
|
||||
else:
|
||||
fails += 1
|
||||
self.conn.execute(
|
||||
"""
|
||||
UPDATE collector_heartbeat
|
||||
SET last_error = ?, last_error_ts_ms = ?, consecutive_failures = ?,
|
||||
meta_json = COALESCE(?, meta_json)
|
||||
WHERE id = 1
|
||||
""",
|
||||
(
|
||||
(error or "unknown")[:2000],
|
||||
now,
|
||||
fails,
|
||||
json.dumps(meta, ensure_ascii=False) if meta else None,
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_heartbeat(self) -> dict[str, Any]:
|
||||
row = self.conn.execute(
|
||||
"SELECT * FROM collector_heartbeat WHERE id = 1"
|
||||
).fetchone()
|
||||
if not row:
|
||||
return {}
|
||||
d = dict(row)
|
||||
meta = d.get("meta_json")
|
||||
if meta:
|
||||
try:
|
||||
d["meta"] = json.loads(meta)
|
||||
except json.JSONDecodeError:
|
||||
d["meta"] = None
|
||||
else:
|
||||
d["meta"] = None
|
||||
return d
|
||||
|
||||
def latest_quotes_by_side(self) -> dict[str, dict[str, Any]]:
|
||||
"""返回 side -> 最新一条。"""
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for side in ("C", "P"):
|
||||
row = self.conn.execute(
|
||||
"""
|
||||
SELECT * FROM option_quotes
|
||||
WHERE side = ?
|
||||
ORDER BY ts_ms DESC, id DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(side,),
|
||||
).fetchone()
|
||||
if row:
|
||||
out[side] = dict(row)
|
||||
return out
|
||||
|
||||
def count_option_quotes(self) -> int:
|
||||
row = self.conn.execute("SELECT COUNT(*) AS n FROM option_quotes").fetchone()
|
||||
return int(row["n"] if row else 0)
|
||||
|
||||
def count_index_ticks(self) -> int:
|
||||
row = self.conn.execute("SELECT COUNT(*) AS n FROM index_ticks").fetchone()
|
||||
return int(row["n"] if row else 0)
|
||||
|
||||
def fetch_option_quotes(
|
||||
self,
|
||||
*,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
side: str = "both",
|
||||
underlying: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""[start_ms, end_ms) 半开区间。"""
|
||||
clauses = ["ts_ms >= ?", "ts_ms < ?"]
|
||||
params: list[Any] = [int(start_ms), int(end_ms)]
|
||||
want = (side or "both").upper()
|
||||
if want in ("C", "P"):
|
||||
clauses.append("side = ?")
|
||||
params.append(want)
|
||||
if underlying:
|
||||
clauses.append("underlying = ?")
|
||||
params.append(underlying)
|
||||
sql = f"""
|
||||
SELECT ts_ms, exchange, underlying, inst_id, expiry_ymd, strike, side,
|
||||
index_px, ask, bid, ask_sz, bid_sz, leverage
|
||||
FROM option_quotes
|
||||
WHERE {' AND '.join(clauses)}
|
||||
ORDER BY ts_ms ASC, id ASC
|
||||
"""
|
||||
rows = self.conn.execute(sql, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_settlement(self, expiry_ymd: str) -> dict[str, Any] | None:
|
||||
row = self.conn.execute(
|
||||
"SELECT * FROM expiry_settlements WHERE expiry_ymd = ?",
|
||||
(expiry_ymd,),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def list_settlements(self, ymds: list[str] | None = None) -> dict[str, dict[str, Any]]:
|
||||
if ymds is not None and not ymds:
|
||||
return {}
|
||||
if ymds is None:
|
||||
rows = self.conn.execute("SELECT * FROM expiry_settlements").fetchall()
|
||||
else:
|
||||
placeholders = ",".join("?" for _ in ymds)
|
||||
rows = self.conn.execute(
|
||||
f"SELECT * FROM expiry_settlements WHERE expiry_ymd IN ({placeholders})",
|
||||
list(ymds),
|
||||
).fetchall()
|
||||
return {str(r["expiry_ymd"]): dict(r) for r in rows}
|
||||
|
||||
def upsert_settlement(
|
||||
self,
|
||||
*,
|
||||
expiry_ymd: str,
|
||||
settle_ts_ms: int,
|
||||
settle_index_px: float,
|
||||
exchange: str,
|
||||
underlying: str,
|
||||
) -> None:
|
||||
self.conn.execute(
|
||||
"""
|
||||
INSERT INTO expiry_settlements (
|
||||
expiry_ymd, settle_ts_ms, settle_index_px, exchange, underlying, created_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(expiry_ymd) DO UPDATE SET
|
||||
settle_ts_ms = excluded.settle_ts_ms,
|
||||
settle_index_px = excluded.settle_index_px,
|
||||
exchange = excluded.exchange,
|
||||
underlying = excluded.underlying
|
||||
""",
|
||||
(
|
||||
expiry_ymd,
|
||||
int(settle_ts_ms),
|
||||
float(settle_index_px),
|
||||
exchange,
|
||||
underlying,
|
||||
_now_ms(),
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def nearest_index_tick(
|
||||
self,
|
||||
*,
|
||||
underlying: str,
|
||||
target_ts_ms: int,
|
||||
max_delta_ms: int,
|
||||
) -> dict[str, Any] | None:
|
||||
row = self.conn.execute(
|
||||
"""
|
||||
SELECT ts_ms, index_px, ABS(ts_ms - ?) AS delta
|
||||
FROM index_ticks
|
||||
WHERE underlying = ?
|
||||
AND ts_ms BETWEEN ? AND ?
|
||||
ORDER BY delta ASC
|
||||
LIMIT 1
|
||||
""",
|
||||
(
|
||||
int(target_ts_ms),
|
||||
underlying,
|
||||
int(target_ts_ms) - int(max_delta_ms),
|
||||
int(target_ts_ms) + int(max_delta_ms),
|
||||
),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
@@ -0,0 +1,75 @@
|
||||
"""SQLite schema(可迁移设计)。时间戳存 UTC ms。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS option_quotes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts_ms INTEGER NOT NULL,
|
||||
exchange TEXT NOT NULL,
|
||||
underlying TEXT NOT NULL,
|
||||
inst_id TEXT NOT NULL,
|
||||
expiry_ymd TEXT NOT NULL,
|
||||
strike REAL NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
index_px REAL NOT NULL,
|
||||
ask REAL,
|
||||
bid REAL,
|
||||
ask_sz REAL,
|
||||
bid_sz REAL,
|
||||
leverage REAL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oq_ts ON option_quotes(ts_ms);
|
||||
CREATE INDEX IF NOT EXISTS idx_oq_side_ts ON option_quotes(side, ts_ms);
|
||||
CREATE INDEX IF NOT EXISTS idx_oq_expiry ON option_quotes(expiry_ymd);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS index_ticks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts_ms INTEGER NOT NULL,
|
||||
exchange TEXT NOT NULL,
|
||||
underlying TEXT NOT NULL,
|
||||
index_px REAL NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_it_ts ON index_ticks(ts_ms);
|
||||
CREATE INDEX IF NOT EXISTS idx_it_u_ts ON index_ticks(underlying, ts_ms);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS expiry_settlements (
|
||||
expiry_ymd TEXT PRIMARY KEY,
|
||||
settle_ts_ms INTEGER NOT NULL,
|
||||
settle_index_px REAL NOT NULL,
|
||||
exchange TEXT NOT NULL,
|
||||
underlying TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collector_heartbeat (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
last_ok_ts_ms INTEGER,
|
||||
last_error TEXT,
|
||||
last_error_ts_ms INTEGER,
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
||||
meta_json TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def init_db(db_path: str | Path) -> sqlite3.Connection:
|
||||
path = Path(db_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(path), check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL;")
|
||||
conn.execute("PRAGMA synchronous=NORMAL;")
|
||||
conn.executescript(SCHEMA_SQL)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO collector_heartbeat (id, consecutive_failures) VALUES (1, 0)"
|
||||
)
|
||||
conn.commit()
|
||||
return conn
|
||||
Reference in New Issue
Block a user