丰富交易逻辑

This commit is contained in:
zlt 2026-08-11 16:19:26 +08:00
parent 69a0857cbc
commit f2f144454b
2 changed files with 115 additions and 38 deletions

View File

@ -72,6 +72,31 @@ def _post(url: str, payload: dict, timeout: int) -> dict:
return r.json() or {} return r.json() or {}
def _strategy_immediate(*, side, now, day, quota, fired_today) -> dict:
"""网格/跟踪止盈的到价即成交决策: 只做硬事实检查(配额/时段/停牌/一字板), 不做 vwap 择时,
不做 14:45 强制真正的合规(单股上限/T+1/暂停买入等) run_tick 随后的 rule_gate 终检兜底
这样网格是"到档位价才成交、当日没成交则由窗口收口作废", 不会像命令那样被收盘强平成市价单"""
left = max(0, int(quota) - int(fired_today))
now_min = et.hm_to_min(now)
def _o(a, r, limit=None):
return {"action": a, "qty_hint": left, "limit_price": limit, "reason": r, "source": "策略即时"}
if left <= 0:
return _o(et.ACT_WAIT, "当日配额已出完")
if not et.in_session(now_min):
return _o(et.ACT_WAIT, "非交易时段")
price = _f(day.get("price"))
if price <= 0 or day.get("halted"):
return _o(et.ACT_SKIP, "停牌或无实时价, 当日顺延")
if str(side).lower() == "buy" and day.get("limit_up"):
return _o(et.ACT_WAIT, "涨停封板, 不追买")
if str(side).lower() == "sell" and day.get("limit_down"):
return _o(et.ACT_WAIT, "跌停封板, 挂单等回封")
disc = 1.002 if str(side).lower() == "buy" else 0.998
return _o(et.ACT_FIRE, f"按档触发即时成交, 现价 {price}", round(price * disc, 2))
def decide(*, side: str, action: str, ts_code: str, now, day: dict, params: dict, def decide(*, side: str, action: str, ts_code: str, now, day: dict, params: dict,
is_last_day: bool, is_command: bool = True, fired_today: int = 0, quota: int = 0, is_last_day: bool, is_command: bool = True, fired_today: int = 0, quota: int = 0,
pos: dict = None, tdays_left=None, prog: dict = None) -> dict: pos: dict = None, tdays_left=None, prog: dict = None) -> dict:
@ -85,6 +110,11 @@ def decide(*, side: str, action: str, ts_code: str, now, day: dict, params: dict
prog 由调用方传入指令的 progress dict, 咨询结果/失败冷却会写进 prog["exec_advice"], prog 由调用方传入指令的 progress dict, 咨询结果/失败冷却会写进 prog["exec_advice"],
随调用方既有的落表动作持久化; None 则本轮结论不缓存 (dry_run 语义) 随调用方既有的落表动作持久化; None 则本轮结论不缓存 (dry_run 语义)
""" """
# 网格/跟踪止盈: 策略层已按档位触发, 这里不做择时博弈, 到价即时成交、收盘不强制。
# 只识别策略来源且类型为 GRID/TRAIL 的指令; 做T(T0)与其余来源一律走原有逻辑, 不受影响。
if (prog or {}).get("origin") == "strategy" and str((prog or {}).get("stype")) in ("GRID", "TRAIL"):
return _strategy_immediate(side=side, now=now, day=day, quota=quota, fired_today=fired_today)
if not available(): if not available():
d = et.decide(side=side, now=now, day=day, params=params, is_last_day=is_last_day, d = et.decide(side=side, now=now, day=day, params=params, is_last_day=is_last_day,
is_command=is_command, fired_today=fired_today, quota=quota) is_command=is_command, fired_today=fired_today, quota=quota)

View File

@ -113,7 +113,9 @@ def _emit_instruction(st: dict, dec: dict, *, forced: bool = False) -> str:
code = st["ts_code"] code = st["ts_code"]
now = datetime.now() now = datetime.now()
iid = f"STR{_today()}{now.strftime('%H%M%S')}_{code.replace('.', '')}_{dec['leg'][:1]}"[:40] iid = f"STR{_today()}{now.strftime('%H%M%S')}_{code.replace('.', '')}_{dec['leg'][:1]}"[:40]
prog = {"is_command": True, "deadline": str(_today()), "children": [], is_cmd = (st.get("type") == "T0") # 做T必须当日轧平→命令口径(含14:45强制平回);
# 网格/跟踪止盈到价即成交、当日没成交则作废, 不强制
prog = {"is_command": is_cmd, "deadline": str(_today()), "children": [],
"origin": "strategy", "strategy_id": st["strategy_id"], "stype": st.get("type"), "origin": "strategy", "strategy_id": st["strategy_id"], "stype": st.get("type"),
"leg": dec["leg"], "reason": dec.get("reason"), "forced": bool(forced)} "leg": dec["leg"], "reason": dec.get("reason"), "forced": bool(forced)}
pms_repo.insert_instruction( pms_repo.insert_instruction(
@ -314,9 +316,21 @@ def _grid_levels(params: dict) -> list:
return levels return levels
def _band(levels: list, price: float) -> int:
"""现价所处的档位下标: 满足 levels[i] <= price 的最大 i; 低于最低档返回 -1。"""
b = -1
for i, lv in enumerate(levels):
if lv <= price:
b = i
else:
break
return b
def _eval_grid(st, pos, day, now, ctx): def _eval_grid(st, pos, day, now, ctx):
"""网格: 现价跌破未买档 → 买 1 份; 现价涨破已买档 → 卖 1 份(从 avail); 越上界停做; """网格(逐档穿越): 价每向下跌破一个新档买一手(只买中枢下方), 向上涨破一个档就把下面对应
跌破下界 = 继续持有不再买 (设计四点拍板①)买入暂停时只停买入卖出照常""" 档买的那手卖掉( avail)一跳只走一档; 跌破下界=继续持有不再买; 买入暂停只停买卖出照常;
filled_levels 每跳与真实 ADD 持仓对账, 被对账冲销后收敛 绝不卖幻影档(误卖底仓)"""
state = ctx["state"] state = ctx["state"]
params = st.get("params") or {} params = st.get("params") or {}
price = _f(day.get("price")) price = _f(day.get("price"))
@ -325,57 +339,90 @@ def _eval_grid(st, pos, day, now, ctx):
levels = _grid_levels(params) levels = _grid_levels(params)
if not levels: if not levels:
return None return None
lo, hi = levels[0], levels[-1] lo = levels[0]
step = round(levels[1] - levels[0], 3) if len(levels) > 1 else 0
per_lot = _round_lot(params.get("per_lot")) or LOT per_lot = _round_lot(params.get("per_lot")) or LOT
max_capital = _f(params.get("max_capital")) max_capital = _f(params.get("max_capital"))
filled = {int(k): v for k, v in (state.get("filled_levels") or {}).items()} filled = {int(k): dict(v) for k, v in (state.get("filled_levels") or {}).items()}
invested = _f(state.get("invested")) invested = _f(state.get("invested"))
avail = int(pos.get("avail_qty") or 0) avail = int(pos.get("avail_qty") or 0)
actual_add = int(pos.get("add_qty") or 0) # 网格买入记 ADD 批次, 这是真实网格持仓
# 跌破下界: 停止买入这一侧、保留已买、告警 (已买档回升仍按规则卖) # —— 对账收敛: filled 声称的网格股 > 真实(被 RECON 冲销) → 收敛; 真实为0则清空重来 ——
claimed = sum(int(v.get("qty") or 0) for v in filled.values())
if claimed > actual_add:
if actual_add <= 0:
if filled:
ctx["notes"].append(f"{st['ts_code']} 网格持仓已被对账冲销(真实网格股0), 清空网格档位重来")
filled, invested = {}, 0.0
state["last_band"] = None
else:
for k in sorted(filled.keys()):
if claimed <= actual_add:
break
q = int(filled[k].get("qty") or 0)
invested = max(0.0, invested - _f(filled[k].get("price")) * q)
claimed -= q
del filled[k]
ctx["notes"].append(f"{st['ts_code']} 网格档位与真实持仓对齐(真实网格股 {actual_add})")
state["filled_levels"] = {str(k): v for k, v in filled.items()}
state["invested"] = invested
cur = _band(levels, price)
last = state.get("last_band")
# 跌破下界: 停买、保留已买、告警; 记基准档但不交易
if price < lo: if price < lo:
if not state.get("below_floor"): if not state.get("below_floor"):
state["below_floor"] = True state["below_floor"] = True
ctx["notes"].append(f"{st['ts_code']} 跌破网格下界 {lo}, 已停止网格买入(继续持有已买档)") ctx["notes"].append(f"{st['ts_code']} 跌破网格下界 {lo}, 已停止网格买入(继续持有已买档)")
else: state["last_band"] = cur
return None
state["below_floor"] = False state["below_floor"] = False
# 卖出: 现价涨破某已买档 (买价 + 一档) → 卖那一份 (从 avail, T+1 由 run_tick 封顶) # 首跳: 只记基准档, 不交易 (等价格真正穿越档位才动)
sell_idx, sell_buyprice = None, -1.0 if last is None:
for idx, info in filled.items(): state["last_band"] = cur
bp = _f(info.get("price")) return None
if bp > 0 and price >= bp + step and bp > sell_buyprice:
sell_idx, sell_buyprice = idx, bp # —— 上行: 价涨破 → 卖掉离开的这一档买的那手 (从 avail, 且确有网格股, 才卖) ——
if sell_idx is not None and avail >= LOT: if cur > last:
k = last
if k in filled and avail >= LOT and actual_add >= LOT:
q = min(per_lot, _round_lot(avail)) q = min(per_lot, _round_lot(avail))
if q >= LOT: if q >= LOT:
return {"side": "sell", "action": A_SELL, "qty": q, "leg": f"grid_sell:{sell_idx}", state["last_band"] = last + 1
"grid_sell_idx": sell_idx, info = filled[k]
"reason": f"网格卖: 现价 {price} 涨破买档 {sell_buyprice}(+一档 {step}), 卖 {q}"} return {"side": "sell", "action": A_SELL, "qty": q, "leg": f"grid_sell:{k}",
"grid_sell_idx": k,
# 买入: 现价跌破某未买档 → 买一份 (受 max_capital 与 单股上限[规则闸] 双约束) "reason": f"网格卖: 现价 {price} 涨破档{k}(买价 {info.get('price')}), 卖 {q}"}
# 下界之下 / 越上界 / 决策系统风控预警暂停买入 时, 都不再买 (卖出不受影响, 已在上面处理)。 return None # 有档可卖但量不足, 先不推进, 下跳再试
if state.get("below_floor") or price < lo or price > hi or ctx.get("buy_paused"): state["last_band"] = last + 1 # 该档无网格持仓可卖, 只随价上移
return None return None
buy_idx, buy_level = None, -1.0
for i, lv in enumerate(levels): # —— 下行: 价跌破新档 → 买这一档 (只买中枢下方; 受下界/暂停/上限约束) ——
if i in filled: if cur < last:
continue k = last - 1 # 刚跌破的这一档
if price <= lv and lv > buy_level: # 现价已跌到/跌破该档 if k < 0:
buy_idx, buy_level = i, lv state["last_band"] = cur
if buy_idx is not None: return None
if k in filled:
state["last_band"] = last - 1 # 已买, 只推进
return None
if ctx.get("buy_paused"):
return None # 暂停买入: 原地等, 不推进
need = per_lot * price need = per_lot * price
if max_capital > 0 and invested + need > max_capital + 1e-6: if max_capital > 0 and invested + need > max_capital + 1e-6:
if not state.get("cap_hit"): if not state.get("cap_hit"):
state["cap_hit"] = True state["cap_hit"] = True
ctx["notes"].append(f"{st['ts_code']} 网格已达最大投入 {max_capital:.0f} 元, 暂停买入") ctx["notes"].append(f"{st['ts_code']} 网格已达最大投入 {max_capital:.0f} 元, 暂停买入")
return None return None # 触顶: 不推进, 下次重试
state["cap_hit"] = False state["cap_hit"] = False
return {"side": "buy", "action": A_GRID_BUY, "qty": per_lot, "leg": f"grid_buy:{buy_idx}", state["last_band"] = last - 1
"grid_buy_idx": buy_idx, "grid_buy_price": price, return {"side": "buy", "action": A_GRID_BUY, "qty": per_lot, "leg": f"grid_buy:{k}",
"reason": f"网格买: 现价 {price} 跌破档位 {buy_level}, 买 {per_lot}"} "grid_buy_idx": k, "grid_buy_price": price,
return None "reason": f"网格买: 现价 {price} 跌破档{k}(档价 {levels[k]}), 买 {per_lot}"}
return None # cur == last, 同档不动
# ================================================================ 评估器: 跟踪止盈 (设计 §七B) # ================================================================ 评估器: 跟踪止盈 (设计 §七B)