tradingSystem/app/services/market.py

195 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
行情与参考位取数 (best-effort, 取不到一律返回 None 由上层降级)
==============================================================
现价: Redis db13 (SIGNAL_REDIS_DB_QUOTES), key `tushare:rt_min:1MIN:{600000.SH}`,
值为当日分钟 K 线数组 —— 与 bionic_trader 既有读法完全一致, 不另立口径。
参考位: 主口径取决策系统 strategy_daily_results 的支撑/压力; 日龄超 PMS_REF_STALE_TDAYS
转兜底自算 (设计 §13): 支撑 = max(MA20, 近20日低点×1.01), 压力 = 近60日高点,
止损 = 底仓成本 N×ATR 与支撑取高。因子分表 gp_stock_factor_pro_YYYYMM 经 153 代理
逐表单查 (跨分表 UNION 会被代理拒绝, 沿用 bionic 的踩坑结论)。
"""
from __future__ import annotations
import json
import logging
import threading
from datetime import datetime, timedelta
from config.settings import settings
from app.db.session import fetch_all
from app.repo import downstream_repo
from app.services import param_store
logger = logging.getLogger("pms.market")
_redis = None
_lock = threading.Lock()
QUOTE_KEY = "tushare:rt_min:1MIN:{code}"
def _r():
global _redis
if _redis is not None:
return _redis
with _lock:
if _redis is None:
import redis
_redis = redis.Redis(
host=settings.SIGNAL_REDIS_HOST, port=settings.SIGNAL_REDIS_PORT,
password=settings.SIGNAL_REDIS_PASSWORD or None,
db=settings.SIGNAL_REDIS_DB_QUOTES, decode_responses=True,
socket_timeout=settings.SIGNAL_REDIS_SOCKET_TIMEOUT)
return _redis
def _bars(code: str):
try:
raw = _r().get(QUOTE_KEY.format(code=code))
if not raw:
return None
bars = json.loads(raw)
return bars if isinstance(bars, list) and bars else None
except Exception as e:
logger.warning("实时行情读取失败 [%s]: %s", code, e)
return None
def get_price(ts_code: str):
bars = _bars(ts_code)
if not bars:
return None
try:
px = float(bars[-1].get("close") or 0)
return px if px > 0 else None
except (TypeError, ValueError):
return None
def get_prices(codes) -> dict:
"""批量取现价 (逐 key GET; 数量级为持仓数, 无需 pipeline 复杂化)。"""
out = {}
for c in codes or []:
out[c] = get_price(c)
return out
def day_snapshot(ts_code: str) -> dict:
"""当日分钟线聚合: 现价/开盘/最高/最低/均价(VWAP近似)/涨幅 —— 择时与不追高检查用。"""
bars = _bars(ts_code)
if not bars:
return {}
try:
closes = [float(b.get("close") or 0) for b in bars if float(b.get("close") or 0) > 0]
if not closes:
return {}
highs = [float(b.get("high") or b.get("close") or 0) for b in bars]
lows = [float(b.get("low") or b.get("close") or 0) for b in bars if
float(b.get("low") or b.get("close") or 0) > 0]
opens = float(bars[0].get("open") or closes[0])
vols = [float(b.get("vol") or 0) for b in bars]
amt = sum(c * v for c, v in zip(closes, vols))
vwap = (amt / sum(vols)) if sum(vols) > 0 else sum(closes) / len(closes)
px = closes[-1]
return {"price": px, "open": opens, "high": max(highs) if highs else px,
"low": min(lows) if lows else px, "vwap": round(vwap, 3),
"day_chg_from_open": (px / opens - 1) if opens else None,
"bar_time": bars[-1].get("time"), "bars": len(bars)}
except (TypeError, ValueError) as e:
logger.warning("行情聚合失败 [%s]: %s", ts_code, e)
return {}
# ================================================================ 参考位
def _shard_tables(months: int = 4) -> list:
now = datetime.now()
out, d = [], now
for _ in range(max(1, months)):
out.append(f"gp_stock_factor_pro_{d.strftime('%Y%m')}")
d = (d.replace(day=1) - timedelta(days=1))
return out
def _factor_rows(ts_code: str, days: int = 70) -> list:
"""逐分表单查 (代理要求单表), 合并后取最近 days 条。symbol 先点式后前缀式试。"""
since = (datetime.now() - timedelta(days=int(days * 1.6))).strftime("%Y-%m-%d")
rows = []
for tbl in _shard_tables():
for sym in (ts_code, downstream_repo.to_prefix(ts_code)):
try:
part = fetch_all(
f"SELECT trade_date, close_qfq, high_qfq, low_qfq, atr_qfq FROM {tbl} "
f"WHERE symbol = :sym AND trade_date >= :since", {"sym": sym, "since": since})
except Exception:
part = []
if part:
rows.extend(part)
break
rows = [r for r in rows if r.get("close_qfq")]
rows.sort(key=lambda r: str(r.get("trade_date")))
return rows[-days:]
def self_calc_refs(ts_code: str, base_cost=None) -> dict:
"""兜底自算参考位 (设计 §13)。数据不足返回 {}"""
rows = _factor_rows(ts_code)
if len(rows) < 20:
return {}
close = [float(r["close_qfq"]) for r in rows]
low = [float(r.get("low_qfq") or r["close_qfq"]) for r in rows]
high = [float(r.get("high_qfq") or r["close_qfq"]) for r in rows]
atr = None
for r in reversed(rows):
if r.get("atr_qfq"):
atr = float(r["atr_qfq"])
break
ma20 = sum(close[-20:]) / 20
support = max(ma20, min(low[-20:]) * 1.01)
pressure = max(high[-60:]) if len(high) >= 60 else max(high)
stop = None
if base_cost and atr:
stop = max(float(base_cost) - param_store.get_float("PMS_STOP_ATR_MULT", 2.0) * atr,
support)
elif atr:
stop = max(close[-1] - param_store.get_float("PMS_STOP_ATR_MULT", 2.0) * atr, support)
return {"support": round(support, 3), "pressure": round(pressure, 3),
"stop": round(stop, 3) if stop else None, "atr": atr, "ma20": round(ma20, 3),
"source": "self_calc", "bars": len(rows)}
def get_refs(ts_code: str, *, base_cost=None) -> dict:
"""参考位: 决策系统主口径 → 日龄超期/缺失时兜底自算 → 都拿不到返回 source=none。"""
stale_days = param_store.get_int("PMS_REF_STALE_TDAYS", 3)
try:
r = downstream_repo.fetch_refs(ts_code)
except Exception as e:
logger.warning("读决策系统结论失败 [%s]: %s", ts_code, e)
r = None
if r and (r.get("support") or r.get("pressure")):
age = _age_days(r.get("trade_date"))
if age is not None and age <= stale_days * 2: # 自然日宽松换算交易日
return {"support": r.get("support"), "pressure": r.get("pressure"),
"stop": r.get("support"), "source": "bionic",
"trade_date": str(r.get("trade_date")), "age_days": age}
try:
s = self_calc_refs(ts_code, base_cost=base_cost)
except Exception as e:
logger.warning("自算参考位失败 [%s]: %s", ts_code, e)
s = {}
if s:
s["note"] = "兜底口径 (决策系统结论缺失或停更)"
return s
return {"support": None, "pressure": None, "stop": None, "source": "none",
"note": "参考位不可用 —— 敞口无法计算, 相关动作按保守处理"}
def _age_days(trade_date):
if not trade_date:
return None
try:
s = str(trade_date)[:10].replace("-", "")
d = datetime.strptime(s, "%Y%m%d").date()
return (datetime.now().date() - d).days
except (ValueError, TypeError):
return None