129 lines
4.5 KiB
Python
129 lines
4.5 KiB
Python
"""录入与导入:人工 CSV 导入 + 自动化接入接口(预留)。
|
||
|
||
- parse_events_csv / parse_etf_csv:解析导入模板(见 ddl/*_template.csv)
|
||
- AutoEventSource / AutoEtfSource:自动化抓取接口(未来挂新闻/公告/资金流),
|
||
现在留空注册表;实现后在 _AUTO_* 注册即可被定时任务调用。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import csv
|
||
import io
|
||
import logging
|
||
from datetime import date, datetime
|
||
from typing import Dict, List, Protocol
|
||
|
||
log = logging.getLogger("as-event.importers")
|
||
|
||
_VALID_DIRECTION = {"bullish", "bearish", "neutral"}
|
||
_VALID_CYCLE = {"top", "bottom", "none"}
|
||
|
||
|
||
def _parse_date(s: str) -> date:
|
||
s = (s or "").strip()
|
||
for fmt in ("%Y%m%d", "%Y-%m-%d", "%Y/%m/%d"):
|
||
try:
|
||
return datetime.strptime(s, fmt).date()
|
||
except ValueError:
|
||
continue
|
||
raise ValueError(f"日期格式错误: {s!r}")
|
||
|
||
|
||
def _clean(s) -> str:
|
||
return (s or "").strip()
|
||
|
||
|
||
def parse_events_csv(content: str) -> List[dict]:
|
||
"""列:event_date,title,category,impact_direction,severity,related_indices,cycle_tag,description,source_url"""
|
||
reader = csv.DictReader(io.StringIO(content))
|
||
out: List[dict] = []
|
||
for i, row in enumerate(reader, start=2): # 含表头,数据从第2行
|
||
title = _clean(row.get("title"))
|
||
if not title:
|
||
continue
|
||
try:
|
||
ev = dict(
|
||
event_date=_parse_date(row.get("event_date", "")),
|
||
title=title,
|
||
category=_clean(row.get("category")) or "其他",
|
||
impact_direction=(_clean(row.get("impact_direction")).lower() or "neutral"),
|
||
severity=int(_clean(row.get("severity")) or 3),
|
||
related_indices=_clean(row.get("related_indices")) or None,
|
||
cycle_tag=(_clean(row.get("cycle_tag")).lower() or "none"),
|
||
description=_clean(row.get("description")) or None,
|
||
source_url=_clean(row.get("source_url")) or None,
|
||
source="import",
|
||
)
|
||
except ValueError as e:
|
||
raise ValueError(f"第 {i} 行解析失败: {e}") from e
|
||
if ev["impact_direction"] not in _VALID_DIRECTION:
|
||
ev["impact_direction"] = "neutral"
|
||
if ev["cycle_tag"] not in _VALID_CYCLE:
|
||
ev["cycle_tag"] = "none"
|
||
ev["severity"] = min(5, max(1, ev["severity"]))
|
||
out.append(ev)
|
||
return out
|
||
|
||
|
||
def parse_etf_csv(content: str) -> List[dict]:
|
||
"""列:trade_date,etf_code,etf_name,category,related_index,net_inflow,shares_change,note"""
|
||
reader = csv.DictReader(io.StringIO(content))
|
||
out: List[dict] = []
|
||
for i, row in enumerate(reader, start=2):
|
||
d = _clean(row.get("trade_date"))
|
||
if not d:
|
||
continue
|
||
try:
|
||
rec = dict(
|
||
trade_date=_parse_date(d),
|
||
etf_code=_clean(row.get("etf_code")),
|
||
etf_name=_clean(row.get("etf_name")) or None,
|
||
category=_clean(row.get("category")) or None,
|
||
related_index=_clean(row.get("related_index")) or None,
|
||
net_inflow=float(_clean(row.get("net_inflow")) or 0) if _clean(row.get("net_inflow")) else None,
|
||
shares_change=float(_clean(row.get("shares_change"))) if _clean(row.get("shares_change")) else None,
|
||
note=_clean(row.get("note")) or None,
|
||
source="import",
|
||
)
|
||
except ValueError as e:
|
||
raise ValueError(f"第 {i} 行解析失败: {e}") from e
|
||
out.append(rec)
|
||
return out
|
||
|
||
|
||
# ============================ 自动化接入接口(预留)============================
|
||
class AutoEventSource(Protocol):
|
||
"""未来自动抓取事件源的接口。实现后注册到 _AUTO_EVENT_SOURCES。"""
|
||
|
||
name: str
|
||
|
||
def pull(self, start: date, end: date) -> List[dict]: # 返回同 parse_events_csv 的 dict 列表
|
||
...
|
||
|
||
|
||
class AutoEtfSource(Protocol):
|
||
name: str
|
||
|
||
def pull(self, start: date, end: date) -> List[dict]:
|
||
...
|
||
|
||
|
||
_AUTO_EVENT_SOURCES: Dict[str, AutoEventSource] = {}
|
||
_AUTO_ETF_SOURCES: Dict[str, AutoEtfSource] = {}
|
||
|
||
|
||
def register_auto_event_source(src: AutoEventSource) -> None:
|
||
_AUTO_EVENT_SOURCES[src.name] = src
|
||
log.info("注册自动事件源: %s", src.name)
|
||
|
||
|
||
def register_auto_etf_source(src: AutoEtfSource) -> None:
|
||
_AUTO_ETF_SOURCES[src.name] = src
|
||
log.info("注册自动ETF源: %s", src.name)
|
||
|
||
|
||
def list_auto_sources() -> Dict[str, List[str]]:
|
||
return {
|
||
"event": list(_AUTO_EVENT_SOURCES.keys()),
|
||
"etf": list(_AUTO_ETF_SOURCES.keys()),
|
||
}
|