24 KiB
开发节点记录(三系统量化交易)
这份文件的用途只有一个:让下一次交接不必重新查一遍。 每个重要节点写一条,写完就补,不攒到最后。一条节点包含五样东西: 做了什么、动了哪些文件、部署方式、真机判收结果、还欠着什么。
三个系统的部署方式(改错等于白改):
- tradingSystem(PMS) 跑在
factor@factorevaluation,源码打进镜像, 改码必须make deploy,跑完make test见 ALL SUITES PASS,盯盘make watch。 容器时钟是北京时间。- bionic_trader(决策系统) 跑在
192.168.16.188:38000,挂载卷,git pull后docker compose restart backend-api worker-brain即生效; 新增 env 必须docker compose up -d --force-recreate,restart 不重读 env。 容器时钟是 UTC,看时间戳先加八小时。- akg-factor-bridge(因子桥) 在
factorevaluation,容器名akg_factor_bridge, API 在 8300,常驻 API 改码要 compose restart。容器时钟是 UTC。记录纪律:判收状态只写「真机见过」或「未判收」两种,不写「应该没问题」。 时间一律写北京时间,引用桥或决策系统的日志时间时先加八小时并标注已换算。
2026-08-06 · 新建仓动作:方案已出,待拍板
做了什么 读完 PMS 的动作引擎主链代码,出了《给动作引擎加「新建仓」动作》的方案,等待拍板。 一行代码都还没改。
读代码得到的、与既有文档不一致的事实(以代码为准)
OPEN这个动作名在 PMS 里已经存在,而且从方案到下单的整条管道是通的:planner.py:31定义A_OPEN、executor.py:41的BUY_ACTIONS已含OPEN、command_service.py:647已经在用action="OPEN"写评审账本。 缺的只有自主提议这一段:action_engine.py:27的动作词表没有 OPEN,action_engine.scan()的输入是持仓列表,结构上不可能引入新标的。caps_ctx(view, ts_code=...)对从未持仓过的票不会带出行业名, 不显式传sector的话,行业集中度那道硬拦截会整段静默跳过。- 从未持仓过的票在
pms_position里没有行,update_position会影响 0 行。 建行要显式调ensure_position,它只写ts_code / status='PLANNED' / updated_at。 command_spec.py里那个"planner": "..."字段全代码库没有任何一处读它, 真正的分派是command_service._dispatch_planner里一串硬编码的if cmd_type ==。- 参考位取数(
ledger_service盘前那一跳)只对only_open=True的持仓取, 候选票没有支撑压力。这对择时实现 A 无影响(它用决策系统自己库里的结论), 但实现 B 的「回踩带」判据对新票是空的。
动了哪些文件
无。方案文件 NEW_POSITION_ACTION_PLAN.md 已放进 tradingSystem 项目目录。
部署方式 不涉及。
真机判收 未判收(尚未动码)。
还欠着什么 方案第十节的四件待拍板:新建仓的档位怎么给、每天最多新开几只、研判闸这一轮做不做、 盘中参考位漂移要不要加防护。拍板之后才动手。
2026-08-06 · 新建仓动作:四条取舍已拍板,决策系统侧评估完毕,方案定稿
做了什么 四条取舍拍板:新建仓单独一个档位参数且默认开启;不设每日开仓上限,由既有上限、资金、 候选池与择时自己收敛;研判闸这一轮双侧一并加上;参考位漂移做首答锁定加偏离即停。 按第三条读了决策系统的相关代码,方案更新为定稿版 V2。代码仍然一行未改。
读决策系统代码得到的三条事实(以代码为准)
- 择时那一侧一个字都不用改。
app/services/pms_advisor.py的_advise(163 到 226 行)从头到尾没有读过action字段,只用ts_code、side、day.price。 买入区间由昨夜支撑压力推出,与这笔买单是建新仓还是加老仓无关。 - 研判那一侧没有任何 action 白名单。
app/api/main.py:323的路由是裸 dict, 只校验总开关与ts_code非空;worker 侧tasks_intraday.py:512是_task_by_action.get(_act, 兜底文案)。所以今天发action="OPEN"就能跑通, 但拿到的是给加仓写的兜底文案,后面还跟着一句「仓位纪律不归你管」。 要改的是三处,都在workers/tasks_intraday.py:加一条 OPEN 判据、 OPEN 且现价缺失时显式回 UNAVAILABLE、边界声明按动作分岔。 - 新票的现价链路最脆弱。
tasks_intraday.py449 到 456 行,新票没有持仓快照,fetch_realtime_close读不到分钟线时返回0.0被or None变成 None, 提示词渲染成「当前现价 未知 元」。加仓类还有摊薄成本可依,建仓判断没有现价锚不成立。
新撞出来的一条硬约束(工程事实,不是取舍)
app/scheduler.py:45 给所有调度任务设了 task_soft_time_limit=240, task_time_limit=300,
而 judge.request 是同步阻塞、单次超时上限 90 秒。三只票送研判就是 270 秒,已经超过
软超时。 今天没出事是因为送研判的只有三只持仓票且多数轮次被去重挡掉。新建仓不节流、
候选池默认取前 30 只,第一跳就会捅穿。
处理办法是给单轮扫描一个研判时间预算 PMS_JUDGE_TICK_BUDGET_SEC(默认 150 秒),
用尽就把剩下的候选留到下一分钟的心跳,并写进 skipped 留痕。
这不是节流:一条候选都没丢,也没有按天计的上限,只是让一次心跳做得完。
动了哪些文件
无代码改动。NEW_POSITION_ACTION_PLAN.md 更新为定稿版 V2,已放进项目目录。
部署方式
不涉及。定稿版里写明:决策系统这次只改 worker 代码、不动 .env,
所以 git pull 后 docker compose restart backend-api worker-brain 即可;
哪天真要往 .env 写值(比如切研判独立队列)才必须 up -d --force-recreate。
PMS 侧因为新建仓档位默认就是自动执行,建议收盘后 make deploy,
让第一次真实建仓发生在次日开盘、人在场的时候。
真机判收 未判收(尚未动码)。
还欠着什么
等点头后开工。开工顺序:决策系统侧先上并用 pms_smoke.py 手工发一条
action=OPEN 的研判请求验证,再回来做 PMS 侧。
2026-08-06 · 新立一条规矩:动 tradingSystem 以外的系统之前先答三题
做了什么 用户指出决策系统本来就不管仓位(它是聚合大量信号的判断系统),往里面塞仓位信息 会把别的在跑的逻辑带偏。据此立了一条通用规矩,并按它把方案重过一遍,改成 V3。
规矩(改 tradingSystem 以外的系统,动手前先答,答不上就别动)
- 这段代码是不是只有我这条路径会走到?共用的提示词模板、裁决解析、上下文组装、 配置项,都不能在里面加只对我有意义的分支。
- 我加进去的字段,会不会被别的逻辑读到并当真?尤其是写共享表、共享缓存、 往共享 context 里塞键。
- 我的东西全删掉,原系统能不能一字不差地回到今天?答不上「能」就不是加法。
按规矩核过的结论
决策系统这三处改动全部落在两段 if direction == "PMS_JUDGE": 里
(workers/tasks_intraday.py 439 到 523 行、632 到 656 行),不碰共用的提示词模板
(557 到 576 行)、共用的裁决解析(579 到 590 行)、资金与板块注入(530 到 555 行);
不写 decision_ledger;不加 settings 字段;留痕仍只有 PMS_PASS / PMS_REJECT /
PMS_UNAVAILABLE 三个值,而 pms_advisor._today_exit_verdict 第 102 行明确跳过
PMS_ 开头的行,所以写进去的东西不会反过来影响择时。第三题答案是能,git revert 一次。
额外补的一条:仓位数字不进研判请求
judge.request 现在把 hard_numbers 整个塞进请求体,决策系统会逐键渲染进提示词。
新建仓的 hard_numbers 里有名额、可投金额、批次比例——等于当面请一个不该管仓位的系统
去看仓位。所以在 PMS 侧加键白名单,只对 OPEN 生效:送现价、分数、主题、档位、
预期空间、热度、名次;不送名额、金额、批次、组合占比。账本照旧存全量。
重过一遍还改掉与补上的(详见方案 V3 第三节)
改掉两处:去掉 PMS_OPEN_TARGET_PCT 与建持仓行(改由首次成交时既有的
ensure_position 自然建行,省掉僵尸空行的清理);去掉 PMS_OPEN_MIN_SCORE
(与候选池的 PMS_PLAN_MIN_SCORE 重复)。
补上六处,其中三条是真问题:
- 候选之间要滚动扣减。
_route_one每条各自用本轮开始时的旧快照算上限, 一跳内五条各自不超、加起来超,规则闸拦不住。命令那条路有_ctx_after滚动, 自主这条没有。滚动放进scan_open,复用planner.check_all_caps。 - 研判驳回要做当日去重(只对 OPEN)。
_rejected_today_keys只读 arbiter='rule', 研判驳回不在里面,_inflight_keys也认不出,于是被驳回的候选每分钟往账本写一行 一模一样的记录——正是 07-29 那个把判分锚淹掉的教训。 - 可投金额要用真实可用资金封顶,并加
PMS_OPEN_REQUIRE_WS_CASH: 拿不到资金快照就不自动新建仓。规则闸那条「拿不到资金只告警不拦」是为已排好的命令 设计的,不适合无人值守地从零建仓。 另外三条:行业名一次批量取本轮复用;研判预算用尽的候选整条跳过不入人工队列; 新建仓退实现B 时择时判据只剩均价一条(已知降级,要在理由里写明)。
一条顺带发现、但这次不夹带的
industry.get_many 走 gp_stock_category 时是逐只查库、没有缓存,
可以照 _hybk_many 加按日缓存。收益是每跳少几十次查询,但它改的是既有函数的时序行为,
按上面第 1 题的规矩不夹带,要做单独一条、单独拍板。
动了哪些文件
无代码改动。NEW_POSITION_ACTION_PLAN.md 更新为 V3。
部署方式 不涉及。
真机判收 未判收(尚未动码)。
还欠着什么 同上,等点头开工。
2026-08-06 · 收盘后四条只读诊断,外加一个由它们暴露出来的窗口末日追高
四条诊断的结论
-
买入信号 67 条,落在候选池里的只有 1 只(
300570.SZ)。intraday_signals:2026-08-06共 73 条,动作分布 BUY 67 / SELL 6。 说明决策系统的盘中转多判断与上游选股计划的排序是两套几乎不相干的视角—— 互补而不是互相验证。所以「必须同时在候选池里」这条口径在PMS_PLAN_TOP_N=30的当前参数下,插队排序的增量只有 1/67,基本无效。 顺带日志说破另一件事:主榜正好吃满 top=300 (打分池 440),上游还有 140 只没拿到。 两条改法(调大PMS_PLAN_TOP_N,或让只被 top_n 纯排序截断切掉的信号票凭信号捞回来) 记在下面「还欠着什么」里,等明天有真实留痕再定。 -
trading_buy_plan最新一行是 07-30,七天没动。 四个状态全是旧数据 → 老的建仓链路是死腿,不存在两套系统同时买的风险,ENTRY_GATE 关不关都行。 -
悬项四定案:不是口径差。 差额 564(08-05)→ 51(08-06 盘中)→ 4.00 元(收盘后), 递减到接近零,就是取数时点差加收盘价精度差(4 元对 177,561 的持仓市值是 0.002%)。 但「
cash_avail里+ sell_return_today双算」这个假设今天没被验证,只是没被触发 ——今天零卖出成交,那个加法加的是 0。真正的验证要等有卖出成交的那天。 -
悬项八有结论:
trade_no是随机串(T-SHADOW-420911c4b8),不是确定式, S3 收尾函那个阻断项不闭环。后果是幂等:同一笔成交若被重推,随机串识别不出来。 得确认回放那侧的去重键用的是trade_no还是单调的seq。
由诊断暴露出来的问题:窗口末日会强制追高,而自主买入不该这样
ws_smoke inbox 里 000063.SZ 那五笔成交是今天 14:46(时间戳换算),1700 股,
成交价 34.71 到 34.76。而交接信写的是「现价高于买入区间上沿,不追,窗口今天到期」。
判了不追,最后还是买了——走的是 exec_timing.hard_gate 买入分支的窗口末日强制完成,
而 hard_gate 在 exec_advisor.decide 里是先于咨询决策系统跑的,
14:45 之后根本不问择时,直接按 现价×1.002 追进去。
那一笔是命令驱动的(INS_20260804_000063SZ_OPEN_003),强制完成是对的:
用户下过「投这么多」的命令,到期必须完成。但同一段代码等新建仓上线就会作用在自主提议上,
那时它与「可以接受买不上」直接冲突,而且是无人值守。
更要紧的是 window_verdict 早就把口径写死了:"PARTIAL" if is_command else "EXPIRED"
——自主类窗口耗尽直接作废。既然到期就作废,就不该在到期当天先被强制完成一遍。
两处本来是矛盾的,这次是把它们对齐,不是新增策略。
改法
exec_timing.hard_gate新增is_command,必填、故意不给默认值——漏传立刻TypeError,不许静默按某一侧走。买入的窗口末日强制完成只对命令生效,自主返回等待 并写明「到期作废」。- 卖出侧一个字没改,两种来源都照常兜底。这不是漏改:买入的强制完成是「多背一份风险」, 卖出的是「少背一份风险」;自主减仓若也到期作废,那是把该降的风险留在账上。 宁可买不上,但不能卖不掉。
exec_timing.decide与exec_advisor.decide的is_command默认True(命令口径 = 改动前的行为),executor.run_tick从指令的progress.is_command显式传下来。- 影响面说破:已有的 FILL / ADD / DCA 自主买单也跟着改——从前也会在末日强制完成, 现在到期作废。「回踩补足」在末日追高买本身就自相矛盾,所以对它们同样是修正, 但确实是既有行为的改变。
动了哪些文件
app/core/exec_timing.py、app/services/exec_advisor.py、app/services/executor.py;
测试 scripts/test_batch12_units.py 加 6 例(含一条「executor 真的把 is_command 传下去了」
的签名与源码检查,防这道分岔形同虚设),scripts/test_batch11_units.py 的夹具补一个参数。
全量单测 ALL SUITES PASS,464 例。
2026-08-06 · 决策系统的买入信号一直被 PMS 丢在门口,已接上
做了什么 用户问「新建仓能不能并入决策系统原有的信号链路」。查下来先纠正了一个前提,又撞出一个洞。
先纠正的前提:决策系统的建仓侧本来就是轮询,不是订阅。
workers/celery_app.py:94-97 的 entry-gate-poll 是 crontab(minute="*", hour="9-11,13-14"),
每分钟去 trading_buy_plan 捞 is_active=7。真正事件驱动的是告警链
(scripts/intraday_watcher.py 常驻进程 xreadgroup 读上游告警流)与卖出链。
所以「PMS 只能轮询不合理」对既有架构不成立——候选池是日频静态清单,没有事件可订阅,
轮询是它唯一的读法,而且与决策系统自己的建仓入口同构。
撞出来的洞(与 08-05 那次同型):决策系统盘中判出 REVERSAL_BUY 会往 db2 的
intraday_signals:{日期} 广播(workers/tasks_intraday.py:783-785),PMS 一直订阅得到
(signal_service.streams() 第一条就是它,每分钟拉一批),但走到
signal_rules.digest(第 106 到 108 行)被归进 ACT_RECORD,而
signal_service._handle 的 RECORD 分支只给持仓票写账本。
于是「决策系统今天看多了某只没持仓的票」——正是新建仓关心的那一批——PMS 收到了、
计了个数,然后一个字都不留:账本查不到、页面看不见,事后复盘问「那天系统看见了吗」答不上来。
当年那句注释的理由(「买什么买多少由动作引擎决定」)在动作引擎没有新建仓动作时成立,
现在失效了。
改法(口径:信号只做加速器,不改资格)
signal_rules.py:新增ACT_NOTE_BUY。BUY 信号仍然不产生任何买入动作, 但持没持仓都要留痕。HOLD 等其余类型口径一个字未动。signal_service.py:新增ACT_NOTE_BUY分支,落action=SIGNAL_BUY、verdict=NOTE的账本行,按(日期,来源,股票,动作)当日去重。用NOTE不用PASS—— 「记下来」和「放行」在账本里必须分得开。pms_repo.buy_signals_today:只读,今天有转多留痕的票。proposal_service._scan_open→action_engine.scan_open:有信号的候选排最前。 只影响先后,不影响资格——不在候选池里的票不会因为有信号就被建仓, 候选层那一整套过滤(ST、黑名单、分数下限、主题限额、预期空间)一道都不绕。judge.OPEN_JUDGE_KEYS加两个键,把「你自己今天判过这只票转多」送回决策系统, 让它拿自己的结论对照一次。这是定性材料不是仓位数字,符合那条白名单的立意。- 新参数
PMS_OPEN_SIGNAL_PRIORITY(默认开),关掉即退回纯分数排序。
为什么走账本传递而不是让 signal_service 直接调建仓 两边各管各的一件事:信号消化管「收到了、记下来」,动作引擎管「买不买、买多少」, 中间靠账本这个既有事实源接。不新增跨模块调用、不复制闸门逻辑。 代价是最多差一分钟(两个调度位各自每分钟一跳),而后面还要等择时区间,这点延迟无所谓。
插队为什么是实打实的增量:候选按分数降序取,名额只剩两个时第 25 名永远轮不上, 哪怕决策系统刚刚判它转多。分数是昨夜算的静态排名,「此刻转多」是盘中才有的新信息, 两者不同量纲,折算成分数得凭空定系数;插队直接表达了这件事。
动了哪些文件
app/core/signal_rules.py、app/services/signal_service.py、app/repo/pms_repo.py、
app/services/proposal_service.py、app/core/action_engine.py、app/services/judge.py、
config/settings.py、app/services/param_store.py;测试 scripts/test_batch12_units.py
加 7 例,scripts/test_batch5_units.py 与 scripts/test_wiring.py 各改一例
(那两例原本钉着 BUY→ACT_RECORD 的旧行为,「不买」这条口径没变,变的是留痕范围)。
全量单测 ALL SUITES PASS,458 例。
还欠着什么
trading_buy_plan四个状态都有行,说明老的建仓链路还活着。关掉 ENTRY_GATE 之前 必须先确认那些行是不是今天的——若老链路今天仍在挂单,而 PMS 同时开始建仓, 就是两套系统都在买。查法见交接说明。- 关 ENTRY_GATE 是往决策系统
.env写ENTRY_GATE_ENABLED=False, 这是新增 env 值,必须docker compose up -d --force-recreate,restart不重读 env。 - 口径三(信号票在择时上放宽一档)本轮没做,要碰「不追高、接受买不上」那条纪律, 等积累一段实证再谈。
2026-08-06 · 新建仓动作:两侧代码写完,容器内全量单测通过,待实机部署
做了什么 按 V3 方案把两侧代码写完了。容器里拼了一份可运行的副本跑全量单测:ALL SUITES PASS, 451 例(新增的第十二批 26 例 + 既有 425 例,含装配自检 58 例)。尚未在实机部署。
动了哪些文件
决策系统(bionic_trader)一个文件,48 增 4 删,三处全在 if direction == "PMS_JUDGE": 块内:
workers/tasks_intraday.py:_task_by_action加 OPEN 建仓判据(含必答「把这只票挑出来的 驱动到今天还成不成立」);OPEN 且现价缺失时块内早退回UNAVAILABLE(加仓类有摊薄成本可依, 建仓没有现价锚就是盲判);边界声明按动作分岔(原句里的「配额」对新建仓不成立)。
持仓系统(tradingSystem)九个文件:
app/core/action_engine.py:加A_OPEN并进JUDGE_ACTIONS;新增eval_open与scan_open。既有scan()一个字未动。app/services/proposal_service.py:新增_scan_open(候选池取数、批量取行业、 真实资金封顶);_route_one加新建仓特判与档位分支;研判时间预算。app/services/judge.py:OPEN_JUDGE_KEYS键白名单,新建仓只送定性材料。app/services/exec_advisor.py:存observed的支撑压力区间;_check_ref_drift首答锁定、 偏离即停。app/services/executor.py:等待分支看到ref_drift落一条账本 WARN,一天一条。app/repo/pms_repo.py:新增只读judge_rejected_today与opened_names_today。app/services/param_store.py:四个新参数的说明、校验、FAIL_CLOSED。config/settings.py:四个新参数;PMS_JUDGE_ACTIONS默认值加OPEN。scripts/test_batch12_units.py(新,26 例)与scripts/run_tests.py(登记新批次)。
写代码过程中改掉的一条设计(单测逼出来的)
原方案的取数口径抄的是命令驱动那条路:want = min(单股目标, 剩余额度)。单测发现它会在
可投金额只剩一万时开出一只 0.5% 的零头仓位——拿一个持仓名额换一个永远补不到目标的半截仓,
还挡住了后面真正建得起来的票。改成「钱不够一整只就不开」,并把与命令那条路的差别写在注释里:
命令是用户明确下了「投这么多」,最后一只缩水是命令的收尾;自主建仓没有这层意思。
部署方式
- 决策系统:不加配置、不动
.env→git pull后docker compose restart backend-api worker-brain。 - 持仓系统:源码打进镜像 →
make deploy,跑完make test要见 ALL SUITES PASS。 新建仓档位默认就是 full(自动执行),make deploy一跑完下一个整分钟的心跳就可能真建仓, 所以务必收盘后部署,让第一次真实运行发生在次日开盘、人在场的时候。
真机判收
未判收。判收步骤见 NEW_POSITION_ACTION_PLAN.md 第十节,顺序是决策系统先上并手工发一条
action=OPEN 的研判请求,确认理由是在谈从零建仓而不是加仓、且请求体里没有仓位数字。
还欠着什么
- 实机部署与八步判收。
- 第一周要盯的四个数:每天有几只候选落在买入区间内、研判驳回率、
intraday_exec一跳的 耗时分布、评审账本每天的行数。 - 未夹带的一条:给
industry.get_many的gp_stock_category分支加按日缓存 (每跳省几十次库查询,但改的是既有函数的时序行为,单独提、单独拍板)。 - 文档补账仍欠着:
WS_INTEGRATION_STATUS.md停在 07-30;BIONIC_PMS_INTERFACE.md要补新建仓这一路(研判闸第五类动作、硬数字裁剪、择时侧零改动)。