Files
xiaobaifupan/next/backend/data/repository.py
T

272 lines
8.9 KiB
Python

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 active_stock_count(self, connection: sqlite3.Connection) -> int:
row = connection.execute(
"""
SELECT COUNT(*) AS count FROM market_entities
WHERE entity_type = 'stock' AND active = 1
"""
).fetchone()
return int(row["count"] if row else 0)
def save_summary(
self,
connection: sqlite3.Connection,
*,
trade_date: str,
observed_at: str,
state: str,
source: str,
coverage: float,
payload: dict[str, Any],
) -> None:
connection.execute(
"""
INSERT INTO market_summaries
(trade_date, observed_at, state, source, coverage, payload_json, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(trade_date) DO UPDATE SET
observed_at = excluded.observed_at,
state = excluded.state,
source = excluded.source,
coverage = excluded.coverage,
payload_json = excluded.payload_json,
created_at = excluded.created_at
""",
(
trade_date,
observed_at,
state,
source,
coverage,
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
datetime.now().astimezone().isoformat(timespec="seconds"),
),
)
def summaries(
self, connection: sqlite3.Connection, through: str, limit: int = 260
) -> tuple[sqlite3.Row, ...]:
rows = connection.execute(
"""
SELECT * FROM market_summaries WHERE trade_date <= ?
ORDER BY trade_date DESC LIMIT ?
""",
(through, limit),
).fetchall()
return tuple(reversed(rows))
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 "")),
),
)
)