rebuild(stage-5): establish market data gateway and charts
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from backend.data.contracts import MarketEntity
|
||||
|
||||
|
||||
class MarketRepository:
|
||||
def replace_calendar(
|
||||
self,
|
||||
connection: sqlite3.Connection,
|
||||
rows: tuple[dict[str, Any], ...],
|
||||
source: str,
|
||||
observed_at: str,
|
||||
) -> None:
|
||||
connection.executemany(
|
||||
"""
|
||||
INSERT INTO trading_days (trade_date, is_open, previous_open_date, source, observed_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(trade_date) DO UPDATE SET
|
||||
is_open = excluded.is_open,
|
||||
previous_open_date = excluded.previous_open_date,
|
||||
source = excluded.source,
|
||||
observed_at = excluded.observed_at
|
||||
""",
|
||||
[
|
||||
(
|
||||
_display(str(row.get("cal_date") or "")),
|
||||
1 if int(row.get("is_open") or 0) == 1 else 0,
|
||||
_display(str(row.get("pretrade_date") or "")) or None,
|
||||
source,
|
||||
observed_at,
|
||||
)
|
||||
for row in rows
|
||||
if _display(str(row.get("cal_date") or ""))
|
||||
],
|
||||
)
|
||||
|
||||
def replace_stocks(
|
||||
self,
|
||||
connection: sqlite3.Connection,
|
||||
rows: tuple[dict[str, Any], ...],
|
||||
source: str,
|
||||
observed_at: str,
|
||||
) -> None:
|
||||
connection.executemany(
|
||||
"""
|
||||
INSERT INTO market_entities (
|
||||
entity_type, identifier, code, name, search_key,
|
||||
sector, active, source, observed_at
|
||||
)
|
||||
VALUES ('stock', ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(entity_type, identifier) DO UPDATE SET
|
||||
code = excluded.code,
|
||||
name = excluded.name,
|
||||
search_key = excluded.search_key,
|
||||
sector = excluded.sector,
|
||||
active = excluded.active,
|
||||
source = excluded.source,
|
||||
observed_at = excluded.observed_at
|
||||
""",
|
||||
[
|
||||
(
|
||||
str(row.get("ts_code") or "").upper(),
|
||||
str(row.get("symbol") or ""),
|
||||
str(row.get("name") or "").strip(),
|
||||
_search_key(row),
|
||||
str(row.get("industry") or "").strip() or None,
|
||||
0 if row.get("list_status") == "D" else 1,
|
||||
source,
|
||||
observed_at,
|
||||
)
|
||||
for row in rows
|
||||
if row.get("ts_code") and row.get("symbol") and row.get("name")
|
||||
],
|
||||
)
|
||||
|
||||
def search(
|
||||
self, connection: sqlite3.Connection, query: str, limit: int = 32
|
||||
) -> tuple[MarketEntity, ...]:
|
||||
normalized = _normalize(query)
|
||||
if not normalized:
|
||||
return ()
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT entity_type, identifier, code, name, sector
|
||||
FROM market_entities
|
||||
WHERE active = 1 AND search_key LIKE ?
|
||||
ORDER BY
|
||||
CASE WHEN code = ? THEN 0 WHEN name = ? THEN 1 WHEN code LIKE ? THEN 2 ELSE 3 END,
|
||||
entity_type, name
|
||||
LIMIT ?
|
||||
""",
|
||||
(f"%{normalized}%", normalized, query.strip(), f"{normalized}%", limit),
|
||||
).fetchall()
|
||||
return tuple(MarketEntity(**dict(row)) for row in rows)
|
||||
|
||||
def entity(
|
||||
self, connection: sqlite3.Connection, entity_type: str, identifier: str
|
||||
) -> MarketEntity | None:
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT entity_type, identifier, code, name, sector
|
||||
FROM market_entities WHERE entity_type = ? AND identifier = ? AND active = 1
|
||||
""",
|
||||
(entity_type, identifier),
|
||||
).fetchone()
|
||||
return MarketEntity(**dict(row)) if row else None
|
||||
|
||||
def open_dates(
|
||||
self, connection: sqlite3.Connection, through: str, limit: int = 12
|
||||
) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
str(row["trade_date"])
|
||||
for row in connection.execute(
|
||||
"""
|
||||
SELECT trade_date FROM trading_days
|
||||
WHERE is_open = 1 AND trade_date <= ?
|
||||
ORDER BY trade_date DESC LIMIT ?
|
||||
""",
|
||||
(through, limit),
|
||||
)
|
||||
)
|
||||
|
||||
def latest_summary(self, connection: sqlite3.Connection, through: str) -> sqlite3.Row | None:
|
||||
return connection.execute(
|
||||
"SELECT * FROM market_summaries WHERE trade_date <= ? ORDER BY trade_date DESC LIMIT 1",
|
||||
(through,),
|
||||
).fetchone()
|
||||
|
||||
def save_chart(
|
||||
self,
|
||||
connection: sqlite3.Connection,
|
||||
*,
|
||||
entity_type: str,
|
||||
identifier: str,
|
||||
interval: str,
|
||||
trade_date: str,
|
||||
observed_at: str,
|
||||
source: str,
|
||||
usage: str,
|
||||
adjustment: str,
|
||||
coverage: float,
|
||||
payload: dict[str, Any],
|
||||
) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO chart_series
|
||||
(entity_type, identifier, interval, trade_date, observed_at, source, usage,
|
||||
adjustment, coverage, payload_json, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(entity_type, identifier, interval, trade_date) DO UPDATE SET
|
||||
observed_at = excluded.observed_at,
|
||||
source = excluded.source,
|
||||
usage = excluded.usage,
|
||||
adjustment = excluded.adjustment,
|
||||
coverage = excluded.coverage,
|
||||
payload_json = excluded.payload_json,
|
||||
created_at = excluded.created_at
|
||||
""",
|
||||
(
|
||||
entity_type,
|
||||
identifier,
|
||||
interval,
|
||||
trade_date,
|
||||
observed_at,
|
||||
source,
|
||||
usage,
|
||||
adjustment,
|
||||
coverage,
|
||||
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
||||
datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
),
|
||||
)
|
||||
|
||||
def chart(
|
||||
self, connection: sqlite3.Connection, entity_type: str, identifier: str, interval: str
|
||||
) -> sqlite3.Row | None:
|
||||
return connection.execute(
|
||||
"""
|
||||
SELECT * FROM chart_series
|
||||
WHERE entity_type = ? AND identifier = ? AND interval = ?
|
||||
ORDER BY trade_date DESC LIMIT 1
|
||||
""",
|
||||
(entity_type, identifier, interval),
|
||||
).fetchone()
|
||||
|
||||
|
||||
def _display(value: str) -> str:
|
||||
compact = value.replace("-", "")
|
||||
if len(compact) != 8 or not compact.isdigit():
|
||||
return ""
|
||||
return f"{compact[:4]}-{compact[4:6]}-{compact[6:]}"
|
||||
|
||||
|
||||
def _normalize(value: str) -> str:
|
||||
return re.sub(r"\s+", "", value).casefold()
|
||||
|
||||
|
||||
def _search_key(row: dict[str, Any]) -> str:
|
||||
return " ".join(
|
||||
filter(
|
||||
None,
|
||||
(
|
||||
_normalize(str(row.get("symbol") or "")),
|
||||
_normalize(str(row.get("ts_code") or "")),
|
||||
_normalize(str(row.get("name") or "")),
|
||||
_normalize(str(row.get("industry") or "")),
|
||||
),
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user