202 lines
8.5 KiB
Python
202 lines
8.5 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
下游/上游只读表访问 (trading_* 与 strategy_daily_results, 153 代理, 严格单表)
|
||
|
|
==============================================================================
|
||
|
|
这三张表**归下游系统维护**, PMS 只读 (设计 §9)。
|
||
|
|
|
||
|
|
列名不确定性 (重要):
|
||
|
|
QMT_INTERFACE_REQUIREMENTS A1/A2/D1 仍在协商 —— 目前只确认 trading_position.stock_code
|
||
|
|
与 trading_order 的 order_* 系列列。因此本模块**不硬编码持仓数量列名**, 而是先取一行
|
||
|
|
探测列名 (候选列表按常见命名排优先级), 并把探测结果回传给上层, 在页面与日报显式展示
|
||
|
|
「本次对账用的是哪一列」。协商答复到位后, 把 PMS_DS_QTY_COL 参数写死即可停用探测。
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from app.db.session import fetch_all, fetch_one
|
||
|
|
|
||
|
|
# 数量/可用量列名候选 (探测顺序即优先级)
|
||
|
|
QTY_CANDIDATES = ["current_qty", "total_qty", "position_qty", "hold_qty", "stock_qty",
|
||
|
|
"volume", "quantity", "qty", "position_volume", "hold_volume"]
|
||
|
|
AVAIL_CANDIDATES = ["available_qty", "avail_qty", "can_use_volume", "available_volume",
|
||
|
|
"sellable_qty", "enable_amount", "can_sell_qty"]
|
||
|
|
COST_CANDIDATES = ["cost_price", "avg_cost", "open_price", "position_cost", "cost"]
|
||
|
|
FROZEN_CANDIDATES = ["frozen_qty", "frozen_volume", "freeze_qty"]
|
||
|
|
|
||
|
|
FILLED_STATUSES = ("completed", "filled")
|
||
|
|
|
||
|
|
|
||
|
|
def to_dot(code: str) -> str:
|
||
|
|
"""SH600000 / 600000 → 600000.SH (PMS 内部统一点式)。"""
|
||
|
|
c = (code or "").strip().upper()
|
||
|
|
if not c:
|
||
|
|
return ""
|
||
|
|
if "." in c:
|
||
|
|
return c
|
||
|
|
if c[:2] in ("SH", "SZ", "BJ") and c[2:].isdigit():
|
||
|
|
return f"{c[2:]}.{c[:2]}"
|
||
|
|
if c.isdigit() and len(c) == 6:
|
||
|
|
return f"{c}.SH" if c[0] == "6" else (f"{c}.SZ" if c[0] in "03" else f"{c}.BJ")
|
||
|
|
return c
|
||
|
|
|
||
|
|
|
||
|
|
def to_prefix(code: str) -> str:
|
||
|
|
"""600000.SH → SH600000 (strategy_daily_results 口径)。"""
|
||
|
|
c = (code or "").strip().upper()
|
||
|
|
if "." in c:
|
||
|
|
num, mkt = c.split(".", 1)
|
||
|
|
return f"{mkt}{num}"
|
||
|
|
return c
|
||
|
|
|
||
|
|
|
||
|
|
def _pick(keys, candidates):
|
||
|
|
low = {str(k).lower(): k for k in keys}
|
||
|
|
for c in candidates:
|
||
|
|
if c in low:
|
||
|
|
return low[c]
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def describe(table: str) -> list:
|
||
|
|
"""SHOW COLUMNS —— 供页面导出、回填 QMT_INTERFACE_REQUIREMENTS D1。"""
|
||
|
|
if table not in ("trading_position", "trading_order", "trading_buy_plan",
|
||
|
|
"strategy_daily_results"):
|
||
|
|
raise ValueError(f"不允许探测的表: {table}")
|
||
|
|
return fetch_all(f"SHOW COLUMNS FROM {table}")
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ trading_position
|
||
|
|
def fetch_positions() -> dict:
|
||
|
|
"""下游持仓快照。返回 {"rows":[{ts_code, qty, avail_qty, cost, frozen}], "columns":{...}}"""
|
||
|
|
rows = fetch_all("SELECT * FROM trading_position LIMIT 1000")
|
||
|
|
if not rows:
|
||
|
|
return {"rows": [], "columns": {"qty": None, "avail": None, "cost": None},
|
||
|
|
"raw_count": 0}
|
||
|
|
keys = rows[0].keys()
|
||
|
|
qty_col = _pick(keys, QTY_CANDIDATES)
|
||
|
|
avail_col = _pick(keys, AVAIL_CANDIDATES)
|
||
|
|
cost_col = _pick(keys, COST_CANDIDATES)
|
||
|
|
frozen_col = _pick(keys, FROZEN_CANDIDATES)
|
||
|
|
code_col = _pick(keys, ["stock_code", "ts_code", "code", "security_code"]) or "stock_code"
|
||
|
|
|
||
|
|
out = []
|
||
|
|
for r in rows:
|
||
|
|
code = to_dot(str(r.get(code_col) or ""))
|
||
|
|
if not code:
|
||
|
|
continue
|
||
|
|
out.append({
|
||
|
|
"ts_code": code,
|
||
|
|
"qty": _int(r.get(qty_col)) if qty_col else None,
|
||
|
|
"avail_qty": _int(r.get(avail_col)) if avail_col else None,
|
||
|
|
"cost": _float(r.get(cost_col)) if cost_col else None,
|
||
|
|
"frozen": _int(r.get(frozen_col)) if frozen_col else None,
|
||
|
|
})
|
||
|
|
return {"rows": out, "raw_count": len(rows),
|
||
|
|
"columns": {"code": code_col, "qty": qty_col, "avail": avail_col,
|
||
|
|
"cost": cost_col, "frozen": frozen_col}}
|
||
|
|
|
||
|
|
|
||
|
|
def _int(v):
|
||
|
|
try:
|
||
|
|
return int(float(v or 0))
|
||
|
|
except (TypeError, ValueError):
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def _float(v):
|
||
|
|
try:
|
||
|
|
return float(v or 0)
|
||
|
|
except (TypeError, ValueError):
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ trading_order
|
||
|
|
def fetch_filled_orders(*, since_id=None, since_time=None, limit: int = 500) -> list:
|
||
|
|
"""已成交单增量拉取 (回放用)。价格口径: filled_price > order_price (下游 filled_amount 不可靠)。
|
||
|
|
|
||
|
|
order_id 可能是字符串委托号 —— 游标同时支持数值与字符串比较, 取不到则按时间兜底。
|
||
|
|
"""
|
||
|
|
# 状态枚举为常量 (QMT A2 待正式确认), 直接内联避免 IN 绑定展开
|
||
|
|
where = ["order_status IN ('" + "', '".join(FILLED_STATUSES) + "')"]
|
||
|
|
p = {"n": int(limit)}
|
||
|
|
if since_id not in (None, "", 0):
|
||
|
|
where.append("order_id > :sid")
|
||
|
|
p["sid"] = since_id
|
||
|
|
elif since_time:
|
||
|
|
where.append("(filled_time >= :st OR order_time >= :st)")
|
||
|
|
p["st"] = since_time
|
||
|
|
sql = ("SELECT * FROM trading_order WHERE " + " AND ".join(where) +
|
||
|
|
" ORDER BY order_id ASC LIMIT :n")
|
||
|
|
rows = fetch_all(sql, p)
|
||
|
|
return [_norm_order(r) for r in rows]
|
||
|
|
|
||
|
|
|
||
|
|
def _norm_order(r: dict) -> dict:
|
||
|
|
side = str(r.get("order_side") or "").strip().lower()
|
||
|
|
if side in ("1", "b", "买入", "买"):
|
||
|
|
side = "buy"
|
||
|
|
elif side in ("2", "s", "卖出", "卖"):
|
||
|
|
side = "sell"
|
||
|
|
qty = r.get("filled_quantity") or r.get("filled_qty") or r.get("order_quantity")
|
||
|
|
price = r.get("filled_price") or r.get("filled_avg_price") or r.get("order_price")
|
||
|
|
return {"order_id": r.get("order_id"), "ts_code": to_dot(str(r.get("stock_code") or "")),
|
||
|
|
"side": side, "qty": _int(qty), "price": _float(price),
|
||
|
|
"done_time": str(r.get("filled_time") or r.get("order_time") or ""),
|
||
|
|
"status": r.get("order_status"), "raw": r}
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ trading_buy_plan
|
||
|
|
def fetch_buy_plans(*, is_active=None, limit: int = 200) -> list:
|
||
|
|
"""上游买入计划 (PMS 作为承接方, 替代原 ENTRY_GATE 角色 —— 设计 §6)。
|
||
|
|
|
||
|
|
is_active: 7=待仲裁 (上游产出待承接), 6=待挂单 (下游取走), 5=盘中拒。
|
||
|
|
"""
|
||
|
|
sql = ("SELECT id, strategy_id, stock_code, stock_name, target_price, buy_amount, "
|
||
|
|
"factor_code, trading_time, create_time, update_time, tp_ratio, sl_ratio, "
|
||
|
|
"prob_thresh, hold_days, is_active, approved_by FROM trading_buy_plan")
|
||
|
|
p = {"n": int(limit)}
|
||
|
|
if is_active is not None:
|
||
|
|
sql += " WHERE is_active = :ia"
|
||
|
|
p["ia"] = int(is_active)
|
||
|
|
sql += " ORDER BY update_time DESC LIMIT :n"
|
||
|
|
rows = fetch_all(sql, p)
|
||
|
|
out = []
|
||
|
|
for r in rows:
|
||
|
|
px = _float(r.get("target_price"))
|
||
|
|
out.append({"plan_id": r.get("id"), "ts_code": to_dot(str(r.get("stock_code") or "")),
|
||
|
|
"name": r.get("stock_name"), "price": px,
|
||
|
|
"amount": _float(r.get("buy_amount")),
|
||
|
|
"score": _float(r.get("prob_thresh")), "factor": r.get("factor_code"),
|
||
|
|
"tp_ratio": _float(r.get("tp_ratio")), "sl_ratio": _float(r.get("sl_ratio")),
|
||
|
|
"is_active": r.get("is_active"), "update_time": r.get("update_time")})
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ strategy_daily_results
|
||
|
|
def fetch_refs(ts_code: str):
|
||
|
|
"""决策系统昨夜结论: 支撑/压力参考位 (主口径; 停更超期由 services 兜底自算)。"""
|
||
|
|
r = fetch_one(
|
||
|
|
"SELECT stock_code, signal_type, support_level, pressure_level, trade_date "
|
||
|
|
"FROM strategy_daily_results WHERE stock_code = :code "
|
||
|
|
"ORDER BY trade_date DESC LIMIT 1", {"code": to_prefix(ts_code)})
|
||
|
|
if not r:
|
||
|
|
return None
|
||
|
|
return {"ts_code": ts_code, "signal_type": r.get("signal_type"),
|
||
|
|
"support": _float(r.get("support_level")) or None,
|
||
|
|
"pressure": _float(r.get("pressure_level")) or None,
|
||
|
|
"trade_date": r.get("trade_date")}
|
||
|
|
|
||
|
|
|
||
|
|
def fetch_sector_from_category(ts_code: str):
|
||
|
|
"""行业适配器 gp_stock_category (映射表就绪前的临时数据源 —— 设计 §5)。"""
|
||
|
|
for col in ("stock_code", "ts_code"):
|
||
|
|
try:
|
||
|
|
r = fetch_one(f"SELECT * FROM gp_stock_category WHERE {col} = :code LIMIT 1",
|
||
|
|
{"code": to_prefix(ts_code) if col == "stock_code" else ts_code})
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
if r:
|
||
|
|
for k in ("industry", "category", "sector", "industry_name", "sw_industry"):
|
||
|
|
if r.get(k):
|
||
|
|
return str(r[k])
|
||
|
|
return None
|