chore: 重构前基线 — 9 个 sync task 全部 ok + akshare 移除 + mairui 资金流接入
状态: - 9 个 sync task(stock_basic / kline_daily / kline_index / kline_5min / moneyflow / industry_sector / sector_features / share_snapshot / market_regime) - 数据源:baostock + mairui + 雪球(pysnowball) + 新浪(4 个) - 项目级约束:永远不用 akshare(已落实) - kline_5min 改用 DB 快照统一全量/增量逻辑 - 零后端 Chrome 扩展 xueqiu_sync(独立项目)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""DB 子包:连接管理 + schema + 业务 CRUD。"""
|
||||
from app.core.db.connection import get_mysql, init_mysql_schema, close_mysql
|
||||
|
||||
__all__ = ["get_mysql", "init_mysql_schema", "close_mysql"]
|
||||
@@ -0,0 +1,104 @@
|
||||
"""MySQL 连接管理(thread-local,模式照搬 dashboard,但适配 market_data_sync 项目)。
|
||||
|
||||
要点:
|
||||
- pymysql 连接不是线程安全的,每个线程各持一份
|
||||
- 内网自签名证书场景:传 ssl_disabled=True 跳过 SSL 校验
|
||||
- DATE / DATETIME / TIMESTAMP 统一转字符串,避免下游类型处理
|
||||
- DictCursor:返回字典风格行,方便业务拼装
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
import pymysql
|
||||
import pymysql.constants.FIELD_TYPE
|
||||
from pymysql.converters import conversions as _pymysql_conv, convert_date, convert_datetime
|
||||
from pymysql.cursors import DictCursor
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
def _strdate(obj):
|
||||
d = convert_date(obj)
|
||||
return d.strftime("%Y-%m-%d") if d else None
|
||||
|
||||
|
||||
def _strdatetime(obj):
|
||||
d = convert_datetime(obj)
|
||||
return d.strftime("%Y-%m-%d %H:%M:%S") if d else None
|
||||
|
||||
|
||||
# 包装默认 converter,让 DATE / DATETIME / TIMESTAMP 一律出字符串
|
||||
_conv = _pymysql_conv.copy()
|
||||
_conv[pymysql.constants.FIELD_TYPE.DATE] = _strdate
|
||||
_conv[pymysql.constants.FIELD_TYPE.DATETIME] = _strdatetime
|
||||
_conv[pymysql.constants.FIELD_TYPE.TIMESTAMP] = _strdatetime
|
||||
|
||||
|
||||
def get_mysql(database: Optional[str] = None) -> pymysql.connections.Connection:
|
||||
"""取当前线程的 MySQL 连接。无则新建。
|
||||
|
||||
关键修复:高并发场景下,连接可能被服务端超时断开 (wait_timeout)。
|
||||
用 ping(reconnect=True) 让 pymysql 自动重连,避免 stale conn 导致
|
||||
2013 Lost connection 错误。
|
||||
"""
|
||||
attr = f"conn_{database or 'default'}"
|
||||
conn = getattr(_local, attr, None)
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.ping(reconnect=True)
|
||||
except Exception:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
conn = None
|
||||
if conn is None:
|
||||
kwargs = dict(
|
||||
host=settings.mysql_host,
|
||||
port=settings.mysql_port,
|
||||
user=settings.mysql_user,
|
||||
password=settings.mysql_password,
|
||||
database=database or settings.mysql_database,
|
||||
charset="utf8mb4",
|
||||
cursorclass=DictCursor,
|
||||
autocommit=False,
|
||||
conv=_conv,
|
||||
)
|
||||
# 内网自签名证书:禁用 SSL(用户场景)
|
||||
kwargs["ssl"] = None
|
||||
# 高并发时设较短 read_timeout / write_timeout
|
||||
kwargs["read_timeout"] = 30
|
||||
kwargs["write_timeout"] = 30
|
||||
conn = pymysql.connect(**kwargs)
|
||||
setattr(_local, attr, conn)
|
||||
return conn
|
||||
|
||||
|
||||
def close_mysql() -> None:
|
||||
"""关闭当前线程的所有连接。"""
|
||||
for attr in list(vars(_local).keys()):
|
||||
conn = getattr(_local, attr, None)
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
delattr(_local, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
# ── Schema DDL 入口 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def init_mysql_schema() -> None:
|
||||
"""集中创建/补齐所有表。被 db/ops.py 在第一次访问时调用。"""
|
||||
# 延后导入避免循环依赖
|
||||
from app.core.db.schema import ensure_all_tables
|
||||
|
||||
ensure_all_tables(get_mysql())
|
||||
@@ -0,0 +1,728 @@
|
||||
"""MySQL 业务 CRUD 函数集合(参考 dashboard/api/services/mysql_ops.py)。
|
||||
|
||||
本文件只包含 market_data_sync 同步任务需要的最小函数集,不涉及 dashboard 的
|
||||
accounts / positions / trade_record / scoring 等业务表。
|
||||
|
||||
设计要点:
|
||||
- 每个 ensure_xxx_table() 调用会触发 init_mysql_schema()(幂等)
|
||||
- 批量 upsert 走 executemany,效率高
|
||||
- 统一使用 DictCursor
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from app.core.db.connection import get_mysql, init_mysql_schema
|
||||
|
||||
|
||||
# ── 内部 helpers ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _now_local_ts() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _today_str() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _date_or_none(val) -> str | None:
|
||||
if val is None:
|
||||
return None
|
||||
s = str(val).strip()
|
||||
return s if s else None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _cursor():
|
||||
"""带 init_mysql_schema 的 cursor 上下文。"""
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
yield cur
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
|
||||
# ── config 表 ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def fetch_all_config() -> list[dict[str, Any]]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT `key`, `value`, category, description, updated_at FROM config")
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def fetch_config_by_key(key: str) -> dict[str, Any] | None:
|
||||
"""按 key 查单条 config(比 fetch_all_config 高效)。"""
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT `key`, `value`, category, description, updated_at FROM config WHERE `key` = %s",
|
||||
(key,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def fetch_configs_by_category(category: str) -> list[dict[str, Any]]:
|
||||
"""按 category 查 config(比 fetch_all_config + 内存过滤高效)。"""
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT `key`, `value`, category, description, updated_at FROM config WHERE category = %s",
|
||||
(category,),
|
||||
)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def upsert_config(key: str, value: str, category: str = "general", description: str = "") -> None:
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO config (`key`, `value`, category, description)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`value` = VALUES(`value`),
|
||||
category = VALUES(category),
|
||||
description = VALUES(description)
|
||||
""",
|
||||
(key, value, category, description),
|
||||
)
|
||||
|
||||
|
||||
def delete_config(key: str) -> None:
|
||||
with _cursor() as cur:
|
||||
cur.execute("DELETE FROM config WHERE `key` = %s", (key,))
|
||||
|
||||
|
||||
# ── dataset_registry ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def upsert_dataset_registry_rows(rows: list[dict[str, Any]]) -> None:
|
||||
"""批量 upsert 同步任务定义到 dataset_registry。"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
for r in rows:
|
||||
dep_json = r.get("dependency_ids")
|
||||
if isinstance(dep_json, (list, dict)):
|
||||
dep_json = json.dumps(dep_json, ensure_ascii=False)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO dataset_registry
|
||||
(dataset_id, name, description, storage_uri, storage_layer,
|
||||
management_role, source, sync_script, dependency_ids,
|
||||
enabled, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
description = VALUES(description),
|
||||
storage_uri = VALUES(storage_uri),
|
||||
storage_layer = VALUES(storage_layer),
|
||||
management_role = VALUES(management_role),
|
||||
source = VALUES(source),
|
||||
sync_script = VALUES(sync_script),
|
||||
dependency_ids = VALUES(dependency_ids),
|
||||
enabled = VALUES(enabled),
|
||||
sort_order = VALUES(sort_order)
|
||||
""",
|
||||
(
|
||||
r.get("dataset_id", ""),
|
||||
r.get("name", ""),
|
||||
r.get("description", ""),
|
||||
r.get("storage_uri", ""),
|
||||
r.get("storage_layer", ""),
|
||||
r.get("management_role", ""),
|
||||
r.get("source", ""),
|
||||
r.get("sync_script", ""),
|
||||
dep_json or "[]",
|
||||
r.get("enabled", 1),
|
||||
r.get("sort_order", 0),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def fetch_dataset_registry_rows() -> list[dict[str, Any]]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM dataset_registry ORDER BY sort_order, dataset_id")
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def fetch_dataset_registry_map() -> dict[str, dict[str, Any]]:
|
||||
rows = fetch_dataset_registry_rows()
|
||||
return {r["dataset_id"]: r for r in rows}
|
||||
|
||||
|
||||
def update_dataset_registry_state(dataset_id: str, **kwargs: Any) -> None:
|
||||
"""按需更新 dataset_registry 的状态字段。"""
|
||||
allowed = {
|
||||
"name", "description", "storage_uri", "storage_layer", "management_role",
|
||||
"source", "sync_script", "dependency_ids", "enabled", "sort_order",
|
||||
"status", "trigger_source", "started_at", "finished_at",
|
||||
"last_success_at", "last_failure_at", "message", "last_error",
|
||||
"needs_resync", "progress_current", "progress_total", "current_step",
|
||||
"updated_at",
|
||||
}
|
||||
payload = {k: v for k, v in kwargs.items() if k in allowed}
|
||||
if not payload:
|
||||
return
|
||||
if "updated_at" not in payload:
|
||||
payload["updated_at"] = _now_local_ts()
|
||||
set_clause = ", ".join(f"`{k}` = %s" for k in payload)
|
||||
values = list(payload.values()) + [dataset_id]
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
f"UPDATE dataset_registry SET {set_clause} WHERE dataset_id = %s",
|
||||
values,
|
||||
)
|
||||
|
||||
|
||||
def recover_interrupted_dataset_registry() -> int:
|
||||
"""把状态卡在 running 的同步任务标记为 failed(启动时调用)。"""
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE dataset_registry
|
||||
SET status = 'failed',
|
||||
finished_at = %s,
|
||||
last_failure_at = %s,
|
||||
last_error = CONCAT('进程在状态为 running 时被中断,已自动恢复。', IFNULL(last_error, '')),
|
||||
message = CONCAT('[recovered] ', IFNULL(message, '同步被中断')),
|
||||
needs_resync = 1
|
||||
WHERE status = 'running'
|
||||
""",
|
||||
(_now_local_ts(), _now_local_ts()),
|
||||
)
|
||||
return cur.rowcount
|
||||
|
||||
|
||||
# ── stocks ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def upsert_stock(
|
||||
code: str,
|
||||
name: str,
|
||||
exchange: str,
|
||||
list_date: str = "",
|
||||
listing_status: str = "normal",
|
||||
industry: str = "",
|
||||
) -> None:
|
||||
"""upsert 一只股票基础信息(单条)。
|
||||
|
||||
注意:MySQL 9.7.0 在 ON DUPLICATE KEY UPDATE 阶段对 DATE 字段的 VALUES()/new.col
|
||||
求值存在 bug(会强制把空串塞进去,触发 1292 严格模式错误)。所以这里:
|
||||
- list_date 字段在首次 INSERT 时写入;UPDATE 分支不更新(保持原值或 NULL)
|
||||
- industry 字段也避开这个 bug
|
||||
|
||||
批量写入请用 upsert_stocks_bulk(),性能高 10-20 倍。
|
||||
"""
|
||||
del list_date # 显式不接受 list_date 更新(旧值保留)
|
||||
del industry # 同上
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO stocks (code, name, exchange, listing_status)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
exchange = VALUES(exchange),
|
||||
listing_status = VALUES(listing_status)
|
||||
""",
|
||||
(code, name, exchange, listing_status),
|
||||
)
|
||||
|
||||
|
||||
def upsert_stocks_bulk(rows: list[dict[str, Any]], chunk_size: int = 500) -> int:
|
||||
"""批量 upsert 股票基础信息。
|
||||
|
||||
每条 row 需有: code, name, exchange, listing_status(可选 list_date / industry)
|
||||
性能:~5000 只股票从 ~50s 降到 ~3s。
|
||||
"""
|
||||
if not rows:
|
||||
return 0
|
||||
sql = (
|
||||
"INSERT INTO stocks (code, name, exchange, listing_status) "
|
||||
"VALUES (%s, %s, %s, %s) "
|
||||
"ON DUPLICATE KEY UPDATE "
|
||||
" name = VALUES(name), "
|
||||
" exchange = VALUES(exchange), "
|
||||
" listing_status = VALUES(listing_status)"
|
||||
)
|
||||
with _cursor() as cur:
|
||||
total = 0
|
||||
for i in range(0, len(rows), chunk_size):
|
||||
chunk = rows[i: i + chunk_size]
|
||||
values = [
|
||||
(r.get("code", ""), r.get("name", ""), r.get("exchange", ""), r.get("listing_status", "normal"))
|
||||
for r in chunk
|
||||
]
|
||||
cur.executemany(sql, values)
|
||||
total += len(chunk)
|
||||
return total
|
||||
|
||||
|
||||
def update_stock_share_snapshot(code: str, total_share: float, float_share: float, trade_date: str = "") -> None:
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE stocks
|
||||
SET total_share = %s, float_share = %s,
|
||||
share_updated_at = COALESCE(NULLIF(%s, ''), CURDATE())
|
||||
WHERE code = %s
|
||||
""",
|
||||
(float(total_share), float(float_share), trade_date or "", code),
|
||||
)
|
||||
|
||||
|
||||
def update_stock_kline_synced_at(code: str, synced_at: str) -> None:
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE stocks SET kline_synced_at = %s WHERE code = %s",
|
||||
(_date_or_none(synced_at), code),
|
||||
)
|
||||
|
||||
|
||||
def fetch_all_stocks() -> list[dict[str, Any]]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM stocks ORDER BY code")
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def fetch_stock_by_code(code: str) -> Optional[dict[str, Any]]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM stocks WHERE code = %s", (code,))
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def stock_count() -> int:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) AS cnt FROM stocks")
|
||||
row = cur.fetchone()
|
||||
return int(row["cnt"]) if row else 0
|
||||
|
||||
|
||||
def iter_stock_codes(active_only: bool = True) -> Iterable[str]:
|
||||
"""yield 6 位股票代码(去除 SH/SZ/BJ 前缀)。"""
|
||||
for s in fetch_all_stocks():
|
||||
if active_only and s.get("listing_status") == "delisted":
|
||||
continue
|
||||
code = str(s.get("code", "")).strip()
|
||||
if not code:
|
||||
continue
|
||||
for prefix in ("SH", "SZ", "BJ"):
|
||||
if code.startswith(prefix):
|
||||
code = code[len(prefix):]
|
||||
break
|
||||
if code.isdigit() and len(code) == 6:
|
||||
yield code
|
||||
|
||||
|
||||
# ── indices ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def upsert_index(
|
||||
index_code: str,
|
||||
index_name: str,
|
||||
market: str = "",
|
||||
category: str = "",
|
||||
source: str = "",
|
||||
enabled: bool = True,
|
||||
) -> None:
|
||||
with _cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO indices (index_code, index_name, market, category, source, enabled)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
index_name = VALUES(index_name),
|
||||
market = VALUES(market),
|
||||
category = VALUES(category),
|
||||
source = VALUES(source),
|
||||
enabled = VALUES(enabled)
|
||||
""",
|
||||
(index_code, index_name, market, category, source, int(enabled)),
|
||||
)
|
||||
|
||||
|
||||
# ── kline 通用批量 upsert ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def _bulk_upsert(
|
||||
cur,
|
||||
table: str,
|
||||
columns: list[str],
|
||||
rows: list[dict[str, Any]],
|
||||
conflict_keys: list[str],
|
||||
chunk_size: int = 1000,
|
||||
) -> int:
|
||||
"""通用批量 INSERT ... ON DUPLICATE KEY UPDATE 工具。"""
|
||||
if not rows:
|
||||
return 0
|
||||
placeholders = ", ".join(["%s"] * len(columns))
|
||||
col_list = ", ".join(f"`{c}`" for c in columns)
|
||||
update_clause = ", ".join(
|
||||
f"`{c}` = VALUES(`{c}`)"
|
||||
for c in columns
|
||||
if c not in conflict_keys
|
||||
)
|
||||
sql = (
|
||||
f"INSERT INTO {table} ({col_list}) VALUES ({placeholders}) "
|
||||
f"ON DUPLICATE KEY UPDATE {update_clause}"
|
||||
)
|
||||
total = 0
|
||||
for i in range(0, len(rows), chunk_size):
|
||||
chunk = rows[i: i + chunk_size]
|
||||
values = [
|
||||
tuple(_date_or_none(r.get(c)) if c.endswith("date") else r.get(c) for c in columns)
|
||||
for r in chunk
|
||||
]
|
||||
cur.executemany(sql, values)
|
||||
total += len(chunk)
|
||||
return total
|
||||
|
||||
|
||||
def upsert_kline_stock(rows: list[dict[str, Any]]) -> int:
|
||||
"""rows: stock_code, trade_date, open, high, low, close, volume"""
|
||||
with _cursor() as cur:
|
||||
return _bulk_upsert(
|
||||
cur, "kline_stock",
|
||||
["stock_code", "trade_date", "open", "high", "low", "close", "volume"],
|
||||
rows, conflict_keys=["stock_code", "trade_date"],
|
||||
)
|
||||
|
||||
|
||||
def upsert_kline_index(rows: list[dict[str, Any]]) -> int:
|
||||
"""rows: index_code, trade_date, open, high, low, close, volume"""
|
||||
with _cursor() as cur:
|
||||
return _bulk_upsert(
|
||||
cur, "kline_index",
|
||||
["index_code", "trade_date", "open", "high", "low", "close", "volume"],
|
||||
rows, conflict_keys=["index_code", "trade_date"],
|
||||
)
|
||||
|
||||
|
||||
def upsert_kline_5min(rows: list[dict[str, Any]]) -> int:
|
||||
"""rows: stock_code, bar_time, open, high, low, close, volume, amount, turnover_rate"""
|
||||
with _cursor() as cur:
|
||||
return _bulk_upsert(
|
||||
cur, "kline_5min",
|
||||
["stock_code", "bar_time", "open", "high", "low", "close", "volume", "amount", "turnover_rate"],
|
||||
rows, conflict_keys=["stock_code", "bar_time"],
|
||||
)
|
||||
|
||||
|
||||
def get_kline_5min_snapshots() -> dict[str, Optional[datetime]]:
|
||||
"""一次 SQL 拉 kline_5min 全表每只股票的最新 bar_time。
|
||||
|
||||
返回:{stock_code (6位): max(bar_time) 或 None}
|
||||
用于 kline_5min 任务的"全量/增量"统一规划:
|
||||
- DB 里有 → 增量(从 max - 2 天 到今天)
|
||||
- DB 里没 → 全量(从 mairui 历史深度起点 到今天)
|
||||
|
||||
DB 里 stock_code 形如 "000001.SZ"(mairui 写入格式),函数剥掉
|
||||
交易所后缀统一为 6 位。bar_time 是 DATETIME 字段,pymysql DictCursor
|
||||
在某些连接参数下会返回 str,这里手动解析为 datetime(解析失败置 None)。
|
||||
"""
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
out: dict[str, Optional[datetime]] = {}
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT stock_code, MAX(bar_time) AS latest FROM kline_5min GROUP BY stock_code")
|
||||
for r in cur.fetchall():
|
||||
raw_code = str(r["stock_code"] or "").strip()
|
||||
code6 = raw_code.split(".")[0] # "000001.SZ" → "000001"
|
||||
if len(code6) != 6 or not code6.isdigit():
|
||||
continue
|
||||
latest = r["latest"]
|
||||
if isinstance(latest, datetime):
|
||||
pass
|
||||
elif isinstance(latest, str) and latest:
|
||||
try:
|
||||
latest = datetime.strptime(latest[:19], "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
latest = None
|
||||
else:
|
||||
latest = None
|
||||
out[code6] = latest
|
||||
return out
|
||||
|
||||
|
||||
def upsert_moneyflow(rows: list[dict[str, Any]]) -> int:
|
||||
"""rows: stock_code, trade_date, main_net_inflow, large_net_inflow, medium_net_inflow, small_net_inflow"""
|
||||
with _cursor() as cur:
|
||||
return _bulk_upsert(
|
||||
cur, "moneyflow",
|
||||
["stock_code", "trade_date", "main_net_inflow", "large_net_inflow", "medium_net_inflow", "small_net_inflow"],
|
||||
rows, conflict_keys=["stock_code", "trade_date"],
|
||||
)
|
||||
|
||||
|
||||
def upsert_share(rows: list[dict[str, Any]]) -> int:
|
||||
"""rows: stock_code, trade_date, total_share, float_share"""
|
||||
with _cursor() as cur:
|
||||
return _bulk_upsert(
|
||||
cur, "share",
|
||||
["stock_code", "trade_date", "total_share", "float_share"],
|
||||
rows, conflict_keys=["stock_code", "trade_date"],
|
||||
)
|
||||
|
||||
|
||||
# ── 行业 / 概念板块 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def replace_all_industries(rows: list[dict[str, Any]]) -> None:
|
||||
"""全量替换 industry 表。rows: code, industry_name, industry_classification, update_date"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
cur.execute("DELETE FROM industry")
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO industry (code, industry_name, industry_classification, update_date)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
industry_name = VALUES(industry_name),
|
||||
industry_classification = VALUES(industry_classification),
|
||||
update_date = VALUES(update_date)
|
||||
""",
|
||||
(
|
||||
str(r.get("code", "")).zfill(6),
|
||||
r.get("industry_name") or None,
|
||||
r.get("industry_classification") or None,
|
||||
_date_or_none(r.get("update_date")),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def fetch_all_industries() -> list[dict[str, Any]]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT code, industry_name, industry_classification, update_date FROM industry")
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def replace_all_sectors(rows: list[dict[str, Any]]) -> None:
|
||||
"""rows: sector_key, sector_name, taxonomy, level, source, enabled"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
cur.execute("DELETE FROM sectors")
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO sectors (sector_key, sector_name, taxonomy, level, source, enabled)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
sector_name = VALUES(sector_name),
|
||||
taxonomy = VALUES(taxonomy),
|
||||
level = VALUES(level),
|
||||
source = VALUES(source),
|
||||
enabled = VALUES(enabled)
|
||||
""",
|
||||
(
|
||||
r.get("sector_key", ""),
|
||||
r.get("sector_name", ""),
|
||||
r.get("taxonomy", ""),
|
||||
r.get("level", ""),
|
||||
r.get("source", ""),
|
||||
int(r.get("enabled", 1)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def replace_all_stock_sector_map(rows: list[dict[str, Any]]) -> None:
|
||||
"""rows: stock_code, sector_key"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
cur.execute("DELETE FROM stock_sector_map")
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO stock_sector_map (stock_code, sector_key)
|
||||
VALUES (%s, %s)
|
||||
ON DUPLICATE KEY UPDATE sector_key = VALUES(sector_key)
|
||||
""",
|
||||
(str(r.get("stock_code", "")).zfill(6), r.get("sector_key", "")),
|
||||
)
|
||||
|
||||
|
||||
# ── 行业聚合(衍生)─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def replace_all_sector_indices(rows: list[dict[str, Any]]) -> None:
|
||||
"""rows: trade_date, sector_name, close, sector_amplitude"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO sector_indices (trade_date, sector_name, `close`, sector_amplitude)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`close` = VALUES(`close`),
|
||||
sector_amplitude = VALUES(sector_amplitude)
|
||||
""",
|
||||
(
|
||||
_date_or_none(r.get("trade_date")),
|
||||
r.get("sector_name", ""),
|
||||
float(r.get("close") or 0),
|
||||
float(r.get("sector_amplitude") or 0),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def replace_all_sector_features(rows: list[dict[str, Any]]) -> None:
|
||||
"""rows: trade_date, sector_name, sector_ret, sector_amplitude, close, ema10, ema20, ema200, score"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO sector_features_daily
|
||||
(trade_date, sector_name, sector_ret, sector_amplitude, `close`, ema10, ema20, ema200, score)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
sector_ret = VALUES(sector_ret),
|
||||
sector_amplitude = VALUES(sector_amplitude),
|
||||
`close` = VALUES(`close`),
|
||||
ema10 = VALUES(ema10),
|
||||
ema20 = VALUES(ema20),
|
||||
ema200 = VALUES(ema200),
|
||||
score = VALUES(score)
|
||||
""",
|
||||
(
|
||||
_date_or_none(r.get("trade_date")),
|
||||
r.get("sector_name", ""),
|
||||
float(r.get("sector_ret") or 0),
|
||||
float(r.get("sector_amplitude") or 0),
|
||||
float(r.get("close") or 0),
|
||||
float(r.get("ema10") or 0),
|
||||
float(r.get("ema20") or 0),
|
||||
float(r.get("ema200") or 0),
|
||||
int(r.get("score") or 0),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── 市场情绪(衍生)─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def upsert_market_regime_rows(rows: list[dict[str, Any]]) -> None:
|
||||
"""rows: trade_date, advancers, decliners, advance_ratio, turnover, turnover_avg_5d, turnover_ratio_5d, source, is_extreme_panic"""
|
||||
if not rows:
|
||||
return
|
||||
with _cursor() as cur:
|
||||
for r in rows:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO market_regime_daily
|
||||
(trade_date, advancers, decliners, advance_ratio, turnover,
|
||||
turnover_avg_5d, turnover_ratio_5d, source, is_extreme_panic)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
advancers = VALUES(advancers),
|
||||
decliners = VALUES(decliners),
|
||||
advance_ratio = VALUES(advance_ratio),
|
||||
turnover = VALUES(turnover),
|
||||
turnover_avg_5d = VALUES(turnover_avg_5d),
|
||||
turnover_ratio_5d = VALUES(turnover_ratio_5d),
|
||||
source = VALUES(source),
|
||||
is_extreme_panic = VALUES(is_extreme_panic)
|
||||
""",
|
||||
(
|
||||
_date_or_none(r.get("trade_date")),
|
||||
float(r.get("advancers") or 0),
|
||||
float(r.get("decliners") or 0),
|
||||
float(r.get("advance_ratio") or 0),
|
||||
float(r.get("turnover") or 0),
|
||||
float(r.get("turnover_avg_5d") or 0),
|
||||
float(r.get("turnover_ratio_5d") or 0),
|
||||
r.get("source", ""),
|
||||
int(r.get("is_extreme_panic") or 0),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── kline 查询(同步时用于判断增量起点)─────────────────────────────────
|
||||
|
||||
|
||||
def get_stock_kline_max_date(code: str) -> Optional[str]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT MAX(trade_date) AS d FROM kline_stock WHERE stock_code = %s",
|
||||
(code,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row.get("d"):
|
||||
return str(row["d"])
|
||||
return None
|
||||
|
||||
|
||||
def get_index_kline_max_date(index_code: str) -> Optional[str]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT MAX(trade_date) AS d FROM kline_index WHERE index_code = %s",
|
||||
(index_code,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row.get("d"):
|
||||
return str(row["d"])
|
||||
return None
|
||||
|
||||
|
||||
def get_5min_max_bar_time(code: str) -> Optional[str]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT MAX(bar_time) AS d FROM kline_5min WHERE stock_code = %s",
|
||||
(code,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row.get("d"):
|
||||
return str(row["d"])
|
||||
return None
|
||||
|
||||
|
||||
def get_moneyflow_max_date(code: str) -> Optional[str]:
|
||||
init_mysql_schema()
|
||||
conn = get_mysql()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT MAX(trade_date) AS d FROM moneyflow WHERE stock_code = %s",
|
||||
(code,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row.get("d"):
|
||||
return str(row["d"])
|
||||
return None
|
||||
@@ -0,0 +1,264 @@
|
||||
"""Schema DDL — 集中所有 CREATE TABLE IF NOT EXISTS。
|
||||
|
||||
参考 dashboard/api/services/mysql_conn.py::init_mysql_schema(),但补齐 dashboard 缺的几张表
|
||||
(kline_stock / kline_5min / kline_index / moneyflow / share / sector_features_daily / sector_indices
|
||||
/ stock_scores / registry),这些是 grid_seeker_model_base 生产库已存在但 mysql_conn.py 没建的表。
|
||||
|
||||
所有 DDL 写在同一个文件,方便审阅、diff 和补字段。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pymysql.connections
|
||||
|
||||
|
||||
# DDL 列表(顺序无关,全部 IF NOT EXISTS)
|
||||
_DDL: list[str] = [
|
||||
# ─── config ─────────────────────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
`key` VARCHAR(128) PRIMARY KEY,
|
||||
`value` TEXT NOT NULL,
|
||||
category VARCHAR(32) NOT NULL DEFAULT 'general',
|
||||
description VARCHAR(255) DEFAULT '',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── dataset_registry (同步状态机) ────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dataset_registry (
|
||||
dataset_id VARCHAR(64) PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
storage_uri VARCHAR(255) DEFAULT '',
|
||||
storage_layer VARCHAR(32) DEFAULT '',
|
||||
management_role VARCHAR(32) DEFAULT '',
|
||||
source VARCHAR(255) DEFAULT '',
|
||||
sync_script VARCHAR(255) DEFAULT '',
|
||||
dependency_ids VARCHAR(4096) DEFAULT '[]',
|
||||
enabled INT DEFAULT 1,
|
||||
sort_order INT DEFAULT 0,
|
||||
status VARCHAR(16) DEFAULT 'idle',
|
||||
trigger_source VARCHAR(32) DEFAULT '',
|
||||
started_at DATETIME DEFAULT NULL,
|
||||
finished_at DATETIME DEFAULT NULL,
|
||||
last_success_at DATETIME DEFAULT NULL,
|
||||
last_failure_at DATETIME DEFAULT NULL,
|
||||
message TEXT,
|
||||
last_error TEXT,
|
||||
needs_resync INT DEFAULT 0,
|
||||
progress_current INT DEFAULT 0,
|
||||
progress_total INT DEFAULT 0,
|
||||
current_step VARCHAR(255) DEFAULT '',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_dataset_registry_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── stocks (基础信息 + 股本字段) ─────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS stocks (
|
||||
code VARCHAR(10) PRIMARY KEY,
|
||||
name VARCHAR(32) NOT NULL DEFAULT '',
|
||||
exchange VARCHAR(8) NOT NULL DEFAULT '',
|
||||
list_date DATE DEFAULT NULL,
|
||||
listing_status VARCHAR(16) NOT NULL DEFAULT 'normal',
|
||||
industry VARCHAR(64) DEFAULT '',
|
||||
total_share DOUBLE DEFAULT 0,
|
||||
float_share DOUBLE DEFAULT 0,
|
||||
share_updated_at DATE DEFAULT NULL,
|
||||
kline_synced_at DATE DEFAULT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── indices (指数字典) ───────────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS indices (
|
||||
index_code VARCHAR(10) PRIMARY KEY,
|
||||
index_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
market VARCHAR(16) DEFAULT '',
|
||||
category VARCHAR(16) DEFAULT '',
|
||||
source VARCHAR(32) DEFAULT '',
|
||||
enabled INT DEFAULT 1,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── kline_stock (日 K 线,按股票) ────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS kline_stock (
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
trade_date DATE NOT NULL,
|
||||
`open` DOUBLE NOT NULL,
|
||||
`high` DOUBLE NOT NULL,
|
||||
`low` DOUBLE NOT NULL,
|
||||
`close` DOUBLE NOT NULL,
|
||||
volume DOUBLE NOT NULL,
|
||||
PRIMARY KEY (stock_code, trade_date),
|
||||
INDEX idx_kline_stock_date (trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── kline_index (指数日 K 线) ────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS kline_index (
|
||||
index_code VARCHAR(10) NOT NULL,
|
||||
trade_date DATE NOT NULL,
|
||||
`open` DOUBLE NOT NULL,
|
||||
`high` DOUBLE NOT NULL,
|
||||
`low` DOUBLE NOT NULL,
|
||||
`close` DOUBLE NOT NULL,
|
||||
volume DOUBLE NOT NULL,
|
||||
PRIMARY KEY (index_code, trade_date),
|
||||
INDEX idx_kline_index_date (trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── kline_5min (5 分钟 K 线) ────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS kline_5min (
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
bar_time DATETIME NOT NULL,
|
||||
`open` DOUBLE DEFAULT NULL,
|
||||
`high` DOUBLE DEFAULT NULL,
|
||||
`low` DOUBLE DEFAULT NULL,
|
||||
`close` DOUBLE DEFAULT NULL,
|
||||
volume DOUBLE DEFAULT NULL,
|
||||
amount DOUBLE DEFAULT NULL,
|
||||
turnover_rate DOUBLE DEFAULT NULL,
|
||||
PRIMARY KEY (stock_code, bar_time),
|
||||
INDEX idx_kline_5min_bar_time (bar_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── moneyflow (资金流,主力/大/中/小单净额) ──────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS moneyflow (
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
trade_date DATE NOT NULL,
|
||||
main_net_inflow DOUBLE DEFAULT 0,
|
||||
large_net_inflow DOUBLE DEFAULT 0,
|
||||
medium_net_inflow DOUBLE DEFAULT 0,
|
||||
small_net_inflow DOUBLE DEFAULT 0,
|
||||
PRIMARY KEY (stock_code, trade_date),
|
||||
INDEX idx_moneyflow_date (trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── share (股本快照) ────────────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS share (
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
trade_date DATE NOT NULL,
|
||||
total_share DOUBLE NOT NULL,
|
||||
float_share DOUBLE NOT NULL,
|
||||
PRIMARY KEY (stock_code, trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── sectors (行业字典) ──────────────────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sectors (
|
||||
sector_key VARCHAR(64) PRIMARY KEY,
|
||||
sector_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
taxonomy VARCHAR(64) DEFAULT '',
|
||||
level VARCHAR(16) DEFAULT '',
|
||||
source VARCHAR(32) DEFAULT '',
|
||||
enabled INT DEFAULT 1,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_sectors_sector_name (sector_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── stock_sector_map (股票→行业映射) ─────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS stock_sector_map (
|
||||
stock_code VARCHAR(6) PRIMARY KEY,
|
||||
sector_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_stock_sector_map_sector_key (sector_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── industry (股票-行业映射,datasource 实际写入用这张) ──────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS industry (
|
||||
code VARCHAR(6) PRIMARY KEY,
|
||||
industry_name VARCHAR(64) DEFAULT '',
|
||||
industry_classification VARCHAR(32) DEFAULT '',
|
||||
update_date DATE DEFAULT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_industry_industry_name (industry_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── sector_indices (行业聚合指数时序) ─────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sector_indices (
|
||||
trade_date DATE NOT NULL,
|
||||
sector_name VARCHAR(64) NOT NULL,
|
||||
`close` DOUBLE DEFAULT 0,
|
||||
sector_amplitude DOUBLE DEFAULT 0,
|
||||
PRIMARY KEY (trade_date, sector_name),
|
||||
INDEX idx_sector_indices_sector_name (sector_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── sector_features_daily (行业特征衍生) ─────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sector_features_daily (
|
||||
trade_date DATE NOT NULL,
|
||||
sector_name VARCHAR(64) NOT NULL,
|
||||
sector_ret DOUBLE DEFAULT 0,
|
||||
sector_amplitude DOUBLE DEFAULT 0,
|
||||
`close` DOUBLE DEFAULT 0,
|
||||
ema10 DOUBLE DEFAULT 0,
|
||||
ema20 DOUBLE DEFAULT 0,
|
||||
ema200 DOUBLE DEFAULT 0,
|
||||
score INT DEFAULT 0,
|
||||
PRIMARY KEY (trade_date, sector_name),
|
||||
INDEX idx_sector_features_sector_name (sector_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── market_regime_daily (市场情绪) ───────────────────────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS market_regime_daily (
|
||||
trade_date DATE PRIMARY KEY,
|
||||
advancers DOUBLE DEFAULT 0,
|
||||
decliners DOUBLE DEFAULT 0,
|
||||
advance_ratio DOUBLE DEFAULT 0,
|
||||
turnover DOUBLE DEFAULT 0,
|
||||
turnover_avg_5d DOUBLE DEFAULT 0,
|
||||
turnover_ratio_5d DOUBLE DEFAULT 0,
|
||||
source VARCHAR(32) DEFAULT '',
|
||||
is_extreme_panic INT DEFAULT 0,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_market_regime_daily_date (trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── registry (模型注册,dashboard 业务无关但库里有) ─────────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS registry (
|
||||
model_name VARCHAR(64) NOT NULL,
|
||||
role VARCHAR(32) NOT NULL,
|
||||
version VARCHAR(16) DEFAULT NULL,
|
||||
artifact_path VARCHAR(256) NOT NULL,
|
||||
architecture VARCHAR(32) DEFAULT NULL,
|
||||
enabled TINYINT(1) DEFAULT 1,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (model_name, role)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
# ─── stock_scores (评分快照,dashboard 业务无关但库里有) ─────────────
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS stock_scores (
|
||||
`date` DATE NOT NULL,
|
||||
stock_code VARCHAR(10) NOT NULL,
|
||||
predicted_rounds DOUBLE DEFAULT 0,
|
||||
stacking_probability DOUBLE DEFAULT 0,
|
||||
meta_ranker_score DOUBLE DEFAULT NULL,
|
||||
latest_close DOUBLE DEFAULT 0,
|
||||
`rank` INT DEFAULT 0,
|
||||
PRIMARY KEY (`date`, stock_code),
|
||||
INDEX idx_stock_scores_date (`date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def ensure_all_tables(conn) -> None:
|
||||
"""遍历 _DDL 列表,逐条执行 CREATE TABLE IF NOT EXISTS。"""
|
||||
with conn.cursor() as cur:
|
||||
for ddl in _DDL:
|
||||
cur.execute(ddl)
|
||||
conn.commit()
|
||||
Reference in New Issue
Block a user