tradingSystem/Makefile

246 lines
14 KiB
Makefile
Raw Normal View History

2026-07-31 13:28:10 +08:00
# tradingSystem (PMS) 常用操作一句话入口
# ============================================================================
# 为什么要这个文件: 日常命令都是「docker compose run --rm --no-deps pms-web python
# scripts/xxx.py」这种一行八十字符的东西, 手打容易漏 --no-deps (于是顺带把 beat/worker
# 也拉起来)、漏 --rm (于是攒一堆退出的容器)。把口径固定在这里, 少一类手误。
#
# make help 看有哪些目标
# make deploy 拉代码 → 重建镜像 → 重建容器 → 建表 (= scripts/deploy.sh)
# make test 跑全部单测 (不连库, 秒级)
#
# **改了 Python 代码必须 build + force-recreate**, 因为源码是打进镜像的 ——
# `docker compose restart` 跑的还是旧镜像里的旧代码, 而且一点报错都没有 (deploy.sh
# 头部有详细说明)。`make up` 用的就是这条路径, 别用 `docker compose restart` 代替。
# ============================================================================
.DEFAULT_GOAL := help
SHELL := /bin/bash
# 各 profile 的组合。只想跑页面: `make up PROFILES=`
PROFILES ?= --profile sched --profile ws
DC := docker compose $(PROFILES)
# 一次性命令一律 --no-deps: 不然跑个单测都会把 beat/worker 拉起来
RUN := docker compose run --rm --no-deps pms-web
2026-08-03 09:10:56 +08:00
.PHONY: help deploy deploy-local build up down ps logs test stale initdb check health \
probe changes industry ws-status rebuild rebuild-accept \
2026-08-03 10:37:42 +08:00
watch loop purge-dead-orders t-pool t-plan t-pre t-issue t-cmd t-cmds \
t-catalog t-plans t-mat t-dry t-tick t-ins t-book t-gate reset-ledger shell
2026-07-31 13:28:10 +08:00
help: ## 列出所有目标
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
# ---------------------------------------------------------------- 部署
deploy: ## 一句话部署: git pull → build → up --force-recreate → 建表
@./scripts/deploy.sh
deploy-local: ## 同 deploy 但跳过 git pull (本地已改好)
@./scripts/deploy.sh --no-pull
build: ## 只重建镜像
$(DC) build
up: ## 起服务 (force-recreate —— 让新镜像真正生效)
$(DC) up -d --force-recreate
@$(DC) ps
down: ## 停掉全部服务 (不删卷)
$(DC) down
ps: ## 看服务状态
@$(DC) ps
logs: ## 跟日志 (make logs S=pms-ws 只看一个服务)
$(DC) logs -f $(S)
# ---------------------------------------------------------------- 自检
2026-08-03 09:10:56 +08:00
test: ## 全部单测 (先查镜像新旧再跑; 应输出 ALL SUITES PASS)
@$(MAKE) --no-print-directory stale
2026-07-31 13:28:10 +08:00
$(RUN) python scripts/run_tests.py
2026-08-03 09:10:56 +08:00
stale: ## 容器里的代码跟工作树对不对得上 (git pull 之后没 build 的话这里会喊)
@host=$$(python3 scripts/code_fingerprint.py 2>/dev/null || echo UNKNOWN); \
img=$$($(RUN) python scripts/code_fingerprint.py 2>/dev/null | tr -d '\r' | tail -1); \
if [ -z "$$img" ]; then img=UNKNOWN; fi; \
if [ "$$host" = UNKNOWN ] || [ "$$img" = UNKNOWN ]; then \
echo " ?? 指纹取不到 (工作树=$$host 镜像=$$img), 跳过新旧判断"; \
elif [ "$$host" = "$$img" ]; then \
echo " OK 镜像与工作树一致 ($$host)"; \
else \
echo ""; \
echo " ##################################################################"; \
echo " # 镜像里的代码不是你工作树里的代码"; \
echo " # 工作树 $$host"; \
echo " # 镜像 $$img"; \
echo " #"; \
echo " # 源码是打进镜像的, git pull 不更新容器。此刻跑任何 make 目标"; \
echo " # (test / rebuild / t-*) 都是在跑旧代码 —— 而它照样会 PASS。"; \
echo " # 先跑: make deploy (本地已改好则 make deploy-local)"; \
echo " ##################################################################"; \
echo ""; \
fi
2026-07-31 13:28:10 +08:00
initdb: ## 建表/补表 (幂等; 加 DRY=1 只演练打印)
$(RUN) python scripts/init_db.py $(if $(DRY),,--yes)
check: ## 实机连通性与表结构自检 (需真实 .env)
$(RUN) python scripts/check_db.py
health: ## 页面健康检查 (配置装载 + 库连通自证)
@curl -s http://127.0.0.1:38100/health | python3 -m json.tool
# ---------------------------------------------------------------- 上游 / 行业 / 通道
probe: ## 上游计划实机探活 (只读; 加 SNAP=1 顺带落一份名册快照)
$(RUN) python scripts/probe_plan_api.py $(if $(SNAP),--snapshot,)
changes: ## 榜单变化: 谁新进谁掉榜、持仓票有没有被上游摘了
@curl -s 'http://127.0.0.1:38100/api/upstream/plan-changes?limit=30' \
| python3 -m json.tool
industry: ## 行业源实探 (ready=false 就是硬拦截失效, 别当它绿着)
@curl -s http://127.0.0.1:38100/api/industry | python3 -m json.tool
ws-status: ## ws 通道状态 (连接态 / seq 水位 / 出口队列)
$(RUN) python scripts/ws_smoke.py status
2026-07-31 13:46:15 +08:00
# ---------------------------------------------------------------- 账本重建 (待办 #4)
rebuild: ## 账本重建预检 (只读)。加 GO=1 才真的建账, FORCE=1 连预检不过也建
$(RUN) python scripts/rebuild_ledger.py $(if $(GO),--yes,) $(if $(FORCE),--force,)
rebuild-accept: ## 只看判收: 安全垫分布 / 批次账 / 行业集中度
$(RUN) python scripts/rebuild_ledger.py --accept
# ---------------------------------------------------------------- 联测 A 段 (shadow 下逐跳)
# 一步一停, 每步都能看清楚 —— 别开 beat, 那会让几个调度位同时动, 出了岔子分不清是谁干的。
# 全程 PMS_DISPATCH_MODE=shadow: 指令照常生成、照常过规则闸、照常记账, **只是不写下游**。
# 顺序就是生产里那几个调度位的顺序, 只是改成手动触发。
2026-07-31 14:19:36 +08:00
#
# **A3 之前必须先下一条「任务命令」。** 参数命令 (SET_SCALE / SET_PORTFOLIO_CAP /
# SET_STOCK_CAP / SET_MAX_NAMES …) 只是设约束, **本来就不产生方案** —— 页面上它们显示
# 「0 条方案」是对的, 不是坏了。要走通链路得下 B/C 组的任务命令:
# INCREASE_EXPOSURE 升仓 pct% (全局: 垫厚票补到目标 + 候选池新票建仓)
# OPEN_TARGET 建仓某股至 x% (个股: 分批 50/25/25, 含一手合并)
#
# **这里的「择时」是 PMS 内置的实现 B, 不经决策系统。** 设计里择时有两个实现:
# 实现 A = 委托决策系统盘中择时 —— 待办 #9, 等 bionic 侧接口, **还没接**
# 实现 B = PMS 自己算 (分日配额 / 分笔 / VWAP / 回踩 / 不追高 / 14:45 兜底) —— 已实现
# 所以跑 A 段不需要动决策系统的任何逻辑, 它这一段本来就不参与。
A ?= http://127.0.0.1:38100
2026-08-03 10:28:09 +08:00
# 中文必须原样出来: `json.tool` 默认把中文转义成 \uXXXX, 而这些接口的 reason 恰恰是最该
# 看的东西。**不能用 `json.tool --no-ensure-ascii`** —— 这条管道跑在**宿主机**上不是容器里,
# 宿主机的 python3 可能低于 3.9 (2026-08-03 实机: unrecognized arguments)。下面这行任何
# python3 都认。解析失败时原样透传, 免得把错误信息也一起吞了。
J := python3 -c 'import sys,json;\
d=sys.stdin.read();\
sys.stdout.write(json.dumps(json.loads(d),ensure_ascii=False,indent=2)+"\n" if d.strip().startswith(("{","[")) else d)'
2026-08-03 09:36:36 +08:00
# t-pool 与 t-plans 是**两个完全不同的东西**, 别被名字带偏 (原名 t-plan/t-plans 只差一个 s,
# 实测把人绕进去过):
# t-pool = 上游选股计划 = **候选池** = "我可以买哪些票" (400 只, 来自 /plan 接口)
# t-plans = PMS 自己排的 **方案** = "买哪只、多少股、分几批" (来自你下的命令)
# 候选池是命令的**原料**, 方案是命令的**产物**。候选池满了不等于会有方案 —— 中间要有命令。
2026-08-03 10:09:16 +08:00
watch: ## 【盯盘看这个】一屏看完整条链 (只读)。加 N=10 每 10 秒刷; 加 WIDE=1 连账本一起
$(RUN) python scripts/watch.py $(if $(N),-n $(N),) $(if $(WIDE),--wide,)
2026-08-03 10:37:42 +08:00
# beat 停着的时候, watch 只是"看", 不会推动任何事 —— 得有人跑 t-tick 才会评估与出手。
# 这个目标把「跑一跳 + 看一眼」串起来, 手动联测期用它代替 beat。默认 20 秒一轮。
# 一跳择时的**一行摘要** (curl 跑在宿主机, 与其它 t-* 目标同一条路)
TICKSUM := python3 -c 'import sys,json;\
d=(json.loads(sys.stdin.read()) or {}).get("data") or {};\
e=d.get("errors") or [];\
print(" ▸ tick: 检查 %s · 出手 %s · 等待 %s · 拒绝 %s%s" % (d.get("checked"),\
len(d.get("fired") or []), len(d.get("waited") or []), len(d.get("rejected") or []),\
(" ⚠ 错误 " + json.dumps(e, ensure_ascii=False)) if e else ""))'
2026-08-03 10:54:30 +08:00
# **出手 + 入账两跳都要跑**: exec-tick 只发单, 成交要 replay 那一跳才认领进账本。
# 第一版只跑了 exec-tick —— 那样一旦成交, 单子成了而账本一直空, 看着像成交没回来。
REPSUM := python3 -c 'import sys,json;\
d=(json.loads(sys.stdin.read()) or {}).get("data") or {};\
w=d.get("ws") or {}; e=(d.get("errors") or []);\
print(" ▸ 入账: ws 成交 %s → 记账动作 %s · 外部成交 %s%s%s" % (w.get("trades",0),\
w.get("actions",0), d.get("fills",0),\
(" 挂起 " + str(len(w.get("held") or []))) if w.get("held") else "",\
(" ⚠ 错误 " + json.dumps(e, ensure_ascii=False)) if e else ""))'
loop: ## 手动联测循环: 每 N 秒跑「出手一跳 + 入账一跳」再刷监视 (beat 停着时用)
@echo "$(or $(N),20) 秒: 出手一跳 + 入账一跳 → 刷新监视。Ctrl-C 退出。"
2026-08-03 10:37:42 +08:00
@while true; do \
sum=$$(curl -s -X POST '$(A)/api/ops/exec-tick' | $(TICKSUM)); \
2026-08-03 10:54:30 +08:00
rep=$$(curl -s -X POST '$(A)/api/ops/replay' | $(REPSUM)); \
2026-08-03 10:37:42 +08:00
$(MAKE) --no-print-directory watch; \
2026-08-03 10:54:30 +08:00
echo "$$sum"; echo "$$rep"; \
2026-08-03 10:37:42 +08:00
sleep $(or $(N),20); \
done
2026-08-03 09:36:36 +08:00
t-pool: ## A1 拉**候选池**(上游选股计划="能买哪些"; = 08:40 调度位)
@curl -s -X POST '$(A)/api/ops/plan-refresh' | $(J)
2026-08-03 09:36:36 +08:00
t-plan: t-pool ## (t-pool 的旧名; 别跟 t-plans 混, 那个是 PMS 排的方案)
t-pre: ## A2 盘前准备: T+1 可卖重置 / 参考位 / 刹车结算 (= 08:50 调度位)
@curl -s -X POST '$(A)/api/ops/premarket' | $(J)
2026-08-03 09:36:36 +08:00
t-issue: ## A3a 下一条命令 (例: make t-issue T=INCREASE_EXPOSURE P='{"pct":0.3}')
@test -n "$(T)" || { echo "用法: make t-issue T=<命令类型> P='<参数JSON>'"; \
echo " 升仓三成: make t-issue T=INCREASE_EXPOSURE P='{\"pct\":0.3}'"; \
echo " 单股建仓: make t-issue T=OPEN_TARGET P='{\"ts_code\":\"600000.SH\",\"target_pct\":0.06}'"; \
echo " 看全部命令类型: make t-catalog"; exit 1; }
@curl -s -X POST '$(A)/api/commands' -H 'Content-Type: application/json' \
-d '{"cmd_type":"$(T)","params":$(if $(P),$(P),{}),"by":"make"}' | $(J)
t-cmd: ## A3b 命令轮询: 把**已下达**的命令排成方案 (它自己不产生命令, 先 t-issue)
@curl -s -X POST '$(A)/api/ops/plan-pending' | $(J)
2026-08-03 09:36:36 +08:00
t-cmds: ## A3c 看命令台: 现在有哪些命令、什么状态
@curl -s '$(A)/api/commands?limit=30' | $(J)
t-catalog: ## 看命令目录: 27 类命令各要什么参数 (行业源没配时相关命令会置灰)
@curl -s '$(A)/api/commands/catalog' | $(J)
t-plans: ## A4 看**方案**("买哪只、多少股、分几批", 还没成指令; 与 t-pool 不是一回事)
@curl -s '$(A)/api/plans?limit=50' | $(J)
t-mat: ## A5 方案 → 指令 (先记账后动作: 指令先落表)
@curl -s -X POST '$(A)/api/ops/materialize' | $(J)
2026-07-31 14:19:36 +08:00
t-dry: ## A6 择时试算 (实现B, 不经决策系统; dry_run 只算不发)
@curl -s -X POST '$(A)/api/ops/exec-tick?dry_run=true' | $(J)
2026-07-31 14:19:36 +08:00
t-tick: ## A7 真出手 (shadow 下 = 置 DISPATCHED 但不写下游, 你照着在 QMT 手工下)
@curl -s -X POST '$(A)/api/ops/exec-tick' | $(J)
2026-08-03 10:54:30 +08:00
# **出手和入账是两条独立的路**, exec-tick 只管发单不管收账。成交回来落在 pms_qmt_inbox,
# 要 replay 这一跳才会认领进批次、算摊薄成本与安全垫。beat 停着时忘了跑它, 表现是
# 「待入账」一直涨而账本持仓一直空 —— 看着像成交没回来, 其实是没人去收。
t-replay: ## A7.5 成交入账 (ws 逐笔 + trading_order 增量 + 盘中轻对账)
@curl -s -X POST '$(A)/api/ops/replay' | $(J)
t-ins: ## A8 看指令与子单状态
@curl -s '$(A)/api/instructions?limit=50' | $(J)
t-book: ## A9 看账本 (持仓 / 批次 / 安全垫 / 行业)
@curl -s '$(A)/api/positions' | $(J)
t-gate: ## 看评审账本: 规则闸/研判闸拒了什么、为什么 (联测时最该盯的一张表)
@curl -s '$(A)/api/ledger?limit=50' | $(J)
2026-07-31 13:28:10 +08:00
# ---------------------------------------------------------------- 危险操作 (要确认)
2026-08-03 10:37:42 +08:00
purge-dead-orders: ## 删掉出口表里**无成交的终态委托** (清账重来前跑; 必须 CONFIRM=1)
@test "$(CONFIRM)" = "1" || { echo "需要 CONFIRM=1。这会删 pms_qmt_order 里"; \
echo "status 属于 EXPIRED/CANCELLED/REJECTED/ABORTED 且 cum_qty=0 的行。"; \
echo "为什么要删: reset_ledger 默认保留通道两表, 于是旧子单号还占着唯一索引;"; \
echo "重来后新指令很可能拿到同一个 seq → 同一个 _D01 子单号 → 撞唯一索引发不出去。"; \
echo "只删无成交的终态行, 有成交的一律不动 (那是账的依据)。"; exit 1; }
$(RUN) python -c "from app.db.session import execute;\
n=execute(\"DELETE FROM pms_qmt_order WHERE status IN ('EXPIRED','CANCELLED','REJECTED','ABORTED') AND cum_qty = 0\");\
print('已删除 %s 行无成交的终态委托' % n)"
2026-07-31 13:28:10 +08:00
reset-ledger: ## 清空账本重来 (影子期专用; 必须 CONFIRM=1)
@if [ "$(CONFIRM)" != "1" ]; then \
echo "这会清空 PMS 账本。确认请加 CONFIRM=1, 例如:"; \
echo " make reset-ledger CONFIRM=1 ARGS='--purge-channel --reset-ws'"; exit 1; fi
$(RUN) python scripts/reset_ledger.py --yes $(ARGS)
shell: ## 进容器 (排查用)
docker compose run --rm --no-deps pms-web bash