rebuild(stage-6): deliver emotion and market pools
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
from backend.data.contracts import DataSource, ProviderResult
|
||||
|
||||
@@ -22,3 +22,7 @@ class MarketDataProvider(Protocol):
|
||||
def daily(self, entity_type: str, identifier: str, end_date: str) -> ProviderResult: ...
|
||||
|
||||
def minute(self, entity_type: str, identifier: str, trade_date: str) -> ProviderResult: ...
|
||||
|
||||
def snapshot_inputs(
|
||||
self, trade_date: str, previous_trade_date: str
|
||||
) -> dict[str, ProviderResult | dict[str, Any]]: ...
|
||||
|
||||
@@ -94,6 +94,11 @@ class EastmoneyProvider:
|
||||
)
|
||||
return ProviderResult(tuple(rows), metadata)
|
||||
|
||||
def snapshot_inputs(
|
||||
self, trade_date: str, previous_trade_date: str
|
||||
) -> dict[str, ProviderResult | dict[str, object]]:
|
||||
raise ProviderError("The display provider cannot build market snapshots")
|
||||
|
||||
@staticmethod
|
||||
def _secid(entity_type: str, identifier: str) -> str:
|
||||
if entity_type == "index" and identifier in INDEX_CODES:
|
||||
|
||||
@@ -84,6 +84,11 @@ class IfindProvider:
|
||||
)
|
||||
return _result(payload, "yuan/share", "forward", SnapshotState.REALTIME)
|
||||
|
||||
def snapshot_inputs(
|
||||
self, trade_date: str, previous_trade_date: str
|
||||
) -> dict[str, ProviderResult | dict[str, Any]]:
|
||||
raise ProviderError("iFinD is not the post-close snapshot authority")
|
||||
|
||||
def _request(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
if not self.configured:
|
||||
raise ProviderError("实时行情服务尚未配置")
|
||||
|
||||
@@ -92,6 +92,44 @@ class TushareProvider:
|
||||
unit="yuan/share",
|
||||
)
|
||||
|
||||
def snapshot_inputs(
|
||||
self, trade_date: str, previous_trade_date: str
|
||||
) -> dict[str, ProviderResult | dict[str, Any]]:
|
||||
current = _compact(trade_date)
|
||||
previous = _compact(previous_trade_date)
|
||||
daily = self._query(
|
||||
"daily",
|
||||
{"trade_date": current},
|
||||
"ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount",
|
||||
unit="mixed",
|
||||
)
|
||||
event_fields = (
|
||||
"trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
|
||||
"float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
|
||||
"open_times,up_stat,limit_times"
|
||||
)
|
||||
datasets: dict[str, ProviderResult | dict[str, Any]] = {"daily": daily}
|
||||
datasets["price_limits"] = self._query(
|
||||
"stk_limit",
|
||||
{"trade_date": current},
|
||||
"ts_code,trade_date,up_limit,down_limit",
|
||||
unit="yuan/share",
|
||||
)
|
||||
for key, limit_type, date in (
|
||||
("limit_up", "U", current),
|
||||
("limit_down", "D", current),
|
||||
("broken", "Z", current),
|
||||
("previous_limit_up", "U", previous),
|
||||
):
|
||||
datasets[key] = self._query(
|
||||
"limit_list_d",
|
||||
{"trade_date": date, "limit_type": limit_type},
|
||||
event_fields,
|
||||
unit="mixed",
|
||||
empty_is_complete=True,
|
||||
)
|
||||
return datasets
|
||||
|
||||
def _query(
|
||||
self,
|
||||
api_name: str,
|
||||
@@ -100,6 +138,7 @@ class TushareProvider:
|
||||
*,
|
||||
unit: str,
|
||||
adjustment: str = "not_applicable",
|
||||
empty_is_complete: bool = False,
|
||||
) -> ProviderResult:
|
||||
if not self.configured:
|
||||
raise ProviderError("行情服务尚未配置")
|
||||
@@ -126,7 +165,8 @@ class TushareProvider:
|
||||
data = payload.get("data") or {}
|
||||
columns = data.get("fields") or []
|
||||
rows = tuple(dict(zip(columns, item, strict=False)) for item in data.get("items") or [])
|
||||
return ProviderResult(rows, _metadata(self.source, unit, 1 if rows else 0, adjustment))
|
||||
coverage = 1 if rows or empty_is_complete else 0
|
||||
return ProviderResult(rows, _metadata(self.source, unit, coverage, adjustment))
|
||||
|
||||
def _token(self) -> str:
|
||||
return str(self._token_provider() or "").strip()
|
||||
|
||||
@@ -126,6 +126,62 @@ class MarketRepository:
|
||||
)
|
||||
)
|
||||
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user