添加交易逻辑
This commit is contained in:
parent
bc3af5f411
commit
c5adcde53f
|
|
@ -0,0 +1,220 @@
|
|||
# PMS 策略自动挂载方案(个股打法状态机)· V2 定稿
|
||||
|
||||
> 日期:2026-08-25。V1 当日经两轮问答修订为本版。读第一到第九节即可,附录都标了「可不读」。
|
||||
>
|
||||
> **拍板记录(2026-08-25,用户,两轮)**:
|
||||
> ① 吸筹标志只认「明确吸筹」这一档,潜在吸筹不触发。
|
||||
> ② 建仓资格不放宽,仍然只从候选池合格票里选(候选宁缺毋滥的纪律不动)。
|
||||
> ③ 自动化直接上全自动档:系统算出来就挂,挂上就跑,不设人工确认期。
|
||||
> ④ 实现骨架采用「个股打法状态机」,V1 即开四条边,**含接力切换(网格换止盈)且默认开启**。
|
||||
> ⑤ 判分闭环进 V1(留痕补齐 + 月度只读统计脚本)。
|
||||
>
|
||||
> **随方案生效的四条小口径**(V1 交付时列出,两轮问答无异议,其中第四条由优先级讨论补出):
|
||||
> 双命中(吸筹加高热)取跟踪止盈;负安全垫不挂跟踪止盈;每日新挂上限默认两条;
|
||||
> 人工撤下的自动策略,同票同规则冷却十个交易日不再自动挂。
|
||||
>
|
||||
> 纪律沿袭:只做加法,不改既有执行器、闸门、动作引擎的任何一行;重大行为变化先在本方案说破。
|
||||
|
||||
---
|
||||
|
||||
## 一、这件事为最终目标买什么
|
||||
|
||||
现在三种交易方案(做T、网格、跟踪止盈)都要你逐股手动挂。哪只票该用哪种打法,这个判断每天靠人。本方案把这个判断变成系统的例行动作:**决策系统判断某只票主力在明确吸筹,它适合网格;某只票热度冲高,它需要跟踪止盈保护利润;网格票突破区间且热度起来,打法自动接力。系统自己看出来、自己挂上、自己切换,全程留痕,你只在页面上看结果。**
|
||||
|
||||
它为总目标买的东西是:把「用什么打法」这层判断从人身上卸下来,让持仓系统离无人值守更近一步;再配上判分闭环,让这层判断的好坏从第一个月起就有数据可查。
|
||||
|
||||
**输出级判收(实机,全部可查)**:
|
||||
|
||||
1. 某个交易日,一只持仓票的吸筹定性是「明确吸筹」,当天上午系统自动给它挂出一条网格策略,策略备注写明依据,网格随后按档位正常买卖。
|
||||
2. 一只持仓票热度分超过阈值且有浮盈,系统自动给它挂出跟踪止盈,创新高抬线、回落触发卖出。
|
||||
3. 一只自动网格票放量站上区间上界且热度超阈值,系统撤下网格、换挂跟踪止盈,账本留一条接力记录。
|
||||
4. 候选池合格票里带明确吸筹标志的,自主建仓时排在前面;建成底仓后的下一个扫描日,网格自动接管它。
|
||||
5. 关掉总开关后一条都不再自动挂;关掉接力开关后只挂不换;已挂的不受影响。
|
||||
6. 你手动撤下一条自动策略,十个交易日内系统不会把它挂回来。
|
||||
7. 每一次「挂了、换了、想挂没挂成」都能在评审账本查到原因;满两周后判分脚本能报出止盈保住的回吐与网格赚的差价。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状、数据源与口径同源声明(以代码为准)
|
||||
|
||||
**已经有的**:三种策略的自动机、挂载前校验、暂停恢复撤下、清仓清场、买入暂停表、宏观闸和全部熔断,这一整套都在跑。策略挂上之后的执行链完全不用动。缺的只有一层:从「信号」到「挂载」之间没有桥,今天这座桥是你的手。
|
||||
|
||||
**两个信号的数据源(已查实,不需要任何上游系统配合)**:
|
||||
|
||||
| 信号 | 存放位置 | 口径 |
|
||||
|---|---|---|
|
||||
| 吸筹定性 | 153 代理的 `strategy_daily_results` 表,`raw_logic_json` 列里的 `fund_flow` 字段 | 决策系统每晚产出。含评分 score、定性 state(字符串,以「明确吸筹 / 潜在吸筹 / 无吸筹迹象 / 高位派发 / 信号不明」五档之一开头)、位置标签 pos_tag。8 月 24 日实测分布:明确 192、潜在 278、不明 82、派发 8 |
|
||||
| 热度分 | 153 代理的 `stock_fund_heat_scores` 表 | 生态热度表,0 到 1 的分数,每天收盘后更新(T+1),一天可能多个批次,取最新交易日的最大批次。代码写法是前缀式(SZ002625) |
|
||||
|
||||
**口径同源声明(回答「数据底座已有吸筹功能,要不要重复开发」)**:算吸筹的只有决策系统一家,数据底座和持仓系统都只是读者,谁都不重算。数据底座那份读出来给人看(明确加潜在都算有标记,存它自己的库);持仓系统这份读出来触发交易(只认明确档),开发量是一个几十行的读取函数,与它今天读支撑压力位同表同路。不走数据底座的接口,是为了不给交易系统新增一条运行时依赖。两家共享的契约是决策系统的五档定性词表:PMS 读到不认识的词一律当无标志处理(宁可不挂),探测脚本会把 PMS 的读数与数据底座前端清单的分布对读一遍,两个读者对不上就说明契约漂了,要去问决策系统。
|
||||
|
||||
**一个说破的风险**:候选池默认只收强传导档、前三十名,明确吸筹的一百九十二只票和这个小集合的重合可能很小甚至为零。资格不放宽是拍板结论,照此设计;探测脚本会把真实重合率量出来。如果实测为零,「吸筹建仓」这半边空转,只剩持仓票挂网格这半边生效,届时拿读数再议,现在不预设。
|
||||
|
||||
---
|
||||
|
||||
## 三、骨架:个股打法状态机
|
||||
|
||||
每只持仓票在任一时刻处于一个阶段,每个阶段有它该用的打法。阶段由三样东西合成判定:决策系统的吸筹定性、热度分、价格与支撑压力区间的相对位置。
|
||||
|
||||
| 阶段 | 判定要点 | 该用的打法 |
|
||||
|---|---|---|
|
||||
| 吸筹震荡期 | 定性为明确吸筹,价格在区间内 | 网格:跌档买、涨档卖,吃震荡摊成本 |
|
||||
| 启动拉升期 | 热度超阈值,有浮盈;或网格票放量越上界 | 跟踪止盈:创新高抬线,回落触发卖 |
|
||||
| 高位派发期 | 定性转高位派发 | 停买收紧:暂停买入腿,卖出照常 |
|
||||
| 深亏修复期 | 安全垫为负 | 什么都不挂,把位置留给补仓评估与清弱票 |
|
||||
| 中性 | 以上都不是 | 不动,留给动作引擎四类自主动作 |
|
||||
|
||||
状态机的「边」就是自动动作。**V1 开四条边**,做成注册表结构,以后加边不加层:
|
||||
|
||||
| 边 | 从哪到哪 | 动作 | 详见 |
|
||||
|---|---|---|---|
|
||||
| 边一 | 中性到吸筹震荡 | 挂网格 | 第四节 |
|
||||
| 边二 | 中性到启动拉升 | 挂跟踪止盈 | 第五节 |
|
||||
| 边三 | 吸筹震荡到高位派发 | 暂停网格买入腿 | 第四节末 |
|
||||
| 边四(接力) | 吸筹震荡到启动拉升 | 撤网格、换挂跟踪止盈 | 第六节 |
|
||||
|
||||
扫描器每交易日 09:40 跑一次(行情和参考位都已就位),对每只持仓票判阶段、走边。每条边的动作先过既有挂载校验,过了就挂(全自动档),挂载动作写进评审账本和策略备注;没过的也留痕。**挂载和切换本身不下单**,真正的买卖仍由策略运行器每分钟评估、每一笔过规则闸和全部熔断,这条链一个字不改。
|
||||
|
||||
---
|
||||
|
||||
## 四、边一与边三:明确吸筹挂网格,转派发停买腿
|
||||
|
||||
**道理**:明确吸筹意味着主力在低位反复收集筹码,股价大概率区间震荡。网格正是吃震荡的工具,和吸筹期的走势形态天然匹配。
|
||||
|
||||
**边一,对已持仓的票**:同时满足以下条件就挂网格。持仓大于零;没有挂着任何策略(生效中或暂停中都算有);吸筹定性是明确吸筹且结论日龄不超过三个交易日;不被冻结、不在黑名单(这两种票网格买腿必被闸拦,挂了只剩半条腿);没有未走完的建仓命令或在途指令(有就缓到下一个扫描日);不在人工撤下或接力切换的冷却期内。
|
||||
|
||||
**对没持仓的票**:不新开建仓入口。吸筹标志只做一件事:候选池合格票里带明确吸筹的,在自主建仓排序里插队靠前(盘中转多信号仍排最前)。建仓照旧走完整链路:规则闸、研判闸、择时区间一道不少。底仓建成后它就是「已持仓的票」,下一个扫描日由边一自动补挂网格。「吸筹网格建仓」由此拆成两个既有机制的接力,没有任何新的下单路径。
|
||||
|
||||
**网格参数自动生成**(原则在此,公式在附录二):区间优先锚定昨夜支撑压力,拿不到就退回围绕现价的百分比带;档距默认百分之二;最大投入默认取该股离单股上限余量的一半;每档一手起。默认值全进参数页。
|
||||
|
||||
**边三,吸筹转向**:定性转成高位派发,或标志消失且超过日龄,就暂停该票网格的买入腿(复用既有买入暂停表,来源标 accum),页面可见,卖出与已买档位照常。定性回到明确吸筹时自动解除,但只解除来源是 accum 的暂停,不碰风控信号触发的那种。不自动清仓,不自动撤策略。
|
||||
|
||||
---
|
||||
|
||||
## 五、边二:高热度挂跟踪止盈
|
||||
|
||||
**道理**:热度冲高的持仓票涨得快也可能跌得快,跟踪止盈是纯离场保护,涨得越高保得越多。
|
||||
|
||||
**触发条件**:持仓大于零;没有挂着任何策略;热度分不低于阈值(初值 0.80,探测脚本出分布后校准定稿);**安全垫为正**。最后这条单独解释:负垫票挂上止盈永远不会武装,却会把该票从动作引擎排除,深亏补仓的评估被白白挡掉,所以亏损票不挂,只给赚钱的票配保护。
|
||||
|
||||
**参数默认值**:启动线沿用厚垫线(浮盈百分之三开始跟踪);回撤触发默认从高点回落百分之五;触发时卖出可卖量的一半,剩余继续跟踪;不设硬止盈目标。都进参数页。
|
||||
|
||||
**热度回落不自动撤**:恰恰是退潮时最需要保护。撤下只有三条路:触发全清后由清场机制撤、你手动撤、清仓命令覆盖。
|
||||
|
||||
**双命中口径**:一只票同时明确吸筹又高热度,挂跟踪止盈不挂网格。与全系统「减持永远比增持畅通」同向:保住利润优先于做波段。
|
||||
|
||||
---
|
||||
|
||||
## 六、边四:接力切换(网格换跟踪止盈,默认开启)
|
||||
|
||||
**道理**:吸筹震荡有走完的一天。价格放量站上网格上界、热度同时起来,说明这只票从「收集期」进了「拉升期」,继续按网格逢涨卖会把趋势腿卖飞,该换成跟踪止盈吃趋势。这是状态机最值钱也最激进的一条边,按拍板 V1 默认开启,配独立开关随时可关。
|
||||
|
||||
**触发条件(09:40 扫描时判,全部同时成立)**:该票挂着**自动挂载的**网格(人工挂的不动);现价已站上网格区间上界;热度分不低于阈值(与边二同一个);安全垫为正;该策略没有在途未终态的委托(有就缓到下一个扫描日,与策略层「上一笔走完才动」同一纪律)。
|
||||
|
||||
**动作**:撤下网格(状态置撤销,备注写接力原因),已买的档位作为持仓保留,立即挂上跟踪止盈,高水位从切换时点的当日高点起算,账本落一条接力留痕(前后策略号都在)。
|
||||
|
||||
**防抖三条**:每票每天最多切换一次;切换后该票的网格边进入冷却(默认十个交易日),防止区间上沿来回震把两种策略翻来覆去换;接力挂出的止盈与普通止盈完全同规格,撤下路径也相同。
|
||||
|
||||
---
|
||||
|
||||
## 七、秩序与安全:自动挂的策略就是普通策略
|
||||
|
||||
总纲:人最大,停机开关其次,用户命令再次,紧急风控可以插队,策略排第六,自主提议排最末;减持永远比增持畅通。自动挂载只改变「谁做挂载这个动作」,不给策略任何新地位。七个具体情形,前六个全是既有行为的自动继承:
|
||||
|
||||
一,清仓类命令下达的瞬间即时撤该票策略、驳回买入提议、撤在途买单,然后紧急直通卖出。自动挂的网格同样被撤,命令至上。
|
||||
|
||||
二,决策系统对策略票发出够门槛的卖出信号:不自动清仓,落提议进「等我拍板」,同时暂停该票策略买入腿,卖出照常。你采纳等于撤策略并清仓。风控不被网格挡住,网格也不被风控直接掀翻,裁决在你。
|
||||
|
||||
三,吸筹定性转派发(本方案的边三)与上一条完全同向:停买不停卖。决策系统对策略票的影响,不论走风控信号还是定性转向,都收敛到这一个动作上。
|
||||
|
||||
四,降仓命令不掐策略腿(8 月 20 日拍板):网格照跑;降仓卖掉一部分持仓后,网格档位账与真实持仓自动对齐,不会拿已卖掉的档卖空气。
|
||||
|
||||
五,各路增持闸门照拦:组合刹车拦网格买腿(网格是自主口径)、全局暂停买入拦一切买入、宏观偏热闸拦策略买开腿;跟踪止盈只卖,全不受影响;做T规则照旧。
|
||||
|
||||
六,止盈全清后持仓归零,清场机制撤策略;之后重新建仓且热度仍超阈值,次日会挂一条新的止盈。新持仓配新保护,说破备查。
|
||||
|
||||
七,唯一的新交互规矩(小口径第四条):你手动撤下一条自动策略,同票同规则冷却十个交易日不再自动挂,杜绝人机拉锯;冷却期内想用就手动挂,人工挂载永远不受冷却限制,也永远不会被系统替换。
|
||||
|
||||
安全阀汇总:总开关读不到参数表按关(fail-closed);每条边各自开关;每日新挂上限默认两条(防部署首日一口气挂满);收盘后部署,第一轮真实扫描发生在次日 09:40 人在场的时候。行为变化说破:被挂上策略的票,动作引擎四类自主动作让位(既有语义),网格接管逢跌买入、止盈接管离场保护,负垫不挂正是为了不误挡补仓评估。吸筹所在结论表盘中会被补扫改写(有实证),扫描每天只在 09:40 读一次判一次,不追盘中改写。
|
||||
|
||||
---
|
||||
|
||||
## 八、判分闭环(进 V1)
|
||||
|
||||
沿用本系统「拒了的后来涨了多少」的判分文化,两件事:
|
||||
|
||||
**留痕补齐(随边动作落)**:每次挂载、接力、想挂没挂成(含每日上限溢出的「没轮上」名单),都在评审账本落行,硬数字里存当时的依据快照:吸筹定性与评分、热度分、现价、生成的策略参数。这份快照是事后对照的锚,缺了它判分无从谈起。
|
||||
|
||||
**月度只读统计脚本**(`scripts/report_strategy_score.py`,步骤二一并交付,满两周后首跑才有意义):三张对照表。第一张,跟踪止盈每次触发保住了多少回吐(触发卖价对比其后十个交易日最低价与最新价)。第二张,网格赚了多少差价(已实现批次盈亏加做T利润口径,对比同期死拿不动)。第三张,对照组:明确吸筹但没轮上挂、热度超阈但被条件挡下的票,事后表现如何。输出写成人话报告,热度阈值、回撤比例、投入比例此后按月用它调。
|
||||
|
||||
---
|
||||
|
||||
## 九、分步落地与验收
|
||||
|
||||
**步骤一:只读探测(脚本已随本方案交付)。** `scripts/probe_strategy_signals.py`,只读不写。打出五组读数:吸筹五档的最新日分布(与数据底座前端清单对读);每只持仓票的定性、日龄、热度分、按 V1 规则的预判去向;候选池合格票与明确吸筹的重合清单;热度分的分位数与超阈值持仓票;两个信号源的新鲜度。
|
||||
|
||||
在桥机 factorevaluation 上运行(新文件要先重建镜像,重建不影响正在运行的容器):
|
||||
|
||||
```bash
|
||||
git pull
|
||||
make build
|
||||
docker compose run --rm --no-deps pms-web python scripts/probe_strategy_signals.py
|
||||
```
|
||||
|
||||
预期读数:五档计数与 8 月 24 日分布同量级;持仓票逐只有定性或标注无结论;重合清单打出具体只数。把输出发我,热度阈值和重合率结论用它定。
|
||||
|
||||
**步骤二:状态机上线(两到三天)。** 信号读取、状态机四条边、参数、调度位、试算端点、判分留痕、单测第十七批。收盘后部署:
|
||||
|
||||
```bash
|
||||
make deploy
|
||||
make test
|
||||
```
|
||||
|
||||
预期读数:ALL SUITES PASS,总例数在 519 基础上增加。部署当晚先手动试算一轮(只算不挂):
|
||||
|
||||
```bash
|
||||
curl -s -X POST 'http://127.0.0.1:38100/api/ops/strategy-attach-scan?dry_run=true' | python3 -m json.tool --no-ensure-ascii
|
||||
```
|
||||
|
||||
预期读数:返回里能看到每只持仓票的阶段判定、将走哪条边、想挂被什么挡下。次日 09:40 第一轮真实扫描后:
|
||||
|
||||
```bash
|
||||
curl -s 'http://127.0.0.1:38100/api/strategies' | python3 -m json.tool --no-ensure-ascii
|
||||
make t-gate
|
||||
```
|
||||
|
||||
预期读数:新策略备注以「自动挂载」开头并写明依据;评审账本有 ATTACH 行;若发生接力另有 HANDOFF 行。
|
||||
|
||||
**步骤三:一周回看与首月判分。** 看四个数:自动挂了几条、网格成交与档位推进质量、止盈与接力触发是否合理、有没有误挂。据此校准阈值与带宽;满两周跑一次判分脚本,此后每月一次。
|
||||
|
||||
---
|
||||
|
||||
## 附录一:新增参数清单(可不读)
|
||||
|
||||
| 参数 | 初值 | 说明 |
|
||||
|---|---|---|
|
||||
| PMS_AUTO_STRATEGY_ENABLED | True | 总开关,读不到参数表按关(fail-closed) |
|
||||
| PMS_AUTO_STRATEGY_RULES | accum_grid,heat_trail,accum_exit,handoff | 启用的边清单,注册表键名 |
|
||||
| PMS_AUTO_STRATEGY_DAILY_MAX | 2 | 每日新挂上限(接力切换不占此额度,它是换不是增) |
|
||||
| PMS_AUTO_ACCUM_STALE_TDAYS | 3 | 吸筹结论日龄上限(交易日,自然日乘二宽松换算) |
|
||||
| PMS_AUTO_GRID_BAND | 0.08 | 支撑压力缺失时网格上下界的百分比带 |
|
||||
| PMS_AUTO_GRID_STEP_PCT | 0.02 | 网格档距 |
|
||||
| PMS_AUTO_GRID_CAP_RATIO | 0.5 | 网格最大投入占单股上限余量的比例 |
|
||||
| PMS_AUTO_HEAT_TH | 0.80 | 热度阈值(探测后校准定稿,边二与边四共用) |
|
||||
| PMS_AUTO_TRAIL_GIVEBACK | 0.05 | 跟踪止盈回撤触发比例 |
|
||||
| PMS_AUTO_TRAIL_SELL_RATIO | 0.5 | 触发时卖出可卖量的比例 |
|
||||
| PMS_AUTO_HANDOFF_ENABLED | True | 接力切换开关(拍板默认开,页面可关) |
|
||||
| PMS_AUTO_HANDOFF_COOLDOWN_TDAYS | 10 | 接力后同票网格边冷却(交易日) |
|
||||
| PMS_AUTO_OPTOUT_COOLDOWN_TDAYS | 10 | 人工撤下后同票同规则冷却(交易日) |
|
||||
|
||||
启动线沿用既有 PMS_CUSHION_SOLID,不新设。
|
||||
|
||||
## 附录二:网格参数生成公式(可不读)
|
||||
|
||||
参照价 P 取 09:40 实时价,无实时价当日不挂。支撑 S、压力 R 取持仓行的参考位(盘前已由既有链路取好)。上界取 R 乘 1.01(要求 R 高于 P),否则取 P 乘(1 加带宽);下界取 S 乘 0.99(要求 S 低于 P 且为正),否则取 P 乘(1 减带宽)。中枢取 P。最大投入等于单股上限余量乘投入比例。每档股数等于最大投入除以下方档数再除以 P,向下取整到一手,最低一手。任何一步算出的区间不满足「零 小于 下界 小于 中枢 小于 上界」就当日放弃并留痕,不硬凑。
|
||||
|
||||
## 附录三:信号读法与工程落点(可不读)
|
||||
|
||||
吸筹:按持仓与候选的代码集转前缀式,单表查 `strategy_daily_results` 近三十天窗口内每票最新一行,解析 `raw_logic_json` 的 `fund_flow`,state 前缀匹配五档词表,未知词当无标志。热度:先查 `stock_fund_heat_scores` 最新交易日与最大批次,再按代码集取分数。两个查询都走既有 153 代理连接与单表守卫,各自失败只让对应的边本轮不动作并留痕,不拖垮扫描。冷却与每日计数用独立运行参数承载(与买入暂停表同一手法),不进策略 state。
|
||||
|
||||
新文件三个:信号读取与阶段判定(app/services/strategy_advisor.py)、探测脚本(已交付)、判分脚本。修改点:settings 与 param_store 加参数并把总开关列入 fail-closed、scheduler 加 09:40 调度位 strategy_attach_scan、web 加试算端点、run_tests 登记第十七批。策略表与挂载校验零改动。测试第十七批覆盖:定性解析五档与未知词、日龄判废、阶段判定矩阵、四条边的命中与排除、双命中取止盈、负垫不挂、接力条件与防抖、两种冷却、区间公式边界、每日上限、幂等、留痕断言。
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
策略自动挂载 · 步骤一只读探测 (STRATEGY_AUTO_ATTACH_PLAN.md 第九节)
|
||||
====================================================================
|
||||
只读, 不写任何表, 随时可跑。回答五个问题:
|
||||
|
||||
1. 吸筹定性的最新日分布 (与数据底座前端清单对读, 口径同源自证)
|
||||
2. 每只持仓票: 定性 / 结论日龄 / 热度分 / 按 V1 规则的预判去向
|
||||
3. 候选池合格票与「明确吸筹」的重合率 (拍板②的实测依据)
|
||||
4. 热度分的分位数与超阈值持仓票 (热度阈值 PMS_AUTO_HEAT_TH 的标定依据)
|
||||
5. 两个信号源的新鲜度
|
||||
|
||||
运行 (桥机 factorevaluation, 新文件要先重建镜像):
|
||||
docker compose run --rm --no-deps pms-web python scripts/probe_strategy_signals.py
|
||||
|
||||
数据源 (都经 153 代理, 严格单表):
|
||||
strategy_daily_results.raw_logic_json 的 fund_flow{score,state,pos_tag} —— 吸筹定性
|
||||
stock_fund_heat_scores(trade_date, batch_no, stock_code, score) —— 热度分
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.db.session import fetch_all, fetch_one # noqa: E402
|
||||
from app.repo import pms_repo # noqa: E402
|
||||
from app.repo.downstream_repo import to_dot, to_prefix # noqa: E402
|
||||
from app.services import param_store # noqa: E402
|
||||
|
||||
# 决策系统定性词表 (契约; 见方案「口径同源声明」)。不在表里的词一律当无标志。
|
||||
FIVE_STATES = ("明确吸筹", "潜在吸筹", "无吸筹迹象", "高位派发", "信号不明")
|
||||
ACCUM_WINDOW_DAYS = 45 # 每票取近 45 自然日内最新一条 (方案口径是 30 天, 取宽探测)
|
||||
|
||||
|
||||
def _ymd_int(v):
|
||||
"""trade_date 可能是 int / str / date, 统一成 int YYYYMMDD; 解析不了返回 None。"""
|
||||
if v is None:
|
||||
return None
|
||||
if hasattr(v, "strftime"):
|
||||
return int(v.strftime("%Y%m%d"))
|
||||
s = str(v).strip().replace("-", "")[:8]
|
||||
return int(s) if s.isdigit() and len(s) == 8 else None
|
||||
|
||||
|
||||
def _age_days(ymd):
|
||||
if not ymd:
|
||||
return None
|
||||
try:
|
||||
d = datetime.strptime(str(ymd), "%Y%m%d").date()
|
||||
return (datetime.now().date() - d).days
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _fund_flow(raw):
|
||||
"""raw_logic_json → fund_flow dict 或 None。脏 JSON 不炸。"""
|
||||
try:
|
||||
d = raw if isinstance(raw, dict) else json.loads(raw or "{}")
|
||||
ff = d.get("fund_flow")
|
||||
return ff if isinstance(ff, dict) and ff.get("state") else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _classify(state):
|
||||
s = str(state or "").strip()
|
||||
for k in FIVE_STATES:
|
||||
if s.startswith(k):
|
||||
return k
|
||||
return "词表外" if s else "无字段"
|
||||
|
||||
|
||||
def _variants(dot_codes):
|
||||
"""{每种写法: 点式} 的反查表 (前缀式 / 点式 / 纯数字)。"""
|
||||
rev = {}
|
||||
for c in dot_codes:
|
||||
d = to_dot(c)
|
||||
if not d:
|
||||
continue
|
||||
rev[d] = d
|
||||
rev[to_prefix(d)] = d
|
||||
rev[d.split(".")[0]] = d
|
||||
return rev
|
||||
|
||||
|
||||
def _in_clause(values, prefix, params):
|
||||
keys = []
|
||||
for i, v in enumerate(values):
|
||||
keys.append(f":{prefix}{i}")
|
||||
params[f"{prefix}{i}"] = v
|
||||
return ", ".join(keys)
|
||||
|
||||
|
||||
# ================================================================ 取数
|
||||
def latest_accum_day():
|
||||
r = fetch_one("SELECT MAX(trade_date) AS td FROM strategy_daily_results")
|
||||
return (r or {}).get("td")
|
||||
|
||||
|
||||
def accum_distribution(td):
|
||||
"""最新一日的定性分布 (全池)。"""
|
||||
rows = fetch_all("SELECT stock_code, raw_logic_json FROM strategy_daily_results "
|
||||
"WHERE trade_date = :td", {"td": td})
|
||||
dist, total = {}, 0
|
||||
for r in rows:
|
||||
total += 1
|
||||
ff = _fund_flow(r.get("raw_logic_json"))
|
||||
dist[_classify((ff or {}).get("state"))] = dist.get(_classify((ff or {}).get("state")), 0) + 1
|
||||
return total, dist
|
||||
|
||||
|
||||
def accum_of(dot_codes):
|
||||
"""{点式: {state, cls, score, ymd, age}} —— 窗口内每票最新一条。"""
|
||||
if not dot_codes:
|
||||
return {}
|
||||
rev = _variants(dot_codes)
|
||||
since = int((datetime.now().date() - timedelta(days=ACCUM_WINDOW_DAYS)).strftime("%Y%m%d"))
|
||||
p = {"since": since}
|
||||
sql = ("SELECT stock_code, trade_date, raw_logic_json FROM strategy_daily_results "
|
||||
f"WHERE trade_date >= :since AND stock_code IN ({_in_clause(list(rev), 'c', p)})")
|
||||
best = {}
|
||||
for r in fetch_all(sql, p):
|
||||
dot = rev.get(str(r.get("stock_code") or "").strip())
|
||||
ymd = _ymd_int(r.get("trade_date"))
|
||||
if not dot or not ymd:
|
||||
continue
|
||||
if dot in best and best[dot]["ymd"] >= ymd:
|
||||
continue
|
||||
ff = _fund_flow(r.get("raw_logic_json"))
|
||||
best[dot] = {"ymd": ymd, "age": _age_days(ymd),
|
||||
"state": (ff or {}).get("state"), "cls": _classify((ff or {}).get("state")),
|
||||
"score": (ff or {}).get("score"), "pos_tag": (ff or {}).get("pos_tag")}
|
||||
return best
|
||||
|
||||
|
||||
def heat_snapshot():
|
||||
"""(交易日, 批次, {点式或原码: score}, 全市场分数列表)。"""
|
||||
r = fetch_one("SELECT MAX(trade_date) AS td FROM stock_fund_heat_scores")
|
||||
td = (r or {}).get("td")
|
||||
if td is None:
|
||||
return None, None, {}, []
|
||||
r2 = fetch_one("SELECT MAX(batch_no) AS b FROM stock_fund_heat_scores "
|
||||
"WHERE trade_date = :td", {"td": td})
|
||||
b = (r2 or {}).get("b")
|
||||
rows = fetch_all("SELECT stock_code, score FROM stock_fund_heat_scores "
|
||||
"WHERE trade_date = :td AND batch_no = :b LIMIT 20000",
|
||||
{"td": td, "b": b})
|
||||
scores, all_scores = {}, []
|
||||
for x in rows:
|
||||
try:
|
||||
v = float(x.get("score"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
scores[str(x.get("stock_code") or "").strip()] = v
|
||||
all_scores.append(v)
|
||||
return td, b, scores, sorted(all_scores)
|
||||
|
||||
|
||||
def _pct(sorted_vals, q):
|
||||
if not sorted_vals:
|
||||
return None
|
||||
i = min(len(sorted_vals) - 1, max(0, int(round(q * (len(sorted_vals) - 1)))))
|
||||
return sorted_vals[i]
|
||||
|
||||
|
||||
# ================================================================ 主流程
|
||||
def main():
|
||||
heat_th = param_store.get_float("PMS_AUTO_HEAT_TH", 0.80)
|
||||
print("=" * 66)
|
||||
print("策略自动挂载 · 只读探测 %s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
print("(只读, 不写任何表; 各段独立, 一段失败不影响其余)")
|
||||
print("=" * 66)
|
||||
|
||||
# ---- 一、吸筹定性最新日分布 ----
|
||||
print("\n【一】吸筹定性最新日分布 (与数据底座前端清单对读)")
|
||||
accum_td = None
|
||||
try:
|
||||
accum_td = latest_accum_day()
|
||||
if accum_td is None:
|
||||
print(" 结论表里一行都没有 —— 先确认决策系统夜间链在跑")
|
||||
else:
|
||||
total, dist = accum_distribution(accum_td)
|
||||
age = _age_days(_ymd_int(accum_td))
|
||||
print(f" 最新结论日 {accum_td} (距今 {age} 个自然日), 当日共 {total} 只")
|
||||
for k in list(FIVE_STATES) + ["词表外", "无字段"]:
|
||||
if dist.get(k):
|
||||
print(f" {k:<6} {dist[k]} 只")
|
||||
if dist.get("词表外"):
|
||||
print(" ⚠ 出现词表外定性 —— 五档契约可能漂了, 需与决策系统核对")
|
||||
print(" 对读: 8 月 24 日前端清单读数为 明确192 / 潜在278 / 不明82 / 派发8, "
|
||||
"同量级即两个读者一致")
|
||||
except Exception as e:
|
||||
print(f" ✗ 本段失败: {type(e).__name__}: {e}")
|
||||
|
||||
# ---- 二、热度分布 ----
|
||||
print(f"\n【二】热度分布 (阈值初值 {heat_th}, 用本段读数校准)")
|
||||
heat_map, heat_rev = {}, {}
|
||||
try:
|
||||
htd, hb, heat_map, all_scores = heat_snapshot()
|
||||
if htd is None:
|
||||
print(" 热度表里一行都没有")
|
||||
else:
|
||||
print(f" 最新交易日 {htd} 批次 {hb}, 覆盖 {len(all_scores)} 只")
|
||||
print(f" 分位数: 一半位 {_pct(all_scores, 0.50)} · 八成位 {_pct(all_scores, 0.80)}"
|
||||
f" · 九成位 {_pct(all_scores, 0.90)} · 最高 {all_scores[-1] if all_scores else '—'}")
|
||||
n_over = sum(1 for v in all_scores if v >= heat_th)
|
||||
print(f" 全市场不低于 {heat_th} 的共 {n_over} 只")
|
||||
except Exception as e:
|
||||
print(f" ✗ 本段失败: {type(e).__name__}: {e}")
|
||||
|
||||
# ---- 三、持仓票逐只 ----
|
||||
print("\n【三】持仓票逐只 (定性 / 日龄 / 热度 / 按 V1 规则的预判去向)")
|
||||
held_dots = []
|
||||
try:
|
||||
positions = pms_repo.list_positions(only_open=True)
|
||||
held_dots = [p["ts_code"] for p in positions]
|
||||
if not positions:
|
||||
print(" 当前无持仓 —— 边一边二暂无对象, 只剩建仓插队那半边")
|
||||
else:
|
||||
acc = accum_of(held_dots)
|
||||
heat_rev = _variants(held_dots)
|
||||
held_heat = {}
|
||||
for raw, v in heat_map.items():
|
||||
dot = heat_rev.get(raw)
|
||||
if dot:
|
||||
held_heat[dot] = v
|
||||
try:
|
||||
strat_codes = {s.get("ts_code") for s in
|
||||
pms_repo.list_strategies(statuses=["ACTIVE", "PAUSED"], limit=500)}
|
||||
except Exception:
|
||||
strat_codes = set()
|
||||
stale = param_store.get_int("PMS_AUTO_ACCUM_STALE_TDAYS", 3)
|
||||
print(f" {'代码':<10} {'定性':<8} {'结论日':<9} {'热度':<6} {'垫':<7} 预判")
|
||||
for p in positions:
|
||||
c = p["ts_code"]
|
||||
a = acc.get(c) or {}
|
||||
cls = a.get("cls") or "无结论"
|
||||
ymd = a.get("ymd") or "—"
|
||||
hv = held_heat.get(c)
|
||||
cushion = p.get("cushion_pct")
|
||||
cu = ("%.1f%%" % (float(cushion) * 100)) if cushion is not None else "未知"
|
||||
fresh = a.get("age") is not None and a["age"] <= stale * 2
|
||||
hot = hv is not None and hv >= heat_th
|
||||
pos_ok = cushion is not None and float(cushion) > 0
|
||||
if c in strat_codes:
|
||||
verdict = "已挂策略, 自动挂载不碰它"
|
||||
elif hot and pos_ok:
|
||||
verdict = ("双命中取止盈" if (cls == "明确吸筹" and fresh) else "挂跟踪止盈")
|
||||
elif cls == "明确吸筹" and fresh:
|
||||
verdict = "挂网格" + ("" if (p.get("frozen_reason") or "NONE") == "NONE"
|
||||
else " (但该股被冻结, 实扫会跳过)")
|
||||
elif cls == "高位派发":
|
||||
verdict = "若挂着网格则停买入腿"
|
||||
elif hot and not pos_ok:
|
||||
verdict = "热度够但垫不正, 不挂止盈 (护补仓评估)"
|
||||
elif cls == "明确吸筹":
|
||||
verdict = "吸筹结论超日龄, 视为无标志"
|
||||
else:
|
||||
verdict = "不动"
|
||||
print(f" {c:<10} {cls:<8} {str(ymd):<9} "
|
||||
f"{('%.3f' % hv) if hv is not None else '—':<6} {cu:<7} {verdict}")
|
||||
print(" 注: 预判未含黑名单与在途指令两道排除, 以实盘扫描为准")
|
||||
except Exception as e:
|
||||
print(f" ✗ 本段失败: {type(e).__name__}: {e}")
|
||||
|
||||
# ---- 四、候选池与明确吸筹的重合 ----
|
||||
print("\n【四】候选池合格票与吸筹的重合 (拍板②「资格不放宽」的实测依据)")
|
||||
try:
|
||||
from app.services import command_service, plan_feed
|
||||
try:
|
||||
black = command_service.blacklist()
|
||||
except Exception:
|
||||
black = set()
|
||||
sel = plan_feed.candidates(held=set(held_dots), black=black)
|
||||
cands = [x["ts_code"] for x in (sel.get("items") or [])]
|
||||
print(f" 候选池合格票 {len(cands)} 只 (计划日 {sel.get('date')}, "
|
||||
f"考察 {sel.get('considered')} 只)")
|
||||
if cands:
|
||||
acc_c = accum_of(cands)
|
||||
clear = [(c, acc_c[c]) for c in cands if acc_c.get(c, {}).get("cls") == "明确吸筹"]
|
||||
maybe = [c for c in cands if acc_c.get(c, {}).get("cls") == "潜在吸筹"]
|
||||
none_ = [c for c in cands if c not in acc_c]
|
||||
print(f" 其中 明确吸筹 {len(clear)} 只 · 潜在吸筹 {len(maybe)} 只 · "
|
||||
f"无结论 {len(none_)} 只")
|
||||
for c, a in clear:
|
||||
print(f" 明确: {c} 结论日 {a.get('ymd')} 评分 {a.get('score')}")
|
||||
if not clear:
|
||||
print(" ⚠ 重合为零 —— 吸筹建仓插队这半边今天空转, 只剩持仓票挂网格那半边。"
|
||||
"多观察几天, 持续为零再议资格")
|
||||
except Exception as e:
|
||||
print(f" ✗ 本段失败 (计划接口没起或已过期都会走到这里): {type(e).__name__}: {e}")
|
||||
|
||||
# ---- 五、新鲜度小结 ----
|
||||
print("\n【五】信号源新鲜度")
|
||||
try:
|
||||
a_age = _age_days(_ymd_int(accum_td)) if accum_td is not None else None
|
||||
print(f" 吸筹结论: 最新 {accum_td} "
|
||||
f"({'距今 %s 天' % a_age if a_age is not None else '取不到'})"
|
||||
f"{' ⚠ 超过 6 个自然日, 按方案口径会被判过期' if (a_age or 0) > 6 else ''}")
|
||||
except Exception as e:
|
||||
print(f" ✗ {e}")
|
||||
print("\n探测结束。请把整段输出发回, 用【二】定热度阈值、用【四】看资格结论。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue