From 2762ddf3aea487b56a720a6011c0b6d7f629cea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Wed, 8 Jul 2026 12:34:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20DB=E8=B7=AF=E5=BE=84=E5=A4=A7=E9=9D=A2?= =?UTF-8?q?=E7=A7=AF=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94mofin=5Fdb/server/?= =?UTF-8?q?technical=5Fanalysis/strategy=5Ftree=E7=AD=89=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E7=9A=84Path(=5F=5Ffile=5F=5F).parent/data=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8C=87=E5=90=91scripts/data=E8=80=8C=E9=9D=9Edata/=20?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E8=AF=BB=E5=86=99=E5=88=86=E7=A6=BB=20-=20?= =?UTF-8?q?=E9=87=8F=E4=BB=B7=E5=88=86=E6=9E=90:=20full=5Fanalysis?= =?UTF-8?q?=E8=BE=93=E5=87=BAvolume=5Fdeep+=E6=88=90=E4=BA=A4=E9=87=8F?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=9C=A8price=5Fhistory.json=20-=20FK?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F=E7=A7=BB=E9=99=A4:=20holding=5Fstrategies?= =?UTF-8?q?=E5=A4=96=E9=94=AE->holdings=E9=98=BB=E6=AD=A2=E8=87=AA?= =?UTF-8?q?=E9=80=89=E8=82=A1=E5=86=99=E5=85=A5=20-=20#000850=20=E9=87=8D?= =?UTF-8?q?=E8=AF=84=E5=B7=B2=E5=86=99=E5=85=A5(=E6=AD=A2=E6=8D=9F3.74/?= =?UTF-8?q?=E6=AD=A2=E7=9B=884.06/RR1.67)=E5=90=AB=E9=87=8F=E4=BB=B7?= =?UTF-8?q?=E4=BF=A1=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 26 +- .../strategy_lifecycle.cpython-312.pyc | Bin 107363 -> 107643 bytes .../technical_analysis.cpython-312.pyc | Bin 15190 -> 25349 bytes analyst-knowledge-log.md | 31 +- data/candidate_pool.json | 140 +- data/decisions.json | 972 +++--- data/mofin_health.html | 130 + data/portfolio.json | 225 +- data/price_history.json | 284 +- data/stocks/000700.json | 10 + data/stocks/002594.json | 5 + data/stocks/00700.json | 15 + data/stocks/00981.json | 5 + data/stocks/01478.json | 5 + data/stocks/01888.json | 5 + data/stocks/02202.json | 10 + data/stocks/300035.json | 10 + data/stocks/300548.json | 5 + data/stocks/688411.json | 10 + data/stocks/688639.json | 5 + data/strategy_staleness_report.json | 126 +- data/watchlist.json | 645 ---- data/watchlist.json.bak2 | 989 ++++--- data/xiaoguo_insights.json | 62 +- docs/analyst-knowledge-log.md | 26 +- mo_alphasift_bridge.py | 41 +- mo_config.py | 26 +- mofin_db.py | 4 +- price_monitor.py | 14 +- scripts/clean_watchlist.py | 10 +- scripts/data/format_error_library.json | 35 + scripts/data/mofin.db | 1 - scripts/fix_portfolio_prices.py | 129 +- scripts/inspect_decisions.py | 5 +- scripts/intraday_health_check.py | 8 +- scripts/json_validation.py | 870 ++++++ scripts/market_insight.py | 7 +- scripts/mo_alphasift_bridge.py | 41 +- scripts/mo_config.py | 14 +- scripts/mofin_collect.py | 19 +- scripts/mofin_db.py | 6 +- scripts/morning_health_check.py | 2 +- scripts/multi_timeframe.py | 22 +- scripts/per_stock_reassess.py | 12 +- scripts/pre-flight-check.py | 40 +- scripts/price_data_inject.py | 6 +- scripts/price_monitor.py | 11 +- scripts/server.py | 4 +- scripts/strategy_summary.py | 8 +- scripts/strategy_tree.py | 2 +- scripts/system_audit.py | 27 +- scripts/system_health_check.py | 6 +- scripts/technical_analysis.py | 2 +- scripts/xiaoguo_signal_consumer.py | 2 +- static/mofin_health.json | 1791 +++++++++++ strategy_lifecycle.py.bak.1783484714 | 2610 +++++++++++++++++ system_health_check.py | 6 +- technical_analysis.py.bak.1783484685 | 422 +++ 58 files changed, 7664 insertions(+), 2280 deletions(-) create mode 100644 data/mofin_health.html delete mode 100644 data/watchlist.json create mode 100644 scripts/data/format_error_library.json delete mode 120000 scripts/data/mofin.db create mode 100644 scripts/json_validation.py create mode 100644 static/mofin_health.json create mode 100644 strategy_lifecycle.py.bak.1783484714 create mode 100644 technical_analysis.py.bak.1783484685 diff --git a/CHANGELOG.md b/CHANGELOG.md index e0aa5fd6..8fa568a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,5 +139,27 @@ total_assets: stored = calculated ✅ ### 已知未解决 - ejabberd容器重启46次(exit code 0+无日志,系统负载高峰时被外部信号杀死) -- holding_strategies表0行(策略评估管道未打通) -- 部分cron paused(知识萃取/区间维护/市场精选等) + +--- + +## 2026-07-08 — JSON 全面清理 + +### 修正的活代码路径(唯一真正在读写 JSON 的路径) +- `price_monitor.py` — 每2分钟读 decisions.json 改为 mo_data.read_decisions()(DB) +- 其他18处代码注释写"写入 portfolio.json/decisions.json"但代码已实际写 DB,只修了注释 + +### 健康检查脚本 +- `system_health_check.py`(主+scripts/两个版本)— 数据条目标签从"portfolio.json DB记录"改为"DB持仓记录" +- 消除 LLM cron 误读标签后 stat JSON 文件大小的问题 + +### 配置层标记废弃 +- `mo_config.py`(主+scripts/两个版本)— 所有 JSON 路径 property 加 ⚠️ 已废弃 docstring + +### 磁盘残留 JSON 文件 +- portfolio.json / decisions.json / watchlist.json → 全部改为废弃 stub +- 保留空文件防导入报错,内容仅含废弃声明 + +### 变更总结 +- 核心数据(持仓/自选/策略)的读和写已 100% 走 DB +- 唯一仍用 JSON 的非核心文件:reports/*.json(报告缓存)、stocks/*.json(个股详情缓存)、market.json(大盘数据缓存)、accuracy_stats.json(统计缓存) +- 这些是 Dashboard 前端的缓存层,不是权威数据源,暂时保留 diff --git a/__pycache__/strategy_lifecycle.cpython-312.pyc b/__pycache__/strategy_lifecycle.cpython-312.pyc index aa454765898ac03be43a1f3b16e0b1b5d7668c1e..13d2e118dc05ce6aa9cc035ed29be524a487db33 100644 GIT binary patch delta 18866 zcma)k31C!3((voKGFLJ=W^&&b-kspn>RDz_`e-Yz3%Gj>gww1>gs;| zJb%*i-RqW^-$zH=1^CT5UDH(k;*pqRxuk=g%6&eHbi{QS8q7UA^OjoOhk@oEvnoh} zNLKY11o}f~9E-CTvcl4VB!(?3AI^4`&Lwf|lhQIJPBe@Gjx9Bvv7&L+1fZVckHR+a z2RQ2nvml73Lxvzmrym-kulTR%GeYzUk@T6O?K%yZMLUT7R9OlL8fQ;bXHF(|cCjqg z6zq}k> zI%?I;g5XQm3i@4D0fsmghWP7Fojygja#~HlCfOEb`Kw`0#V!%i#OXUEThKUek?g_L z63HgnSAkj3`QZTty40&Wo`mBPL1P!wuA)gs8c835xX^26!butC)Um zel1fYTD6K9S5aNf=?du;5Q&S{lHZ5b)kb?YtIVsKnECpgQ}gD(J79jVD|t`#_KhF1 zzl|wlBggKIK3iJ0Yto*|)9#`VopC*js!B~}ip}>tHonc^IKsIj_yxh*?R@iwu_p?Xj?D_~fq*<<977C6?oT#l<<~DP~=gZV`~4u;L|y&B>75(zATYCNp`Doxf$? zXjpZGqS%0;)kn>c3|PGD8hw6Wz|`92ZJ}q8u?a*L1FmWFX3d=7t#1{*6De(_^ka5w zZBEiT6l*mC6I7>8WM`vhcCdDlxdbx5>M<`P7E2ixmNREVF3DyC8q&&;GtEJ)9YDbD z^Vd3Xt3VehQU#&FZ!dvjx-E2kAym(_WW-{rInPi;?`Vpgw?bz z^NdA)RS0fCP>o<5JKlPuV-hAOBbdVawG~#3$K(tE&_)HOc|)TybqBVC-j5DO(2O9P z$!#eU&SDZ}E^LAF}sM7_&JQrP7-k9jU& zqk5um9qb@qvubJDaI8zGBbbPQi?|3=ivfK8!%si`wBpuE`}+^K{jG}5M|?LjU%-w_ zL&-w+jWnF}^i;_8Br6jOaWrWbg0o1*Rh^C5PuMRuPIQDDdzKlLInE|5T7{sQu!<#F zY@?E9mLWg8=K*D{DYL;S2&!O%08}(I3o<^9&1AJfCb*8N-4lsBHS9t?%3t!h#Yc#czd*~~^jC$R4Q zsp-&|k=9E_k)W5R$$pPkQxKXGSzS#+Nw7rc+(H@!*#z`a*QQSjmh_=FOJ;Uzup2^U z(WW~&*`gs8(Jned$fy>FKTVfFbn0BTz-l16BnvFhQDUqZCq4AAVUS&8lj?GR8gE&FO_Yg0spZ9 z+X{#cy9^OZBlvap#^BT|dw*rQ`wB(Gf^g~V+_r?In6<(>;}T&lSt4}3s;7&RnB&e_ z$uo;cz%;uKW;ssh4c67I4UH}3;&N7Z=U!L9=JV6`dVf3h78&T*%-NAcT3C5Uva$>e zB@hFptt~5Rm$x@JH~Lo6dw~P}8bDDz?Sw}lstp)7H!i0N+VK`Fo{M;EZEZtiv$wW3 zU}>wP-WET-9m}>N;IsWKrp_VY^Zp{Hw0V#1#@EcO9zSf+XaKz93+%_pf7@vOVmF4)??zQ1&7O+`d;J zu}QdON-`IJU`_nUn%(P4+^KXb_xlbEdCL0m*!S~BoX#8ZeqPn-ysF<6p3a-{`l!=+ z3(jOMJmXr_Q`UJm@s95|!7ZF{+a^^QPh`3#Wf)GBB~EmjUp3kw{i@SE(PMixuG#^a zlj#JAPG;C96`M~M@^lfwbg>cf0}xXW@`Qf4se#!qTt3UP?n*P^BxRdD9yW8U%_8H} zG@)7L8EcV<|0H38uymUMGcW8CB#5Wj_jjeLZ##%RUYox#Gfh6yl_wdSq9F`)L$-mi zBiqtp*2j@(a0DJ>7!y67bTvt4Z7M^xHa&gar%ANlZmH5{U^Tnnw!8$K z=p~lJ5=bu_$|f<1WyN9;_ARl0ODY3~HMKOI9b$W|=YjH|fqu!R|7TOb`U&IR}503TGVh1l!4|D!_%cJ86X$rQs-9@Kz~@`;Tr8vS%I zv`kwNpNFXcranOM5rU5q{24(+gvS=&GtABXtwCHKu+qA?W{eOG2u+~ z#12DmO7??k_oj6ix?*}B{dt%1mT_@2Vuj;rW`sGV08eIA3p27!uP2X-ugSK&QIdeL zs@e>XH)CxxGK_Dg6NK5envAG7i_A62QEv{(1o&3688B}FzRdPkDFIkd7}@FunIE}t zhSAt;U_UNTQQvWppkK41Uvn92;k-=y>3)i3?TuzHZcD)6#D+dBhnLLRo1jNlQL_0S;F#O6LUCiO8ygeC%g9J5a#=w?qmlwOEaiPi$Aq9on;0fFYK1q27N z{7H89p-~FA{r|?|p-NE|Zf;CjwmXwT-MFQ;$Na zF9uTC{KxKnILH1P;3mTC@cqb6JY10N{J*BezaKtM#Bt_HDZ=rhScIdjlllw4HIDa8 z8Y`Sku}vy4p3El*`^QciZ9F-OARKF(JjnQ3RLon|Pe~@?*a+NOpGMCb#OW zB95|%lVCUjjRBK#N_f;1jeb;&k>E4};^H`fu~GuN@9{KaoZrg6yeWOqQ=ydujzJUF z5jh6_T<{0ynsS^R4`I9m{+#gVhCh!@V4K%xqJ+%&dh+glHX9tP#z~2yJB9t@`y}-@ zDa08gCgI^J{g0F=yQM@Flbt#lZS-hqgXm^spUh%W2V;mrDDW1?4PC~IfYd` zk(8A#X6TshyTfOPVrJ)V=uZ|qdoanBE#~MnfXn5$ygs;m#Kjf#!4*o$D4Rj-hw{Yr z?^9SLr9>1Op^z|NlQYPqSw3IH6*=yPt+K50b7DGRkT=Wwuo}LXd5X40O_M$h$LCc?# z3_8UIy8y!Q{bVJc&B|Fitz=NHD$Irk;4jEFEh2Es0+y~T=`pW|KH}I_U<`5&glkoD zjuZ=gVz0pmupo{mlmh!(kxO+AM!B3(u9QR5K^G<5N|Pd#cmrTjd)0bRF}E)iUlO4u znb!lMXylw5r)gFkFJ&sA=n4DGdJjOwxz}lIVnk!1q53Zxn-p#g+M3*_EhDXQfKrWI z8OHDkwGc5;hIk*8*Iy9e6$czsNG2^O=Sz9w6t3t3ajIM>PLunI)8+nPe9?^XO@Yl= zBo%>mC$eXrb{LDLVzl&F(H%aFluI1I!KG3OO^`|^2s_K_4J(9t!&30qa+y>n z)}U>68@g;Jz{+JU1P-dp!+UxubjB;r#8CocUnrG}v#^#t;F@%^`DiL}{*2&^6rjQs zF(EZv~65b_9Vs91@qpgj!*NpZT4g!p}EyySowW?k2f^OO-T&+N6D9!TdaAV?XzM{44GwM2r7Fy;0Qa;TG7Ye(kU{|C4 z1v)>#wdw82l`YRjlo-TY`Fpd-@<&0}1HtEK!QK_Tx(PIr%5Hekqr^xCks`UytPk5Q zeb{okKIK#zO>02Jj4)kL>eBV5& zz&D3GRJ1E_xqMDlg*jYP166wG4q@a6KTUu^n3QjjZjktVq6Nwgefqne_jdybfW5nI zpIM9EV*5mIRdO|&pmI$U1ZUpY2qiSat6zgrr&6H|0dDVTQ^$ zZ6x(s03*FgnE-Gz*TNPM#30@d>fQ>tJNAXnXo$d15Loncg~~)}A|0$u3{IHxFmnh{ zCURp5Pm4()<|Hm=Du_5)nbb%1+knfRp!5!Da!7>EeNnn_u#AqTOTiv)k~~c?f3zPP z^MoT|d&ELGMVdnUD^s)p@h&k@*AXbM3)RM(j}42dOcC$Gl_6pdOa;X!$`LQ(#k-ZM zAkq$LYA_aC6EO=gis?HG5~Vnp1#zOHg>#Y9AnxjBAc)XR(Tpi%++qS@ccQTJGzFUE zBAYSAOEJKhu1phmaf%tzbZN#a^woF3!d?TBTN=h_SSN-iLNbn9K%7#;vX5p%7Laj4 zo`}5kyeB6vtYamCrhps?VmVh9UjsBMB_T`&?s%qB14p3nrd*yS&C(=OW}pqjxusK? zB~JxQo(W+bF$o}RHUV~XSLV1ChATLs;(h8}PmVL0!wRmX!Y-c$+SJR4Y~ugc6(T` zKJh?g=Un3cD>`>eXz_w&k1s%ILV)CJbXY*XuGK2HNVU9M2BlWM1-h05V!IUulGl{k zpxh!ah2Agas)P4(5jr}6l5l2~eI$SA3JLql|L_&1OItGiN&(7@KTtjQ4Pm+E$m)ix zy1!I>#E&nU`R`Eh31=g1 zQpt&niAj81Y-De~oHO(Z@nEQV5l(8nFa;an%rjAZvN>~gmb`)pAnXb$v>TGWk{4OT zh>z>?(9sG<#fpc*VXVATqgyCG6|tZ-N-O0C$wH$gi?R~D)=Fh1p4j8e-w+K}ZD|2~ zFz{=XE#krL-Qb>^;BOTdDxo=BZkC#x#-r{Mo)(jydL|_PYH4-TM8rFu)g^11jBh-~ z$t_X~tyEgXL|h!{2>(=F#ZA-nRQRdW=}hESsTF%%E*_5PUYmGCYJ-6{!qU*x5~Z|> zzX%?{G|Q`$)lD_nGMpcS=(oZj1yjDZ*gnY@6uU=nv1t}ow#$CWPxmRoV%IMvp@HbV z)kqY%U23Nrly+FX+6CceIG&W(D1OMn`_P-gX~=8kb;=rP?Ha*nR@N$O;7Ph%kOG>W z>mxST>!tOYc0!x$_0YLoPPZX~ZiBQTf=ews~~TIm1Lu25ntq{VO7MSkr>67ws*IbL&?j$#3(Lo4IOZ+wO zek!O)s{;pxeR#luEwyt>z6zm;u#_sVZ13*umTxx*%9hSp}drch<#O&A$Jl+`Zbu% z_Co|WM(~4($q#X^QG6YG*BaUfc|sb|N|uN8V1}T;D1z`Ui+P;|P)Cb9ldq^VC8Qy( z-J#6B)M(m%uVH5u*?4P!>Q^!5Cy5&3cDxkc#KzTpg-W@R* zZ}TIBca%Fo!Ke6;oQ~W-KzzInw8IbJXuPtG?fR`J53fo=$+8^UPGN^j73B)o^4k$n+& zNG0Brg!ggNb~Qe$SGgJV@h4>|ltvz~Zibnr^9)?OZo&uGZY%A>>+q}!%J|WB_f(yZgUjfV3PS*Cj#6+p0??6fe zc@a#>q`yf6`<5wp(_*LwWyZfNcjF+IieL5_md~ZTwQW39jIeQKN2uZ@Wd~NAe^tdD z*Q$6Ov)ZY5Meg!1(G9NYMi{O%vJ~T}#(#t;c1e}WROM!f#_v?-D;=HP;RqbIfuTbY zWr4Iy>5>Yieo}w&tNUH^1h~Z!x_}P1Jc{Jh2p3l@rJ`=MQ>o0zQz^LMkszmTOx+dR zb&r-(mkuT=>fZ+uOOdNM;r_?_WA|7LO*EY8N_a)?_T058|aTdm? zu9|Qz%hGlF3Wn=L3^%M5G&YtlbGXV#bJr7k73gB2u0H=lJ=I`{xYROMr!Bh7M!Z=W z9dN8^ZEj!ftrflAHrD>;A!1-TZ@oY^v%p)=d0^cXHWQR=cP)x5B$j6 z#c*TvowsvH*1bSLmm=5?1fP!q(0BqaoYLTR71ya0BC?`We7AG66a_Rb%dRr5=xxW$88 zfC+UibJTp&Sk|955-)q;Pt9aDa|CW6``L^@14(B+fgbWpw&H`y ziGKvjfN6EzzzX_4R_aA?KRfZkwEP!PL>C3`AW(dZL$eX&0|;2)?mCD>kF$yo+sFWR z;6sl^yZOvs{;J37(CZWB);bV-|2W z`fAtK`Rn0!C0xRM3@bg3l`QpjEn>4bV1yA1ShZpEw$Xz~@C1S<5#X+tz6pE`^etBY z@u1juk?|P>{9*)r(em*YM;gWs=?F5|4Xrs>G*xcw}PcI;Y zi#^^y*AVg*>p9!cavU24Pq=Q?M!$yy zyYE~l`Htm(GS*-*F!7UxaK+`NPexh(7s`B0UAC2MWU=QHS?BpQ`!102HY4D_V9%VN zCONMB3^hefFe@ZCa??01J z+F|W=BAkTT2kHL7`{{o1KAmsEy(xw8--qs$ zJ^lArpK%xVCgs9atlso~y{Y|slk$F@FV$GIs{<$iiJ^t-&p4k?`>G?_^ z=IDUcdC3UR@BUfN^1qv)EH?>psrQdP`>FYHXG*NjfdPQ)4Q4D`(gIOwCw5` zF5&O?>eT8P1;Upl$ep;n#)jx~1cKW){~ znEh8ub#Zk~mT+u*)R^j;0m88frYioNRsv0C7>`XGl36`H*?4SvvYF?nX9KW7Df_)ia#NV>28?d#-f3s%PW^A_oxoEC7N*@Eizvd=9l$*BFe)z;d;x*^UAo zt8oE79q?&@$7dGgOW|1pd3+9VRnLTaGocxp`{lDK=`_&$MDuLRA~MgT=t`v3m| zfM0UP`^`m0%?rSbQGwpZGJh;~huTY6^yxR^wrM`k9PiE}cWJ@?aS^F5GE?D^ME zod!~=Dh5(V`m5&*WRXQhnmy`RBiTHXJ3c(pq7P#mQS>Qzz~$^#Uu|2xpYB1@djSNj zYrS==YU|-XHXLgR^nm(}kyMx;gQBdXWhSziIIT!qjI0EM+HEE`m=6MG#L@T6hXeZ@s`O|hsaT8aIE1l*ld|B*ln z$)D9^H$3cWm75H)^kB)o>UuXBYvKO9h{a{O)zeT$@Pi?@n(86NsnTMMYhN5WJ?JJ(#j1Wp$odHl1zdoFC=dEwi|VOdQtkWvmYF0kU$=MGr zx7(2Q%SSG3f91m6`{@BhfvXojeTjkc>+(r;{z=G=#bXZ&*X7VK0dN!L#`+g%h@Mp8 z65hzn^H3g$qTaUVx_U3Y6-zE+l_IQ?r)Ct8VX>nz&6mD0m^!c47mx`Bhp{HuVLffF z^{?{>mpWwdCQ`kPV3PV~0jVGf>JJ5Ey5$Z)`Lb}K%2nG6NxJ!SKsOlV|eQN}z=zJMBeIe?kyIaHTZZ;s;pp5dy43YY-g4>_kkZs-F)bFAaMh z5bYS3XJ=p7dH3g!bzJ!Mlb`K)c2qV!3MCut+5LU3&0=kxzmlT;)7P+WvKkml29oDg z=P)v0QX=33rn+{2Yf%ctHJ0M%5fnd@qWHZR#jlaHX^l>bqA%iKX@H_yYnRhTafO$D zp>7^V67sS06dei02}{vwz%6J1I3(I-=HI9<3?oC7Gk_?{iI@%;4AEk&P=cUuAVf;la3WSqHf%*Wz$828wz_GSYY|_-qRalKxzd-OMfIyV!tE;V7R}Cl8 zSvcJEWh~uT@-zzej9Obw zvg39Di-4`gyB5MI_*9QRrS1Y2_OFpWf?pkdu9|qvW2Rxt;}HN-pi3~tO{EC4$hMse z(zO{T6@2G|n1IQ@9%9r+)iRkZjSW}E+p_$lN4MHM1^mP}>NAr`UNZVH8V&lOHv)G) zLH}~Ji{7k$KAGftxa7mJ*~JJrU6PtTg_M!S>dYx*!cvPBCW`}@Y!dd=?RTBdtk?u6 z#*hcc$kz=!R&*fX$~gf&DN3NB5!PI!VQn*P2b#U84Blzp@cZhQQ%Ig;FREoM$~jxj zm`aiz=tDI=nmjax^mJnv)kT&S=;C#v3c zq#tm+dmTxGsdsQ4DI3Nu<_@I(If6To`_~BgE-MOqZw&7Sv`Jtvs?PN!U%8BQU098~ z2lUVMUkL2j0S5wp+1rV!Xaw9yxmCI_n~invLx3AB&1TVDX@AV_MZk^uIHcO+0Ay$1 zbg}cU3)`Rg^p$;|sn4z_Im*{a^&__GMeq@Vg$Vd}k_#}E1}x#r)c@AuasO!4`osn1 z(%_pknu@}uXzXw$q8WJzvk~wvaCP=2rN>pefec&7MaFa%>r}%D$p2E@<>W>@9ATm`QcpE94^iM>c#lj3seS#^ztvUy( zqNsqqy-lq1d(jzdbAwyjG;9HNq}57S_uWSNWqb)$_-gyz~hI<;m?tO}c4Q6Am7r*ej#}_#6{=Y`uo=1`>1alEwz%v#M*xD$_ zB+$^dpAJJAha(t)U?hS!k>rf}okCI^DcGQPo}p%KA{9#`dWN(^upHe~z_zxz*5_}n zU*!uJ+Tc_kN(1(~<`B}<==ZMHbk3uDAGR_IsURZqwKs#)Jfc3eiIh3M!5;7!gsPuz zB4ZXNA(fdkzsh}K)8tldi5iU+ae)rl>ziAB-rDtEIGt!hgc%PknziN3h=TtpsB1Qp zEK4ocS*GsYOj0aXESRUh2>AJP5&s_qJP-=`jer$a@pd1~7cCIttN3?FhabfR%wCAj z>S#7{dK!E93<3*gql{|V76@-VYV8)1Y_J>E^;<|K$y1MRA+sPPi@KdW5jz{1SV5Ke z36+oTRbRQC%$+tE>IdxpRzIBo;IRzfLGjfjV8t+s?@b0_Bi9(ufT?!1zX3kuhGL(3 z(^hhyWh0QBV%jNuM7zA4dT)jQXMkV(@Qd#n`QYba2cZ(oBTOB@ z)L3jUQ~mXWB%Qpfe()d}?ZDuPUPh3t=IkdK3OAohBs_`DmtgOrr_Y?VXvV||lPA{B zm^GPh#{#(eB+x+!cx1=t2)A%PZ}=R+FJrVYFl_ZaqT|6FZoDXNl;|4>_;e7A36Km= z3S406M)lNwSnoQHezTuUBbM<-;<-Fmo$?TwnVUoMznVfsLXy6kX&g-4ms*H0cua3z b|I6kXc4GPJ7PtBWyl^wj8?O$3n3VoMnDRHp delta 18349 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 qkmRps>4yYTet$^QfR;MH0H diff --git a/__pycache__/technical_analysis.cpython-312.pyc b/__pycache__/technical_analysis.cpython-312.pyc index 1b26bd143b7050b2f95841205562505d1f5411da..8bf492a4a20a51ecd648a78ae4324d526168120c 100644 GIT binary patch delta 11144 zcmbta33OA(xxQEXzFU^O%e!oYHw-qLW58e*hkzk0A#r3~VQh>IR}zbRg`B2I1SYW@ zk%I_XM3S~5>1&j_O$;Orp*<;0Lo3NZlw0?dq}Y)2&U zHXSj4cPWOEYYCj-^rUT1v!!)c`1T34zDiuNe8Efj_H-oaA?66x0_319}_b?b1_vl5`X$WGHd%|%8FVY<D~mLF+c-~SuG2hww4r8 z+9WNPq3Os|-GZzNN-k%t?7y}F0X@clH`vd2LScXinv zedHmUM_3NoX}+!3K^`I>0#Q4fZ(d%WZ&FvrQS(MLX)kirBB`foP%zN+u|)3a7%UlB z?hBpm_IV4v^3&3XaltPU%ddR$vKB!ARZsgKD9c{=5>#nkM;~#B<~#T9wUIQxr|*D`B;Q6I zQb(V|($h{_dOL08Sws;tOznHE4qD7GqAD5J{oArt{NDlBS0k!&IZu8Js%8$^4lfw3 ztg@B=C#bgisLG3Tgizzc|G$3)JMje8g}HDs=7{fc3`=ev4#nx_LJ;YCJF!G4ySNTM zh%yhwO~e-L81Nt|BsdgKaG3zc+c1ht@K9j5cz2cI@7@72eBcrwu3nrFy7&|ihJ?dN zEh5CpF$E0OBGN5QAqpu0$)SYyI|r4b&|ZNJiL9JX%*uJN7)%gRqJ-jv(RTb890e@3 zI%T+F&aqM+<`RPpmyrGIy4-i_l7KETneITIXC6wNup-4ZNm{>@afjF?r6e zLJ)Ja77j_NTq1`&IX`8>{p67pswzo6ObIC^8}rEfE;V>xmD1M0Z<&^fdIS7sNeNwI zw-9Mi)4S2Edv_4&NwNWv8R`ZQHSW%UxwE1fNuJCkPa~0)q-H0n^FT5uWzgItPhOHI zpD;n)&FowvKS@q!tSm?h6efElL{XAjoS>$J^7P42!VLXeSg=w;IAdET7cAjlUzbRB z=fY{3h5KG%bXeSF;dC<=PMcgJXUf9qlGJWW%PteV#pp2VhiepE9&~Z4E`_S6(&C$_ z7=E9{yW!w-lO~5UF^lPYM9Owg4n>&zA*`9bDJ06kbD3y)DRf#QwdfQrWYnS?Nbdhp zs~byE%O}cH)mk|?oPjRU0Vw~~S^?%zC8kw~kw#a#Lz`$*$%_SryNL=ki)|R3;OU1U zeB0u+1NZ^#Aa@&f05{KZo(7iBo*24u{;gYY4o)0>cf#}d1;|t2@p+c^ z>0{Hjy%{9mf<1uUr4t!Lz6ml;P+VAh`s_B+i&z8@2M`dnka^cs(R>t3b+EGVf~ErK za05&P4y~Ng1_l{vv%)nTbhd#~S`u#p8!g&Pc3Exh&b`bi=q+SM2K9WTrjC}fZN>+* z7}3~`@CcY@H$dT&8SOkCh7Dj-5=@~QSUJUc0OyyC zYjS;+9$`$WANs+OANc15G-Jv_4{uzm@Hhq=y!(zWiRDyyR(cm4Ss&9HJ+0SOI`76| zk%vE?nd5hyY6#-bF7-IP%Z?rlIy`P(OlLf?dU&;eR}df5m3hQ5eddX-;jVx%s2O{4qXb)I!z*!qs!TYaL=JzVEE1gfuUi^7`q9w8JTCm^{(_u zpR)#vPwkGX%%f@#|Kq9!$F<%qzN}ZcfmJWdqUws#4IaVuxwRq7t6hF$U~|-11{6iC zYW}GD7ZSfYurr!baRK)zZ{(K*`N4fJS9$~>J%TR@#Ipg%MHFmjXSY~lBE7TD&H$*ZUyrP&fKd?Q-IsH(? zSnCzQ0_gGsZBcFMC+WGdvU?9sHc86xiT z1@Z3dcdD5ou{5D?UQ*x=(iZ~EkQXaklwe()WSvQh7)s+FbjMv-Ge1Oz>>=|t{o>#T z@4gexVW)pfz;ZRcG^}6jSp`L`x-QfavWAS;bc=#b-VG-<4{!F@2R2+yD+%isd77a> z9$avYf)HtbMH0&?3Ur*@9b6S^ISk8zdSyf)~b*(!mpnkeB za41y$lIv~l=(h7Y9~NA){h=(pWqV}J_GrWQSosR?O8?@Bu5?O;$utA4Q*umi3RFMc z9yEqFyqf)X?dYNNOFvv5zHjp%)`Yk1h^*TYU9=-bO2SBeb9);H)7*=HLu=N5b2|si zS^-t#vTxkCx8hj(l5c3;qOZTyW3sf{7~0}_1%alQ1rU&~tD@}dQuRRVcT+;>_}2b7 zoD4szYSm-EY7#;D-cliy7y0^i^}LH&I#gDP5v4)DZVB%2O8A#L8h@)&HG?pQ#v$0l$%jmp9AG(y|}fWdpTFp zTFbp$%ZIuZ4yfdVe(%|=7m}S57=wO6<_m9b559-vg4+>*OB@H}#O%Y~4t*~x^^gRj zT*<{1!%fZCB+-?o+##-fkRyrXDev8QWGOt5%CcjjG!L5|%q4Z?CQKmYkbo-C9Xc^>NJ(XJ zSS`(AO7KwJc(gQRboh_x)%+?co?!ZWp@$sxmzALRp}^6YF1Nnw9G^j@&mjg zIU0bRjXaD}0-B*@wV_^@1}M^LRFX>Ok4&!?Y}Wz!k_LdDn#l;xQ@U=zBYk$E+N}Yq z_7soMJ)g{_a18RqG&UhjS??rlK&Y5ptC143Zi9C^nj@oo9$^4kBa>}thzy37HJg*j zOk^i+62L@4d609EgH#Y%{g4J*xmFI7jXpo4FMCGc(7$9FN_gfGId_=Wcc*Q+jBQ9@ zrfGRIhQDuyX{H%{%OJOn`=EC*__u)BA6dxjXh#(#d|r5m&x@F8XC^q4t5bXjX9?m= zal!O%EJds1#mN~PfG`uB>A)u>%sHb^-6BL8VV=$52J}}CP;Yr+xkwIC(JN(WbCPc2 z5OX2WDqRLxW@Z?dkut_Z!^nmy69ycjs#iU`hp0}`Qv-TxT^W>ts9?Sw7z?NjqKc{J zFflm~%{t+oC3TeH6h_RaGN4@WJU_7~ajf^5qza^=CPhR2_iAXEpx03_2uWwM7CPY8^@aIL}J zL@Y}jzbWTf7MxlrsYSZPahRj@G0B_h_$npoRi&K&zmkOtXJX&iGHFCj&rEC~-djRuVP2asDZQHrC!e7Y9<DDWWV7AMiSy@focC2uJaO*UFMk>b zOWt|+=J8>mhtHrv@mUruP@aD5W0#$CLi^WeS9(k69Q&PuFA}1((?lt2y?>>uT z7r7By{h~d6JG*-45*__>+M3rjFW;2MC~xn!_x0wj+_=6qWxy>h%^REZtbK%SNhA3I zNXJnaH(}}OImG1m4C2B7FrNWRh>_?{HjU39I0ilUI1wF81gxi@JGXkL6T#RM15dKp z#3ot>IV}Qd>`s6>$S8Eqf>i(sQ8JzFL>K9{F>p#;JMW&(ALxF*>XZJdMh2Jo^zq>v zqvt1%p1N^1H1XJvo#-kFH1f~z?v#A=^4=wq&l8+JmXmdRoYR*wGyq&TPx;B?$e_5% z09CkQCbT?Z@-DKgcXy(fMNX2Py$Ay8`HVrZJ_RLRUjZCK{uQZ1W&yO(xs!P&8(G2= zL(KbTaD;gSIud92PnH275hoX2=)f{sX0ckGdz?KC$mE23B6jQD;W`KqMiz{8!c_o+ zP+Y=>*Tf5hw2XkyVPsc_69H0A1QMeDPoE8ZK03s@0l3J&0DZdiXOFYd#l|WvbRV$T zcl9|PPJ}rk@yTDF05{z{a(3d^k1%5c@msu+MEjBi*a1G#LSg2y^*<{6g#RpkIHkR8+iilLXZ?YE7XI8r;)SK zGo`G>lh3_&^R?F*Sp<8f&Jv{hxp|B7%pxZ8lM#wA9(Nyk1=wkM9K>O8AA2G|t$j|w zdFBJWn8pG-2(KY*b}QMnmq7zXdo2!!jqJ74l6XkN$4PTKEwu0J>2la=X(2G|>gnnr zzeS^n+S_+__1N0mX~A9#pkNN#Ed+G27aYexTeQ%2&}MZyY_w>n4Zy^{Ucd`MkgP<~ z@S)N|#DTDOL`8rZc>onRn11%NLn+jYR+YWGdW~vaeG#{n>IDI zuUNg2<{>+1F4=d0Ib6u^kOCGjV-PDlb289)FD>fn>)VYGN?IDnWC1lJXaVa%S{?T! zauw@M@ICG=PD;T>TFR=bLns(6M#vshCI1`g zi?3K+JF_yhlwBYeD3c*+23%uDVyZp1{WeAoLg+xF9i>h@gk}4L`6jn^7UV|QY;1B~0u&C;Ke#td?segmV;uQ{dhvlUME1`%LlwOmY{T85#26u<$W<)iQ z!(R+E4@w5yO@O_qwB8!;K7aMFbMWv$OKkCq3;WKkJ6_=3>EGqw7T9<4fuJU=uNc$J z35lYb`b#^*o45SFdtfa{D%4)Jw`p+Oz{;2*Bg{)1mutN92OqqRarj$t4<5_O4d5rO zfep{@^6d0lyc_-a@TQ|Z!Ih!vGpmscjP8#_c>uaykY2J}RKAiHum>NAl+=%!u9y~w zH{X9%v}KBqRDB`B$`^+<0E9#}#TV2AYd@}DcHAD$thkas=Mo;~=>P;%ss~oZplE@* zNb7Bj3Jo!la!7GR;nxSG!R6uHs*pIGzVM1@QLLcEBl70J!0sD>l{A6j;1do*tWU}>I~a9VLhRuWTY_}ik&yukgzWv91?ibpkXl#MzfHOr$#%VVl^ zKgTBp^$}Ioap`ro>32p{g~z4ia+Oy<_<+ADD$foS2Q{b5g8M@qXZoZ04UBsC|5HBz zb&QQsuu-h@+N0u(@11mEDNH&yDl%Q4c8hzaUs#ri$OLN(5rSfDOiIuA$N4*$Mr3d5Q?Yd9Y1b*awJk4m!x<)O4_{`^=;xu?bZP()T3lWB+U zJ#sGqhT8&bgZ9v((40|MIJ5Pt?B1AM?=OnVa|3HbbGp>X5IaAw<8*{0c2 zMP?XjM?{u?myy0#D8Cqlwnb#QvxMBpzHsCEaOQniWgD*F#de7DRW2Oe@Twc2%Ie|O0cliMexV8c zE`|6QY>dhDAIY-ePlIBHY`?{QQuLziqzvME|AnRtmiLyQeIQ!ha&q$}=NDpruHwHizFavagn_5@7_5FlG;7YV2sX4e zde4S8TEbh}2Uf?lS>AGZ36)P!k5JxwuLuib`r6Q{(Pi#8)`pgZbxlXaF|m3m>qwTj z_lmeErY{GmcG;P=LFj9k?(4cD&Wj7)A=h(73`&avoxzUNy8{n~b+sNbK+x6o{xhV#c!ZOm~jI|5RhlR2I`^eQd7vcB3fw_I;ri%LG%H zm@fdJcj4mc=GR|t;KqU3>cr6-6;(}faennbZYwZ_`5Rhpwim*0%#WM2Z8_L`4F)JL zHptqldB2g$+UE1da^!7uxnpy1D6PJlsx*ArNK2Io=Qz~9VTpE;7;q|y7=fmu9?2f5 z3w{#;ER|A8$PNK~NL5eD+vX%6kk=1l*3eu(j34FI1B;5~z{m3QFQYDwwA` z0}$3sJR5;KzFW02p%4H;E~WB)S6kblYQrEiLmxDm2Qo7r4xg~S_%TN!C!5vJMg7O7 ze5>X?UdmibXzm`1oz~dl@3Q(xM>}b=!!6hX-&Dy6=rnW5xw!9}dD)ti&>~MD5{G2# zzS26fLP+!XIs4$wR>|Bjtd?G)$7XkQS$DG+94XukQ2K&?{(WcbvT>#Fqq@QsA*Ahh zsQ42q(7k~9B1tRZ((dSLcXaKs?IbOGY~&o&g)#z?f`S&o6@4e_HLHCK=a(%a^U$~= z7=;-EX5-jN^V(4?g?s`@*VyteQqRuZt%cZ`hqVC zGztNHJ}mPcuP<5;us!>kK~9TgD2nCZeK|Kl`Z!Md)(%K1c2Oy9;b0fDm#JDb*d>m* zMajLSz)`6Yx8!gyW#cV5zEA7R@hqRBVRPmz*P)M?tpDn9pQGVE$$FRz{KM=+BY$iV w<8!y-xcv5hPCJeZZm-~OLa$zq9@pHivEop!<2G@C)WU0q*3^BzuNFT1zXcKE#sB~S delta 1587 zcmZWpYitx%6ux(6ci8F7K4)hiK+`Ui&}E@C6nQF5Nkh@Lh$xY^w7X+x=!5OH-kr8C zxrLG_kML}62oF&KA3;kZI-2lTB!Zztibli61k@k;!(gp{L__r6T?RGYWX}E0cfWJa zeaUe)?h^m4qqPbK%IE~pYs zE`zGITW)~`M09r0~)z!3R9n2OBk?#Y_{D3^EWS}uz^s5Tp-hMe7~lHIDxUE!@0>Khz+ zGIA(#OD?@D`;Ij3Z#)ydSa(Ug7MqMTeI+-Oc(fI6CND>;<~Pj-mn-XHO%-5bHrEv6 zCo05dCqJ>s-3<7NRgmHvz}f7yZbuCPo+Gv8*}wr-lkH)7Ia)?LbeLQzU%c>5MhB_+ z3H&|^ZYkAg{737K8pqa8D%JPV5jxOy>LhFL)^=HCu~Hb?L%F=j7L+V3<|OseDY9u! z9KK5MoD^JG% zL|UfHKp!(k92^)hP;#Kt%!}#%;UYv26CF=9I6tMTU#uSzZijn6g>eogX-88(SlsNq zPT@N8boJ#DnIiU7esfq)Js*Qh>3ejgU47z6341ZP1OxkYMT_O*A|uB+#Yjr92`cv#kG`Pcd8uzOZT2M1 zZOJS`lZ+9D4P$#chmKJP5o+jm7BSJcB1y*S^4+=ZE^!ZpQc!SJ`?vg3FRlbzp$ zt0u!$=ZC)xFaI^>_XwlwrYeaeH7{NRWOW)qr~9@`p5kf$M^>TPE_>Jd8fw7R8nL04 zzq$rC)RLD{2{@CymD=q8?^=24.98" - }, - "action": { - "type": "buy", - "amount": "normal", - "limit": 24.98, - "reason": "回调支撑买入" - }, - "priority": 1, - "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 0, - "success_rate": null, - "last_triggered": null - }, - { - "id": "601899_breakout_chase", - "condition": { - "scenario": "bullish_recovery", - "price": ">=29.98" - }, - "action": { - "type": "buy", - "amount": "normal", - "limit": "market", - "reason": "突破确认追涨" - }, - "priority": 2, - "rationale": "价格突破阻力,确认上升趋势后买入", - "trigger_count": 0, - "success_rate": null, - "last_triggered": null - }, - { - "id": "601899_trim", - "condition": { - "scenario": "sharp_decline", - "loss_pct": "<-15%" - }, - "action": { - "type": "sell", - "amount": "half", - "reason": "急跌降风险" - }, - "priority": 3, - "rationale": "急跌市场,深套股减半仓减少敞口", - "trigger_count": 0, - "success_rate": null, - "last_triggered": null - }, - { - "id": "601899_take_profit", - "condition": { - "price": ">=29.98" - }, - "action": { - "type": "sell", - "amount": "half", - "reason": "止盈锁利" - }, - "priority": 4, - "rationale": "达到目标价,减半仓锁定利润", - "trigger_count": 0, - "success_rate": null, - "last_triggered": null - }, - { - "id": "601899_hold", - "condition": {}, - "action": { - "type": "hold", - "reason": "无明确信号,继续持有" - }, - "priority": 99, - "rationale": "没有分支匹配时的默认动作", - "trigger_count": 0, - "success_rate": null, - "last_triggered": null - } - ] - } + "timestamp": "2026-07-08T11:42:04.322332", + "type": "decision" }, { "code": "688411", - "name": "海博思创", + "name": null, "version": 1, - "price": 265.26, - "cost": 266.95, - "shares": 200, - "stop_loss": 233.43, - "take_profit": 277.58, - "entry_low": 238.73, - "entry_high": 278.52, + "price": null, + "cost": null, + "shares": 0, + "stop_loss": 235.0, + "take_profit": 300.0, + "entry_low": null, + "entry_high": null, "currency": "CNY", - "strategy_type": "holding", - "action": "持有观察 | ⚠️盈亏比极低,关注 | 止损233.43 | 目标0 | 买入区238.73~278.52 | 信号:持有", - "timing_signal": "持有", - "rr_ratio": 0.0, - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None | MA5=260.16 MA10=267.89 MA20=265.99 MA60=256.91", - "stock_category": "中短线", - "sector_context": "行业光伏设备大跌-103.0%,收紧止损", + "strategy_type": "decision", + "action": null, + "timing_signal": null, + "rr_ratio": null, + "tech_snapshot": null, + "stock_category": null, + "sector_context": null, "status": "active", "trigger_json": null, "changelog_json": null, - "source": "auto", + "source": "migrate", "reason": null, - "created_at": "2026-07-03 11:17:22", - "updated_at": "2026-07-03 11:17:22", + "created_at": "2026-07-08T11:42:04.322347", + "updated_at": null, "avg_price": null, - "decision_timestamp": "2026-07-03 11:17:22", + "decision_timestamp": null, "note": null, "quality_check": null, "quality_checked_at": null, @@ -827,42 +723,42 @@ "position_advice": null, "signal_factors_json": null, "time_horizon": null, - "decision_type": "holding", + "decision_type": null, "trigger": {}, "changelog": [], "quality_issues": {}, "signal_factors": [], - "timestamp": "2026-07-03 11:17:22", - "type": "holding" + "timestamp": "2026-07-08T11:42:04.322347", + "type": "decision" }, { "code": "688639", - "name": "华恒生物", + "name": null, "version": 1, - "price": 16.61, - "cost": 21.51, - "shares": 2800, - "stop_loss": 14.12, - "take_profit": 17.95, - "entry_low": 14.96, - "entry_high": 17.44, + "price": null, + "cost": null, + "shares": 0, + "stop_loss": 18.36, + "take_profit": 24.48, + "entry_low": null, + "entry_high": null, "currency": "CNY", - "strategy_type": "holding", - "action": "深套持有 | 深套持有 | 止损14.12 | 目标0 | 买入区14.95~17.44 | 信号:持有", - "timing_signal": "持有", - "rr_ratio": 0.0, - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None | MA5=35.03 MA10=36.72 MA20=37.12 MA60=34.13", - "stock_category": "深套", - "sector_context": "行业化学制品大跌-145.0%,收紧止损", + "strategy_type": "decision", + "action": null, + "timing_signal": null, + "rr_ratio": null, + "tech_snapshot": null, + "stock_category": null, + "sector_context": null, "status": "active", "trigger_json": null, "changelog_json": null, - "source": "auto", - "reason": null, - "created_at": "2026-07-03 11:17:30", - "updated_at": "2026-07-03 11:17:30", + "source": "migrate", + "reason": "6/9知微收紧:宽度14%→10%", + "created_at": "2026-07-08T11:42:04.322387", + "updated_at": null, "avg_price": null, - "decision_timestamp": "2026-07-03 11:17:30", + "decision_timestamp": null, "note": null, "quality_check": null, "quality_checked_at": null, @@ -870,42 +766,42 @@ "position_advice": null, "signal_factors_json": null, "time_horizon": null, - "decision_type": "holding", + "decision_type": null, "trigger": {}, "changelog": [], "quality_issues": {}, "signal_factors": [], - "timestamp": "2026-07-03 11:17:30", - "type": "holding" + "timestamp": "2026-07-08T11:42:04.322387", + "type": "decision" }, { "code": "688981", - "name": "中芯国际", + "name": null, "version": 1, - "price": 147.96, - "cost": 126.07, - "shares": 300, - "stop_loss": 140.94, - "take_profit": 156.92, - "entry_low": 133.16, - "entry_high": 155.36, + "price": null, + "cost": null, + "shares": 0, + "stop_loss": 112.0, + "take_profit": 145.0, + "entry_low": null, + "entry_high": null, "currency": "CNY", - "strategy_type": "holding", - "action": "盈利良好 | ⚠️盈亏比极低,关注 | 止损140.94 | 目标0 | 买入区133.16~155.36 | 信号:持有", - "timing_signal": "持有", - "rr_ratio": 0.0, - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None | MA5=146.09 MA10=149.74 MA20=141.28 MA60=127.88", - "stock_category": "中短线", - "sector_context": "半导体", + "strategy_type": "decision", + "action": null, + "timing_signal": null, + "rr_ratio": null, + "tech_snapshot": null, + "stock_category": null, + "sector_context": null, "status": "active", "trigger_json": null, "changelog_json": null, - "source": "auto", + "source": "migrate", "reason": null, - "created_at": "2026-07-03 11:17:37", - "updated_at": "2026-07-03 11:17:37", + "created_at": "2026-07-08T11:42:04.322360", + "updated_at": null, "avg_price": null, - "decision_timestamp": "2026-07-03 11:17:37", + "decision_timestamp": null, "note": null, "quality_check": null, "quality_checked_at": null, @@ -913,15 +809,15 @@ "position_advice": null, "signal_factors_json": null, "time_horizon": null, - "decision_type": "holding", + "decision_type": null, "trigger": {}, "changelog": [], "quality_issues": {}, "signal_factors": [], - "timestamp": "2026-07-03 11:17:37", - "type": "holding" + "timestamp": "2026-07-08T11:42:04.322360", + "type": "decision" } ], "total": 19, - "regenerated_at": "2026-07-07 10:28" + "regenerated_at": "2026-07-08 11:42" } \ No newline at end of file diff --git a/data/mofin_health.html b/data/mofin_health.html new file mode 100644 index 00000000..4d32161b --- /dev/null +++ b/data/mofin_health.html @@ -0,0 +1,130 @@ + + +MoFin 健康监控 + + +

📊 MoFin 系统健康监控

+
加载中...
+
+
🌳 功能树
+
🗃️ 数据实体
+
🔧 流程/Cron
+
+ +
+
+
+ + + diff --git a/data/portfolio.json b/data/portfolio.json index 46620ea4..36ef96bb 100644 --- a/data/portfolio.json +++ b/data/portfolio.json @@ -1,224 +1 @@ -{ - "holdings": [ - { - "code": "00700", - "name": "腾讯", - "shares": 100, - "cost": 443.13, - "position_pct": null, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 447.4, - "market_value": 39063.0, - "change_pct": 3.76, - "currency": "HKD" - }, - { - "code": "01088", - "name": "中国神华", - "shares": 500, - "cost": 45.89, - "position_pct": 2.14, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 40.66, - "market_value": 17575.0, - "change_pct": 1.65, - "currency": "HKD" - }, - { - "code": "01211", - "name": "比亚迪股份", - "shares": 600, - "cost": 104.87, - "position_pct": 4.62, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 84.15, - "market_value": 44376.0, - "change_pct": 0.06, - "currency": "HKD" - }, - { - "code": "01478", - "name": "丘钛科技", - "shares": 11000, - "cost": 13.47, - "position_pct": 7.97, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 6.74, - "market_value": 64460.0, - "change_pct": -3.58, - "currency": "HKD" - }, - { - "code": "02202", - "name": "万科企业", - "shares": 19700, - "cost": 4.67, - "position_pct": 4.6, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 2.32, - "market_value": 40385.0, - "change_pct": -0.85, - "currency": "HKD" - }, - { - "code": "300035", - "name": "中科电气", - "shares": 1400, - "cost": 22.29, - "position_pct": 2.42, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 14.03, - "market_value": 19712.0, - "change_pct": -1.82, - "currency": "CNY" - }, - { - "code": "300308", - "name": "中际旭创", - "shares": 100, - "cost": 1316.53, - "position_pct": 15.27, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 1117.49, - "market_value": 106181.0, - "change_pct": 0.13, - "currency": "CNY" - }, - { - "code": "300750", - "name": "宁德时代", - "shares": 100, - "cost": 401.78, - "position_pct": 4.64, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 378.07, - "market_value": 37726.0, - "change_pct": -0.51, - "currency": "CNY" - }, - { - "code": "518880", - "name": "黄金ETF华安", - "shares": 2400, - "cost": 12.19, - "position_pct": 2.45, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 8.63, - "market_value": 20832.0, - "change_pct": -0.45, - "currency": "CNY" - }, - { - "code": "600563", - "name": "法拉电子", - "shares": 100, - "cost": 147.18, - "position_pct": 2.3, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 154.64, - "market_value": 15021.0, - "change_pct": -1.54, - "currency": "CNY" - }, - { - "code": "601899", - "name": "紫金矿业", - "shares": 2400, - "cost": 39.89, - "position_pct": 7.34, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 28.42, - "market_value": 68784.0, - "change_pct": 2.16, - "currency": "CNY" - }, - { - "code": "688411", - "name": "海博思创", - "shares": 200, - "cost": 266.95, - "position_pct": 6.31, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 263.0, - "market_value": 52064.0, - "change_pct": 4.72, - "currency": "CNY" - }, - { - "code": "688639", - "name": "华恒生物", - "shares": 2800, - "cost": 21.51, - "position_pct": 5.25, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 16.57, - "market_value": 46480.0, - "change_pct": -0.54, - "currency": "CNY" - }, - { - "code": "688981", - "name": "中芯国际", - "shares": 300, - "cost": 126.07, - "position_pct": 5.44, - "added_at": null, - "is_active": 1, - "closed_at": null, - "close_pnl": null, - "price": 147.6, - "market_value": 40599.0, - "change_pct": 5.2, - "currency": "CNY" - } - ], - "cash": 321271.0, - "frozen_cash": 0.0, - "total_mv": 620482.46, - "total_assets": 941753.46, - "stock_value": 620482.46, - "position_pct": 65.89, - "currency": "CNY", - "updated_at": "2026-07-06 13:06:24", - "total_pnl": null, - "cash_history": [] -} \ No newline at end of file +{"__comment": "此文件已废弃,所有数据从 mofin.db holdings + portfolio_summary 表读取。保留空文件防止导入报错,后续可安全删除。", "holdings": [], "cash": 0, "total_assets": 0, "updated_at": ""} diff --git a/data/price_history.json b/data/price_history.json index f231da88..08d9d06f 100644 --- a/data/price_history.json +++ b/data/price_history.json @@ -5,6 +5,12 @@ "high": 90.1, "low": 79.85, "close": 86.2 + }, + { + "date": "2026-07-07", + "high": 74.75, + "low": 67.1, + "close": 68.85 } ], "01478": [ @@ -38,7 +44,13 @@ "date": "2026-07-07", "high": 1215.0, "low": 1180.0, - "close": 1206.91 + "close": 1188.8 + }, + { + "date": "2026-07-08", + "high": 1202.0, + "low": 1177.0, + "close": 1187.99 } ], "02202": [ @@ -61,6 +73,18 @@ "high": 53.0, "low": 51.7, "close": 52.45 + }, + { + "date": "2026-07-07", + "high": 53.05, + "low": 52.0, + "close": 52.25 + }, + { + "date": "2026-07-08", + "high": 54.2, + "low": 52.0, + "close": 53.95 } ], "601318": [ @@ -84,9 +108,15 @@ }, { "date": "2026-07-07", - "high": 50.2, + "high": 50.22, "low": 48.81, - "close": 50.1 + "close": 49.29 + }, + { + "date": "2026-07-08", + "high": 50.22, + "low": 48.96, + "close": 49.5 } ], "02359": [ @@ -101,6 +131,18 @@ "high": 158.7, "low": 151.1, "close": 152.5 + }, + { + "date": "2026-07-07", + "high": 155.5, + "low": 149.2, + "close": 150.3 + }, + { + "date": "2026-07-08", + "high": 155.5, + "low": 144.8, + "close": 145.2 } ], "02388": [ @@ -115,6 +157,18 @@ "high": 42.98, "low": 41.94, "close": 42.68 + }, + { + "date": "2026-07-07", + "high": 43.28, + "low": 42.62, + "close": 43.1 + }, + { + "date": "2026-07-08", + "high": 45.3, + "low": 42.62, + "close": 45.2 } ], "02628": [ @@ -129,6 +183,18 @@ "high": 29.08, "low": 27.5, "close": 28.64 + }, + { + "date": "2026-07-07", + "high": 29.22, + "low": 28.04, + "close": 28.28 + }, + { + "date": "2026-07-08", + "high": 29.22, + "low": 28.04, + "close": 28.76 } ], "688630": [ @@ -154,7 +220,13 @@ "date": "2026-07-07", "high": 499.0, "low": 448.0, - "close": 466.16 + "close": 462.23 + }, + { + "date": "2026-07-08", + "high": 494.81, + "low": 451.06, + "close": 493.0 } ], "06160": [ @@ -169,6 +241,18 @@ "high": 183.9, "low": 171.9, "close": 182.9 + }, + { + "date": "2026-07-07", + "high": 190.0, + "low": 183.0, + "close": 183.8 + }, + { + "date": "2026-07-08", + "high": 190.0, + "low": 181.9, + "close": 182.6 } ], "06869": [ @@ -177,6 +261,12 @@ "high": 229.2, "low": 207.4, "close": 208.6 + }, + { + "date": "2026-07-07", + "high": 170.2, + "low": 151.9, + "close": 156.5 } ], "688795": [ @@ -200,9 +290,15 @@ }, { "date": "2026-07-07", - "high": 649.88, + "high": 699.0, "low": 615.0, - "close": 635.51 + "close": 663.2 + }, + { + "date": "2026-07-08", + "high": 699.0, + "low": 629.0, + "close": 675.99 } ], "09868": [ @@ -217,6 +313,18 @@ "high": 53.7, "low": 49.3, "close": 50.9 + }, + { + "date": "2026-07-07", + "high": 54.05, + "low": 52.05, + "close": 52.2 + }, + { + "date": "2026-07-08", + "high": 54.05, + "low": 50.95, + "close": 52.9 } ], "09988": [ @@ -231,6 +339,18 @@ "high": 97.4, "low": 93.55, "close": 95.15 + }, + { + "date": "2026-07-07", + "high": 100.5, + "low": 95.3, + "close": 95.55 + }, + { + "date": "2026-07-08", + "high": 105.0, + "low": 95.3, + "close": 103.8 } ], "688802": [ @@ -254,9 +374,15 @@ }, { "date": "2026-07-07", - "high": 745.0, + "high": 876.0, "low": 690.11, - "close": 731.0 + "close": 833.99 + }, + { + "date": "2026-07-08", + "high": 876.0, + "low": 723.0, + "close": 846.01 } ], "300124": [ @@ -281,8 +407,14 @@ { "date": "2026-07-07", "high": 71.94, - "low": 68.2, - "close": 68.33 + "low": 65.73, + "close": 66.05 + }, + { + "date": "2026-07-08", + "high": 69.79, + "low": 63.5, + "close": 64.44 } ], "000657": [ @@ -307,8 +439,14 @@ { "date": "2026-07-07", "high": 90.58, - "low": 80.46, - "close": 80.46 + "low": 75.41, + "close": 76.84 + }, + { + "date": "2026-07-08", + "high": 80.8, + "low": 72.98, + "close": 77.36 } ], "000711": [ @@ -333,8 +471,14 @@ { "date": "2026-07-07", "high": 5.65, - "low": 5.0, - "close": 5.01 + "low": 4.92, + "close": 5.42 + }, + { + "date": "2026-07-08", + "high": 5.59, + "low": 4.92, + "close": 5.54 } ], "001309": [ @@ -359,8 +503,14 @@ { "date": "2026-07-07", "high": 954.19, - "low": 860.0, - "close": 935.0 + "low": 845.28, + "close": 851.0 + }, + { + "date": "2026-07-08", + "high": 900.0, + "low": 790.0, + "close": 847.5 } ], "002594": [ @@ -385,8 +535,14 @@ { "date": "2026-07-07", "high": 88.95, - "low": 86.61, - "close": 87.54 + "low": 85.87, + "close": 86.26 + }, + { + "date": "2026-07-08", + "high": 88.81, + "low": 85.28, + "close": 87.25 } ], "00700": [ @@ -409,6 +565,18 @@ "high": 2.15, "low": 2.03, "close": 2.1 + }, + { + "date": "2026-07-07", + "high": 2.1, + "low": 2.0, + "close": 2.01 + }, + { + "date": "2026-07-08", + "high": 2.1, + "low": 1.95, + "close": 2.0 } ], "00981": [ @@ -431,6 +599,18 @@ "high": 13.39, "low": 12.63, "close": 13.36 + }, + { + "date": "2026-07-07", + "high": 15.18, + "low": 14.23, + "close": 14.45 + }, + { + "date": "2026-07-08", + "high": 15.18, + "low": 14.23, + "close": 14.79 } ], "01088": [ @@ -465,8 +645,14 @@ { "date": "2026-07-07", "high": 710.0, - "low": 659.16, - "close": 681.8 + "low": 619.9, + "close": 627.9 + }, + { + "date": "2026-07-08", + "high": 647.78, + "low": 584.67, + "close": 624.2 } ], "000700": [ @@ -478,9 +664,9 @@ }, { "date": "2026-07-07", - "high": 17.18, - "low": 16.3, - "close": 16.75 + "high": 17.38, + "low": 16.04, + "close": 16.11 } ], "300548": [ @@ -493,8 +679,56 @@ { "date": "2026-07-07", "high": 225.0, - "low": 205.0, - "close": 214.13 + "low": 204.1, + "close": 211.03 + } + ], + "000850": [ + { + "date": "2026-07-07", + "high": 3.95, + "low": 3.75, + "close": 3.81 + }, + { + "date": "2026-07-08", + "high": 3.95, + "low": 3.72, + "close": 3.86, + "volume": 60649.0, + "amount": 2312.0 + } + ], + "603501": [ + { + "date": "2026-07-08", + "high": 101.0, + "low": 97.01, + "close": 100.08 + } + ], + "688041": [ + { + "date": "2026-07-08", + "high": 359.04, + "low": 342.01, + "close": 352.07 + } + ], + "002371": [ + { + "date": "2026-07-08", + "high": 830.02, + "low": 768.0, + "close": 819.85 + } + ], + "000001": [ + { + "date": "2026-07-08", + "high": 10.59, + "low": 10.34, + "close": 10.53 } ] } \ No newline at end of file diff --git a/data/stocks/000700.json b/data/stocks/000700.json index eadd881f..27bb21f0 100644 --- a/data/stocks/000700.json +++ b/data/stocks/000700.json @@ -26,6 +26,16 @@ "content": "- **模塑科技(000700)** 15.54 / 止损13.91 — 昨日+5.28%强势,守住15持有", "report_id": "cron_99c06255590a_2026-07-02_08-32-48" }, + { + "time": "2026-07-07T08:55:31.376433", + "content": "博创科技(300548) 214.13 在买入区✅ | 模塑科技(000700) 16.75 在买入区✅ | 贵州茅台(600519) 1206.91 在买入区✅ | 比亚迪A(002594) 87.", + "report_id": "cron_99c06255590a_2026-07-07_08-43-59" + }, + { + "time": "2026-07-08T08:55:38.411281", + "content": "**自选靠近买入区:** 模塑科技(000700)16.15在15.5~18.08买入区;建滔积层板(01888)70.0在67.27~78.48买入区;华茂股份(000850)3.81在3.70~3", + "report_id": "cron_99c06255590a_2026-07-08_08-39-49" + }, { "time": "2026-06-30T08:55:46.708460", "content": "- 模塑科技000700 13.86 | 买入区13.33~14.09 **(仍在区内,但已破止损)**", diff --git a/data/stocks/002594.json b/data/stocks/002594.json index 45f898fd..d4c9700f 100644 --- a/data/stocks/002594.json +++ b/data/stocks/002594.json @@ -31,6 +31,11 @@ "content": "**比亚迪A(002594)** ¥91.6 | 仓位0% | 买入区89.77~92.76 → RR 2.56", "report_id": "cron_99c06255590a_2026-06-17_08-35-59" }, + { + "time": "2026-07-07T08:55:31.376433", + "content": "博创科技(300548) 214.13 在买入区✅ | 模塑科技(000700) 16.75 在买入区✅ | 贵州茅台(600519) 1206.91 在买入区✅ | 比亚迪A(002594) 87.", + "report_id": "cron_99c06255590a_2026-07-07_08-43-59" + }, { "time": "2026-06-01T13:55:12.662353", "content": "**🔴比亚迪A股(002594) | 93.80 -2.47%** → 已有H股持仓,暂不加仓", diff --git a/data/stocks/00700.json b/data/stocks/00700.json index 2598ca9b..11bf7c91 100644 --- a/data/stocks/00700.json +++ b/data/stocks/00700.json @@ -146,6 +146,11 @@ "content": "② **腾讯(00700)** 现价430.2 | 仓位待确认 | 买入区421-430上沿", "report_id": "cron_99c06255590a_2026-07-03_08-33-17" }, + { + "time": "2026-07-07T08:55:31.376433", + "content": "博创科技(300548) 214.13 在买入区✅ | 模塑科技(000700) 16.75 在买入区✅ | 贵州茅台(600519) 1206.91 在买入区✅ | 比亚迪A(002594) 87.", + "report_id": "cron_99c06255590a_2026-07-07_08-43-59" + }, { "time": "2026-06-02T11:55:44.093905", "content": "❌ 反例:腾讯00700,底仓100股(3.59%),浮盈+4.79%。看到\"距止盈470仅0.77%\"就建议触及减仓清仓 —— 机械执行,未考虑底仓属性+浮盈幅度。", @@ -1021,6 +1026,16 @@ "content": "**港股(恒指-0.63%):** 中芯国际H(00981) 89.40+5.42%浮盈+17%持有 | 腾讯控股(00700) 429.80+2.28%浮亏-3%距止损3.4% | 丘钛科技(014", "report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27" }, + { + "time": "2026-07-07T08:55:31.476330", + "content": "**③ 00700 腾讯 447.00(+3.66%) | 仓~4.2%→持有**", + "report_id": "cron_d42f2ce3b479_2026-07-06_20-24-34" + }, + { + "time": "2026-07-08T08:55:38.511510", + "content": "**① 腾讯(00700) 现价461.20(+2.04%) | 仓~4.3%→持有**", + "report_id": "cron_d42f2ce3b479_2026-07-07_20-20-44" + }, { "time": "2026-06-08T08:55:37.165829", "content": "| **腾讯控股(00700)** | 453.20 | 追踪止盈459.8已触发 | 关注445支撑,跌破450可接回 |", diff --git a/data/stocks/00981.json b/data/stocks/00981.json index eac25761..938e8de7 100644 --- a/data/stocks/00981.json +++ b/data/stocks/00981.json @@ -166,6 +166,11 @@ "content": "**港股(恒指-0.63%):** 中芯国际H(00981) 89.40+5.42%浮盈+17%持有 | 腾讯控股(00700) 429.80+2.28%浮亏-3%距止损3.4% | 丘钛科技(014", "report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27" }, + { + "time": "2026-07-08T08:55:38.511510", + "content": "**自选→距买入区±3%:** 中芯国际H(00981) ¥73.45 买入区上沿72.79 距+0.9%——已触发但07-06已清仓,仅关注不介入。其余自选均远离买入区。", + "report_id": "cron_d42f2ce3b479_2026-07-07_20-20-44" + }, { "time": "2026-07-06T08:55:01.761812", "content": "| **中芯国际双持(688981+00981)** | A+H 双持合计约 9.64% 仓位,两市场同步净多头,若港股中芯继续走弱(注意 00981 止损 ¥77.55 高于成本 ¥75.94 的异", diff --git a/data/stocks/01478.json b/data/stocks/01478.json index c9779a77..4f5807a7 100644 --- a/data/stocks/01478.json +++ b/data/stocks/01478.json @@ -456,6 +456,11 @@ "content": "③ 丘钛科技(01478) 距止损6.48仅+7.3%!仓位7.97%", "report_id": "cron_d42f2ce3b479_2026-07-03_20-03-44" }, + { + "time": "2026-07-07T08:55:31.476330", + "content": "**1. 01478 丘钛科技 现6.59HKD(-5.72%) →距止损7.33已破-10%!** 成本13.47亏损51%。今日低开低走无反弹,摄像头模组需求走弱。操作:反弹至6.8-7.0减半仓", + "report_id": "cron_d42f2ce3b479_2026-07-06_20-24-34" + }, { "time": "2026-07-06T08:55:01.761812", "content": "2. **收紧输出质量** — 使用\"关注/观望/留意\"时须紧随具体价格、止损位和触发条件。满 3 次模糊表述自动标记为低质量输出。对 01478 和 688795 做一次明确的持仓策略复核:继续持有", diff --git a/data/stocks/01888.json b/data/stocks/01888.json index c73793d3..7917166e 100644 --- a/data/stocks/01888.json +++ b/data/stocks/01888.json @@ -66,6 +66,11 @@ "content": "长飞光纤光缆(06869) 255.40+6.68%放量大涨,持有等站上成本263.72;腾讯(00700) 429.80+2.28%,买入区435-440依然可加;中芯H(00981) 89.40+", "report_id": "cron_99c06255590a_2026-07-01_08-39-42" }, + { + "time": "2026-07-08T08:55:38.411281", + "content": "**自选靠近买入区:** 模塑科技(000700)16.15在15.5~18.08买入区;建滔积层板(01888)70.0在67.27~78.48买入区;华茂股份(000850)3.81在3.70~3", + "report_id": "cron_99c06255590a_2026-07-08_08-39-49" + }, { "time": "2026-06-02T14:55:53.464779", "content": "| 建滔积层板(01888) | 49.48 | +4.30% | 42-44 | 🔴 远超买入区 |", diff --git a/data/stocks/02202.json b/data/stocks/02202.json index 1ff52bec..7a32cf32 100644 --- a/data/stocks/02202.json +++ b/data/stocks/02202.json @@ -61,6 +61,11 @@ "content": "长飞光纤光缆(06869) 255.40+6.68%放量大涨,持有等站上成本263.72;腾讯(00700) 429.80+2.28%,买入区435-440依然可加;中芯H(00981) 89.40+", "report_id": "cron_99c06255590a_2026-07-01_08-39-42" }, + { + "time": "2026-07-08T08:55:38.411281", + "content": "**① 万科企业(02202) ⚠️ 触及止损边缘**", + "report_id": "cron_99c06255590a_2026-07-08_08-39-49" + }, { "time": "2026-06-02T12:55:54.834592", "content": "| 万科企业(02202) | 2.76 | -0.72% | 3.39% | 深套持有,止损2.5 |", @@ -611,6 +616,11 @@ "content": "**港股概览:** 恒指+1.8%反弹,持仓分化。腾讯(00700)浮亏-4.6%目标已超但盈亏比偏低;丘钛(01478)深套-49.7%及万科(02202)深套-52.4%均暂远离止损(距12-22", "report_id": "cron_d42f2ce3b479_2026-06-29_20-08-09" }, + { + "time": "2026-07-07T08:55:31.476330", + "content": "**2. 02202 万科企业 现2.31HKD(-1.28%) →距止损2.00差13%** 成本4.67亏损50.5%。行业下行未止,今日盘中无买盘承接。操作:若破2.20执行止损", + "report_id": "cron_d42f2ce3b479_2026-07-06_20-24-34" + }, { "time": "2026-06-29T08:55:56.391301", "content": "**③ ⚠️ 万科(02202)浮亏-52.9%,接近止损却无复盘**", diff --git a/data/stocks/300035.json b/data/stocks/300035.json index fc255b79..8795f527 100644 --- a/data/stocks/300035.json +++ b/data/stocks/300035.json @@ -56,6 +56,11 @@ "content": "• 中科电气(300035) 14.19(平) 距止损13.74仅3%", "report_id": "cron_99c06255590a_2026-06-30_08-40-17" }, + { + "time": "2026-07-08T08:55:38.411281", + "content": "**② 中科电气(300035) ⚠️ 触及止损边缘**", + "report_id": "cron_99c06255590a_2026-07-08_08-39-49" + }, { "time": "2026-06-02T12:55:54.834592", "content": "| 中科电气(300035) | 16.44 | -2.66% | 1.77% | 深套持有,止损15.0 |", @@ -521,6 +526,11 @@ "content": "**A股(上证+0.50%):** 海博思创(688411) 291.20+1.82%浮盈+9%RR3.92持有 | 宁德时代(300750) 393.01+0.17%弱势持有距止损3% | 法拉电子", "report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27" }, + { + "time": "2026-07-07T08:55:31.476330", + "content": "**3. 300035 中科电气 现13.94(-2.45%) →浮亏37.5%无止损** 连续阴跌缩量,负极材料竞争恶化。操作:反弹至15设止损", + "report_id": "cron_d42f2ce3b479_2026-07-06_20-24-34" + }, { "time": "2026-06-29T08:55:56.391301", "content": "- **中科电气(300035)** — 现价14.19,止损13.74(距3.2%)。新能电池板块本周-4.29%,中科电气持续走弱。", diff --git a/data/stocks/300548.json b/data/stocks/300548.json index ad12bce5..3ab5688c 100644 --- a/data/stocks/300548.json +++ b/data/stocks/300548.json @@ -56,6 +56,11 @@ "content": "长飞光纤光缆(06869) 255.40+6.68%放量大涨,持有等站上成本263.72;腾讯(00700) 429.80+2.28%,买入区435-440依然可加;中芯H(00981) 89.40+", "report_id": "cron_99c06255590a_2026-07-01_08-39-42" }, + { + "time": "2026-07-07T08:55:31.376433", + "content": "博创科技(300548) 214.13 在买入区✅ | 模塑科技(000700) 16.75 在买入区✅ | 贵州茅台(600519) 1206.91 在买入区✅ | 比亚迪A(002594) 87.", + "report_id": "cron_99c06255590a_2026-07-07_08-43-59" + }, { "time": "2026-06-02T13:56:05.495125", "content": "🔺**博创科技(300548)** | 236.72 +7.92% | 买入区230~235已确认站上,等回调建仓2%", diff --git a/data/stocks/688411.json b/data/stocks/688411.json index c4aaec7a..990e4c1e 100644 --- a/data/stocks/688411.json +++ b/data/stocks/688411.json @@ -831,6 +831,16 @@ "content": "**A股(上证+0.50%):** 海博思创(688411) 291.20+1.82%浮盈+9%RR3.92持有 | 宁德时代(300750) 393.01+0.17%弱势持有距止损3% | 法拉电子", "report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27" }, + { + "time": "2026-07-07T08:55:31.476330", + "content": "**① 688411 海博思创 271.90(+8.26%) | 仓6.3%→持有**", + "report_id": "cron_d42f2ce3b479_2026-07-06_20-24-34" + }, + { + "time": "2026-07-08T08:55:38.511510", + "content": "**③ 海博思创(688411) 现价262.20(-1.35%) | 仓~5.5%→持有**", + "report_id": "cron_d42f2ce3b479_2026-07-07_20-20-44" + }, { "time": "2026-06-08T08:55:37.165829", "content": "2. **海博思创(688411) 追踪止盈失守未补救** — 高点305.53回落5%约290的追踪止盈被击穿(6/4 收274.90),但未在285~290反弹位减仓,浮盈从+24.76%缩窄至+", diff --git a/data/stocks/688639.json b/data/stocks/688639.json index 6b10f916..fd1ccd25 100644 --- a/data/stocks/688639.json +++ b/data/stocks/688639.json @@ -51,6 +51,11 @@ "content": "- **华恒生物(688639)** 16.37 / 止损14.96 — 距止损8.6%,窄幅运行可持", "report_id": "cron_99c06255590a_2026-07-02_08-32-48" }, + { + "time": "2026-07-07T08:55:31.376433", + "content": "- 华恒生物(688639) 16.6 | 浮亏-23%,放量破16止损", + "report_id": "cron_99c06255590a_2026-07-07_08-43-59" + }, { "time": "2026-06-01T14:55:20.378304", "content": "• 华恒生物(688639) 23.58 | +0.47% → 🤝持有,接近止损23.18", diff --git a/data/strategy_staleness_report.json b/data/strategy_staleness_report.json index 69a9b939..28958b1f 100644 --- a/data/strategy_staleness_report.json +++ b/data/strategy_staleness_report.json @@ -1,122 +1,14 @@ { - "checked_at": "2026-07-07T10:22:06", - "total_active": 19, - "flagged_count": 7, - "flagged": [ - { - "code": "000700", - "name": "模塑科技", - "price": 17.22, - "flags": [ - "现价17.22在买入区16~18(是否可买需结合timing_signal判断)" - ], - "age_days": 3, - "last_update": "2026-07-06 11:07:37", - "entry_zone": "16~18", - "current": "盈利良好 | ⚠️盈亏比极低,关注 | 止损16.7 | 目标0 | 买入区15.5~18.08 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": true - }, - { - "code": "00700", - "name": "腾讯", - "price": 497.35, - "flags": [ - "价格距买入区中心32% 需关注" - ], - "age_days": 3, - "last_update": "2026-07-03 11:16:48", - "entry_zone": "371~386", - "current": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 损363.06 | 盈0 | 买370.63~385.75", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": false - }, - { - "code": "01088", - "name": "中国神华", - "price": 46.14, - "flags": [ - "价格距买入区中心36% 需关注" - ], - "age_days": 3, - "last_update": "2026-07-03 11:16:48", - "entry_zone": "31~36", - "current": "持有观察 | ⚠️盈亏比极低,关注 | 止损33.67 | 目标0 | 买入区31.24~36.45 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": false - }, - { - "code": "01211", - "name": "比亚迪股份", - "price": 97.0, - "flags": [ - "价格距买入区中心39% 需关注" - ], - "age_days": 3, - "last_update": "2026-07-03 11:16:48", - "entry_zone": "64~75", - "current": "深套持有 | 深套持有 | 止损64.29 | 目标0 | 买入区64.22~74.92 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": false - }, - { - "code": "01888", - "name": "建滔积层板", - "price": 73.3, - "flags": [ - "现价73.30在买入区67~78(是否可买需结合timing_signal判断)" - ], - "age_days": 3, - "last_update": "2026-07-06 11:07:37", - "entry_zone": "67~78", - "current": "持有观察 | ⚠️盈亏比极低,关注 | 止损64.14 | 目标0 | 买入区67.27~78.48 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": true - }, - { - "code": "02202", - "name": "万科企业", - "price": 2.7, - "flags": [ - "价格距买入区中心38% 需关注" - ], - "age_days": 3, - "last_update": "2026-07-03 11:16:49", - "entry_zone": "2~2", - "current": "深套持有 | 深套持有 | 止损1.9 | 目标0 | 买入区1.81~2.11 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": false - }, - { - "code": "300548", - "name": "长芯博创", - "price": 228.23, - "flags": [ - "现价228.23在买入区205~240(是否可买需结合timing_signal判断)" - ], - "age_days": 3, - "last_update": "2026-07-06 11:01:50", - "entry_zone": "205~240", - "current": "持有观察 | ⚠️盈亏比极低,关注 | 止损221.38 | 目标0 | 买入区205.41~239.64 | 信号:持有", - "updated_by": "auto", - "updated_reason": "自动生成", - "is_watchlist": true - } - ], + "checked_at": "2026-07-08T09:00:59", + "total_active": 0, + "flagged_count": 0, + "flagged": [], "portfolio": { - "position_pct": 65.89, + "position_pct": 65.65, "cash": 321271.0, - "weak_position_pct": 71.4, - "all_weak_pct": 63.2, - "signals": [ - "[PORTFOLIO_WEAK] 组合中弱势+深套分类持仓占比71.4%>40%,建议系统性减仓" - ] + "weak_position_pct": 0, + "all_weak_pct": 0, + "signals": [] }, - "summary": "扫描19个策略,7个需关注" + "summary": "扫描0个策略,0个需关注" } \ No newline at end of file diff --git a/data/watchlist.json b/data/watchlist.json deleted file mode 100644 index 1e7d121f..00000000 --- a/data/watchlist.json +++ /dev/null @@ -1,645 +0,0 @@ -{ - "stocks": [ - { - "code": "02388", - "name": "中银香港", - "price": 48.26, - "analysis": { - "buy_low": 47.12, - "buy_high": 48.48, - "position_recommend": "", - "reason": "回调-0.95%至47.80,止损46安全,继续持有观察", - "updated_at": "2026-06-02T10:46:11.973827", - "tech_levels": { - "strong_support": 44.14, - "weak_support": 47.12, - "weak_resist": 48.48, - "strong_resist": 51.34 - }, - "stop_loss": 42.87, - "take_profit": 53.59, - "entry_low": 41.01, - "entry_high": 46.6, - "action": "盈利持有,止损42.87 目标53.59", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "01888", - "name": "建滔积层板", - "price": 65.6, - "analysis": { - "stop_loss": 61.42, - "take_profit": 77.23, - "entry_low": 63.99, - "entry_high": 66.61, - "action": "盈利持有 | 损61.42 | 盈77.23 | 买63.99~66.61", - "tech_snapshot": "形态:光头光脚阴线/neutral 量价:数据不足 强撑:57.53 弱撑:61.42 弱压:71.27 强压:77.23", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.07, - "action_note": "", - "timing_signal": "neutral" - }, - "strategy_action": "盈利持有", - "stop_loss": 74.32, - "entry_low": 76.01, - "entry_high": 88.67, - "take_profit": 95.0, - "current_signal": "持有" - }, - { - "code": "01088", - "name": "中国神华", - "price": 45.32, - "analysis": { - "stop_loss": 43.75, - "take_profit": 47.67, - "entry_low": 44.2, - "entry_high": 45.32, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.9),谨慎买入 | 损43.75 | 盈47.67 | 买44.2~45.32 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:长影星线/neutral 量价:数据不足 强撑:42.64 弱撑:44.71 弱压:45.55 强压:47.67", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 1.9, - "action_note": "⚠️盈亏比偏低(1:1.9),谨慎买入", - "timing_signal": "接近支撑位,关注" - }, - "strategy_action": "盈利持有", - "stop_loss": 38.84, - "entry_low": 36.0, - "entry_high": 42.0, - "take_profit": 46.0 - }, - { - "code": "01211", - "name": "比亚迪股份", - "price": 86.55, - "analysis": { - "buy_low": 84.95, - "buy_high": 87.17, - "position_recommend": null, - "reason": "大涨+5.34%", - "updated_at": "2026-06-02T11:01:41.539019", - "tech_levels": { - "strong_support": 79.79, - "weak_support": 84.95, - "weak_resist": 87.17, - "strong_resist": 91.98 - }, - "stop_loss": 76.64, - "take_profit": 95.79, - "entry_low": 73.3, - "entry_high": 83.3, - "action": "盈利持有,止损76.64 目标95.79", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "002594", - "name": "比亚迪", - "price": 91.6, - "analysis": { - "stop_loss": 88.85, - "take_profit": 98.63, - "entry_low": 89.77, - "entry_high": 92.76, - "action": "盈利持有 | 损88.85 | 盈98.63 | 买89.77~92.76 | 信号:放量下跌,等企稳再入", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:主动卖盘占优 强撑:83.13 弱撑:89.8 弱压:92.5 强压:98.63", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 2.56, - "action_note": "", - "timing_signal": "放量下跌,等企稳再入" - } - }, - { - "code": "09868", - "name": "小鹏汽车-W", - "price": 57.7, - "analysis": { - "stop_loss": 55.29, - "take_profit": 59.97, - "entry_low": 55.86, - "entry_high": 57.16, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.7),谨慎买入 | 损55.29 | 盈59.97 | 买55.86~57.16 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:十字星/neutral 量价:数据不足 强撑:54.27 弱撑:56.08 弱压:58.03 强压:59.97", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 1.74, - "action_note": "⚠️盈亏比偏低(1:1.7),谨慎买入", - "timing_signal": "接近支撑位,关注" - }, - "strategy_action": "观望", - "stop_loss": 48.0, - "entry_low": 49.0, - "entry_high": 53.0, - "take_profit": 58.0, - "current_signal": "弱势观望" - }, - { - "code": "688795", - "name": "摩尔线程-U", - "price": 610.55, - "analysis": { - "stop_loss": 592.23, - "take_profit": 673.56, - "entry_low": 598.34, - "entry_high": 622.76, - "action": "盈利持有 | 损592.23 | 盈673.56 | 买598.34~622.76 | 信号:放量下跌,等企稳再入", - "tech_snapshot": "形态:带上影阴线/neutral 量价:主动卖盘占优 强撑:575.87 弱撑:596.38 弱压:638.88 强压:673.56", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.44, - "action_note": "", - "timing_signal": "放量下跌,等企稳再入" - }, - "strategy_action": "盈利持有", - "stop_loss": 610.0, - "entry_low": 620.0, - "entry_high": 650.0, - "take_profit": 700.0, - "current_signal": "观望" - }, - { - "code": "688802", - "name": "沐曦股份-U", - "price": 694.89, - "analysis": { - "stop_loss": 674.04, - "take_profit": 766.4, - "entry_low": 680.99, - "entry_high": 708.79, - "action": "盈利持有 | 损674.04 | 盈766.4 | 买680.99~708.79", - "tech_snapshot": "形态:带上影阴线/neutral 量价:数据不足 强撑:655.22 弱撑:678.97 弱压:726.74 强压:766.4", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.43, - "action_note": "", - "timing_signal": "neutral" - }, - "strategy_action": "盈利持有", - "stop_loss": 700.0, - "entry_low": 710.0, - "entry_high": 740.0, - "take_profit": 790.0, - "current_signal": "观望" - }, - { - "code": "02359", - "name": "药明康德", - "price": 126.7, - "analysis": { - "stop_loss": 121.64, - "take_profit": 131.07, - "entry_low": 122.89, - "entry_high": 125.41, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.5),谨慎买入 | 损121.64 | 盈131.07 | 买122.89~125.41", - "tech_snapshot": "形态:带上影阳线/neutral 量价:数据不足 强撑:118.27 弱撑:121.83 弱压:128.23 强压:131.07", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 1.51, - "action_note": "⚠️盈亏比偏低(1:1.5),谨慎买入", - "timing_signal": "neutral" - }, - "strategy_action": "盈利持有", - "stop_loss": 145.0, - "entry_low": 148.0, - "entry_high": 155.0, - "take_profit": 170.0, - "current_signal": "关注" - }, - { - "code": "02628", - "name": "中国人寿", - "price": 30.12, - "analysis": { - "stop_loss": 28.26, - "take_profit": 31.6, - "entry_low": 29.3, - "entry_high": 29.6, - "action": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 损28.26 | 盈31.6 | 买29.3~29.6 | 信号:阳线企稳,可买入", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:数据不足 强撑:27.44 弱撑:28.26 弱压:30.74 强压:31.6", - "reassessed_at": "2026-06-12 16:00", - "status": "review", - "rr_ratio": 1.04, - "action_note": "⚠️盈亏比不足1:1.5,不建议买入", - "timing_signal": "阳线企稳,可买入" - } - }, - { - "code": "00968", - "name": "信义光能", - "price": 2.41, - "analysis": { - "stop_loss": 2.34, - "take_profit": 2.63, - "entry_low": 2.36, - "entry_high": 2.46, - "action": "盈利持有 | 损2.34 | 盈2.63 | 买2.36~2.46 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:带上影阴线/bearish 量价:数据不足 强撑:2.24 弱撑:2.37 弱压:2.47 强压:2.63", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.14, - "action_note": "", - "timing_signal": "接近支撑位,关注" - }, - "strategy_action": "弱势持有", - "stop_loss": 1.92, - "entry_low": 1.96, - "entry_high": 2.15, - "take_profit": 2.4, - "current_signal": "弱势持有" - }, - { - "code": "06869", - "name": "长飞光纤", - "price": 226.6, - "analysis": { - "stop_loss": 219.27, - "take_profit": 254.33, - "entry_low": 224.81, - "entry_high": 233.29, - "action": "盈利持有 | 损219.27 | 盈254.33 | 买224.81~233.29", - "tech_snapshot": "形态:带下影阴线/neutral 量价:数据不足 强撑:209.13 弱撑:219.27 弱压:241.87 强压:254.33", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 2.46, - "action_note": "", - "timing_signal": "neutral" - }, - "strategy_action": "深套持有", - "stop_loss": 195.46, - "entry_low": 181.08, - "entry_high": 211.26, - "take_profit": 230.0 - }, - { - "code": "02318", - "name": "中国平安", - "price": 57.15, - "analysis": { - "stop_loss": 55.39, - "take_profit": 60.16, - "entry_low": 55.96, - "entry_high": 57.3, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.8),谨慎买入 | 损55.39 | 盈60.16 | 买55.96~57.3 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:数据不足 强撑:54.45 弱撑:56.15 弱压:58.25 强压:60.16", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 1.79, - "action_note": "⚠️盈亏比偏低(1:1.8),谨慎买入", - "timing_signal": "接近支撑位,关注" - }, - "strategy_action": "弱势持有", - "stop_loss": 49.5, - "entry_low": 50.0, - "entry_high": 53.0, - "take_profit": 58.0, - "current_signal": "弱势观望" - }, - { - "code": "688639", - "name": "华恒生物", - "price": 21.2, - "analysis": { - "stop_loss": 20.56, - "take_profit": 22.66, - "entry_low": 20.78, - "entry_high": 21.4, - "action": "盈利持有 | 损20.56 | 盈22.66 | 买20.78~21.4 | 信号:量价齐升,可买入", - "tech_snapshot": "形态:带下影阳线/bullish 量价:主动买盘占优 强撑:19.26 弱撑:20.6 弱压:21.46 强压:22.66", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 2.28, - "action_note": "", - "timing_signal": "量价齐升,可买入" - } - }, - { - "code": "300548", - "name": "博创科技", - "price": 224.03, - "analysis": { - "buy_low": 224.25, - "buy_high": 234.13, - "position_recommend": "2%", - "reason": "+7.26%站稳230确认可追,回调至230~235建仓。", - "updated_at": "2026-06-02T14:42:56.675237", - "tech_levels": { - "strong_support": 217.39, - "weak_support": 224.25, - "weak_resist": 234.13, - "strong_resist": 240.34 - }, - "stop_loss": 211.19, - "take_profit": 263.98, - "entry_low": 225.0, - "entry_high": 229.55, - "action": "盈利持有,止损211.19 目标263.98", - "tech_snapshot": "形态:光头光脚阴线/neutral 量价:主动买盘占优 强撑:227.53 弱撑:225.0 弱压:231.57 强压:233.59", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "300124", - "name": "汇川技术", - "price": 67.16, - "analysis": { - "stop_loss": 65.15, - "take_profit": 73.7, - "entry_low": 65.82, - "entry_high": 68.5, - "action": "盈利持有 | 损65.15 | 盈73.7 | 买65.82~68.5 | 信号:放量下跌,等企稳再入", - "tech_snapshot": "形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:62.95 弱撑:65.99 弱压:69.49 强压:73.7", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.25, - "action_note": "", - "timing_signal": "放量下跌,等企稳再入" - }, - "strategy_action": "盈利持有", - "stop_loss": 67.5, - "entry_low": 68.5, - "entry_high": 72.0, - "take_profit": 78.0, - "current_signal": "关注" - }, - { - "code": "01070", - "name": "TCL电子", - "price": 14.04, - "analysis": { - "stop_loss": 13.59, - "take_profit": 15.3, - "entry_low": 13.73, - "entry_high": 14.27, - "action": "盈利持有 | 损13.59 | 盈15.3 | 买13.73~14.27 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:数据不足 强撑:13.06 弱撑:13.75 弱压:14.42 强压:15.3", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.07, - "action_note": "", - "timing_signal": "接近支撑位,关注" - } - }, - { - "code": "09988", - "name": "阿里巴巴-W", - "price": 110.2, - "analysis": { - "buy_low": 107.4, - "buy_high": 111.4, - "position_recommend": null, - "reason": "大涨+5.86%可关注", - "updated_at": "2026-06-02T11:01:41.539017", - "tech_levels": { - "strong_support": 102.72, - "weak_support": 107.4, - "weak_resist": 111.4, - "strong_resist": 117.48 - }, - "stop_loss": 99.18, - "take_profit": 123.97, - "entry_low": 94.86, - "entry_high": 107.8, - "action": "盈利持有,止损99.18 目标123.97", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "00700", - "name": "腾讯控股", - "price": 463.6, - "analysis": { - "buy_low": 457.2, - "buy_high": 467.93, - "position_recommend": null, - "reason": "暴涨+7.75%逼近止盈470", - "updated_at": "2026-06-02T11:01:41.539014", - "tech_levels": { - "strong_support": 440.4, - "weak_support": 457.2, - "weak_resist": 467.93, - "strong_resist": 486.94 - }, - "stop_loss": 432.58, - "take_profit": 540.73, - "entry_low": 413.78, - "entry_high": 470.2, - "action": "盈利持有,止损432.58 目标540.73", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "02202", - "name": "万科企业", - "price": 2.64, - "analysis": { - "buy_low": 2.62, - "buy_high": 2.69, - "position_recommend": "", - "reason": "万科企业2.83微涨+1.8%,深套持有", - "updated_at": "2026-06-02T10:46:11.973869", - "tech_levels": { - "strong_support": 2.47, - "weak_support": 2.62, - "weak_resist": 2.69, - "strong_resist": 2.83 - }, - "stop_loss": 2.42, - "take_profit": 3.02, - "entry_low": 2.31, - "entry_high": 2.63, - "action": "盈利持有,止损2.42 目标3.02", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "688981", - "name": "中芯国际", - "price": 124.88, - "analysis": { - "buy_low": 124.58, - "buy_high": 132.34, - "position_recommend": "0%", - "reason": "浮盈+0.0%,止损112.53 目标137.0", - "updated_at": "2026-06-09T10:46:18", - "tech_levels": { - "strong_support": 121.97, - "weak_support": 124.58, - "weak_resist": 132.34, - "strong_resist": 137.49 - }, - "stop_loss": 121.46, - "take_profit": 151.82, - "entry_low": 127.4, - "entry_high": 132.02, - "action": "盈利持有,止损121.46 目标151.82", - "tech_snapshot": "形态:倒锤头/bullish 量价:买卖均衡 强撑:130.86 弱撑:127.4 弱压:132.8 强压:133.58", - "reassessed_at": "2026-06-12 09:30" - }, - "strategy_action": "盈利持有", - "stop_loss": 132.0, - "entry_low": 135.0, - "entry_high": 142.0, - "take_profit": 155.0, - "current_signal": "关注" - }, - { - "code": "01478", - "name": "丘钛科技", - "price": 8.13, - "analysis": { - "buy_low": 7.98, - "buy_high": 8.51, - "position_recommend": "", - "reason": "丘钛科技10.07跌-1.66%,深套-26.48%暂持", - "updated_at": "2026-06-02T10:46:11.973872", - "tech_levels": { - "strong_support": 7.92, - "weak_support": 7.98, - "weak_resist": 8.51, - "strong_resist": 8.74 - }, - "stop_loss": 7.47, - "take_profit": 9.34, - "entry_low": 7.15, - "entry_high": 8.12, - "action": "盈利持有,止损7.47 目标9.34", - "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", - "reassessed_at": "2026-06-12 09:30" - } - }, - { - "code": "001309", - "name": "德明利", - "price": 617.43, - "analysis": { - "stop_loss": 598.91, - "take_profit": 682.35, - "entry_low": 605.08, - "entry_high": 629.78, - "action": "盈利持有 | 损598.91 | 盈682.35 | 买605.08~629.78", - "tech_snapshot": "形态:光头光脚阴线/neutral 量价:主动买盘占优 强撑:583.56 弱撑:601.91 弱压:648.48 强压:682.35", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 3.51, - "action_note": "", - "timing_signal": "neutral" - }, - "strategy_action": "盈利持有", - "stop_loss": 795.0, - "entry_low": 820.0, - "entry_high": 870.0, - "take_profit": 950.0, - "current_signal": "关注" - }, - { - "code": "06160", - "name": "百济神州", - "price": 158.9, - "prev_close": 163.1, - "market": "HK", - "analysis": { - "stop_loss": 152.78, - "take_profit": 170.7, - "entry_low": 154.35, - "entry_high": 159.95, - "action": "盈利持有 | 损152.78 | 盈170.7 | 买154.35~159.95 | 信号:接近支撑位,关注", - "tech_snapshot": "形态:锤子线/T字线/neutral 量价:数据不足 强撑:143.3 弱撑:155.0 弱压:159.5 强压:170.7", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 2.8, - "action_note": "", - "timing_signal": "接近支撑位,关注" - } - }, - { - "code": "688411", - "name": "海博思创", - "price": 264.66, - "analysis": { - "stop_loss": 239.18, - "take_profit": 264.86, - "entry_low": 241.3, - "entry_high": 259.89, - "action": "盈利持有 | 止损239.18 | 目标264.86 | 买入区241.3~259.89", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:买卖均衡 强撑:239.18 弱撑:241.3 弱压:259.89 强压:264.86", - "reassessed_at": "2026-06-12 10:02", - "status": "updated", - "buy_low": 241.3, - "buy_high": 277.96, - "tech_levels": { - "strong_support": 235.42, - "weak_support": 241.3, - "weak_resist": 277.96, - "strong_resist": 286.68 - } - } - }, - { - "code": "000700", - "name": "模塑科技", - "price": 14.4, - "analysis": { - "stop_loss": 13.97, - "take_profit": 15.27, - "entry_low": 14.11, - "entry_high": 14.49, - "action": "盈利持有 | 损13.97 | 盈15.27 | 买14.11~14.49", - "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动买盘占优 强撑:13.75 弱撑:14.08 弱压:14.84 强压:15.27", - "reassessed_at": "2026-06-12 16:00", - "status": "updated", - "rr_ratio": 2.02, - "action_note": "", - "timing_signal": "neutral" - } - }, - { - "code": "301308", - "name": "江波龙", - "added_at": "2026-07-05", - "source": "position_analyst", - "currency": "CNY", - "strategy_action": "盈利持有", - "stop_loss": 590.45, - "buy_zone_low": 618.38, - "buy_zone_high": 630.83, - "take_profit": 691.4, - "sector": "半导体/存储芯片" - }, - { - "code": "000850", - "name": "华茂股份", - "price": 3.81, - "analysis": { - "buy_low": 3.7, - "buy_high": 3.9, - "position_recommend": "2%", - "reason": "纺织是壳核心是金融资产, PB0.78破净提供安全垫, 7/16股东会催化", - "updated_at": "2026-07-06", - "tech_levels": { - "strong_support": 3.57, - "weak_support": 3.8, - "weak_resist": 4.04, - "strong_resist": 4.16 - }, - "stop_loss": 3.5, - "take_profit": 4.3, - "entry_low": 3.7, - "entry_high": 3.9, - "action": "3.70~3.90区间可建仓1~2%", - "tech_snapshot": "形态:弱势回调 量价:利好出尽缩量回调 强撑:3.57 弱撑:3.80 弱压:4.04 强压:4.16", - "reassessed_at": "2026-07-07" - }, - "sector": "纺织/金融投资" - } - ], - "updated_at": "2026-07-06" -} \ No newline at end of file diff --git a/data/watchlist.json.bak2 b/data/watchlist.json.bak2 index 31b5bcdb..1e7d121f 100644 --- a/data/watchlist.json.bak2 +++ b/data/watchlist.json.bak2 @@ -3,572 +3,643 @@ { "code": "02388", "name": "中银香港", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 39.47, - "take_profit": 47.08, - "entry_low": 41.53, - "entry_high": 43.23, - "action": null, + "price": 48.26, "analysis": { - "stop_loss": 39.47, - "take_profit": 47.08, - "entry_low": 41.53, - "entry_high": 43.23, - "action": "盈利持有 | 目标47.08 | 止损39.47 | 买入区41.53~43.23 | 信号:关注", - "tech_snapshot": "形态:带下影阴线/neutral 量价:数据不足 强撑:38.12 弱撑:41.6 弱压:43.6 强压:47.08 | MA5=44.88 MA10=46.42 MA20=47.03 MA60=45.88", - "multi_tf_context": "多周期看多 | MA20=47.03 | MA60=45.88 | 长撑:日强支撑=41.82 | 长压:日强阻=49.36", - "reassessed_at": "2026-07-01 09:01", + "buy_low": 47.12, + "buy_high": 48.48, + "position_recommend": "", + "reason": "回调-0.95%至47.80,止损46安全,继续持有观察", + "updated_at": "2026-06-02T10:46:11.973827", + "tech_levels": { + "strong_support": 44.14, + "weak_support": 47.12, + "weak_resist": 48.48, + "strong_resist": 51.34 + }, + "stop_loss": 42.87, + "take_profit": 53.59, + "entry_low": 41.01, + "entry_high": 46.6, + "action": "盈利持有,止损42.87 目标53.59", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } + }, + { + "code": "01888", + "name": "建滔积层板", + "price": 65.6, + "analysis": { + "stop_loss": 61.42, + "take_profit": 77.23, + "entry_low": 63.99, + "entry_high": 66.61, + "action": "盈利持有 | 损61.42 | 盈77.23 | 买63.99~66.61", + "tech_snapshot": "形态:光头光脚阴线/neutral 量价:数据不足 强撑:57.53 弱撑:61.42 弱压:71.27 强压:77.23", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 3.7, + "rr_ratio": 3.07, "action_note": "", - "timing_signal": "关注" + "timing_signal": "neutral" }, - "trigger": { - "stop_loss": 39.47, - "entry_zone": "41.53~43.23", - "take_profit_zone": "0~47.08" + "strategy_action": "盈利持有", + "stop_loss": 74.32, + "entry_low": 76.01, + "entry_high": 88.67, + "take_profit": 95.0, + "current_signal": "持有" + }, + { + "code": "01088", + "name": "中国神华", + "price": 45.32, + "analysis": { + "stop_loss": 43.75, + "take_profit": 47.67, + "entry_low": 44.2, + "entry_high": 45.32, + "action": "盈利持有 | ⚠️盈亏比偏低(1:1.9),谨慎买入 | 损43.75 | 盈47.67 | 买44.2~45.32 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:长影星线/neutral 量价:数据不足 强撑:42.64 弱撑:44.71 弱压:45.55 强压:47.67", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 1.9, + "action_note": "⚠️盈亏比偏低(1:1.9),谨慎买入", + "timing_signal": "接近支撑位,关注" }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 38.84, + "entry_low": 36.0, + "entry_high": 42.0, + "take_profit": 46.0 + }, + { + "code": "01211", + "name": "比亚迪股份", + "price": 86.55, + "analysis": { + "buy_low": 84.95, + "buy_high": 87.17, + "position_recommend": null, + "reason": "大涨+5.34%", + "updated_at": "2026-06-02T11:01:41.539019", + "tech_levels": { + "strong_support": 79.79, + "weak_support": 84.95, + "weak_resist": 87.17, + "strong_resist": 91.98 + }, + "stop_loss": 76.64, + "take_profit": 95.79, + "entry_low": 73.3, + "entry_high": 83.3, + "action": "盈利持有,止损76.64 目标95.79", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } }, { "code": "002594", "name": "比亚迪", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 77.31, - "take_profit": 85.52, - "entry_low": 78.11, - "entry_high": 80.59, - "action": null, + "price": 91.6, "analysis": { - "stop_loss": 77.31, - "take_profit": 85.52, - "entry_low": 78.11, - "entry_high": 80.59, - "action": "盈利持有 | 目标85.52 | 止损77.31 | 买入区78.11~80.59 | 信号:观望", - "tech_snapshot": "形态:长影星线/neutral 量价:主动卖盘占优 强撑:73.7 弱撑:78.67 弱压:80.64 强压:85.52 | MA5=99.16 MA10=98.47 MA20=96.75 MA60=96.44", - "multi_tf_context": "震荡/无明显方向 | MA20=96.75 | MA60=96.44 | 长撑:日强支撑=77.6 | 长压:周强阻=105.97", - "reassessed_at": "2026-07-01 09:00", + "stop_loss": 88.85, + "take_profit": 98.63, + "entry_low": 89.77, + "entry_high": 92.76, + "action": "盈利持有 | 损88.85 | 盈98.63 | 买89.77~92.76 | 信号:放量下跌,等企稳再入", + "tech_snapshot": "形态:光头光脚阳线/bullish 量价:主动卖盘占优 强撑:83.13 弱撑:89.8 弱压:92.5 强压:98.63", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 2.44, + "rr_ratio": 2.56, "action_note": "", - "timing_signal": "观望" - }, - "trigger": { - "stop_loss": 77.31, - "entry_zone": "78.11~80.59", - "take_profit_zone": "0~85.52" - }, - "currency": "CNY" + "timing_signal": "放量下跌,等企稳再入" + } }, { "code": "09868", "name": "小鹏汽车-W", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 45.44, - "take_profit": 51.57, - "entry_low": 46.98, - "entry_high": 47.89, - "action": null, + "price": 57.7, "analysis": { - "stop_loss": 45.44, - "take_profit": 51.57, - "entry_low": 46.98, - "entry_high": 47.89, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.6),谨慎买入 | 止损参考45.44 | 买入区46.98~47.89 | 信号:弱势持有", - "tech_snapshot": "形态:带下影阳线/bullish 量价:数据不足 强撑:44.43 弱撑:47.4 弱压:51.54 强压:55.69 | MA5=78.84 MA10=79.64 MA20=78.58 MA60=73.08", - "multi_tf_context": "震荡/无明显方向 | MA20=78.58 | MA60=73.08 | 长撑:日弱支撑=46.22 | 长压:月强阻=84.6", - "reassessed_at": "2026-07-01 09:01", - "status": "manual", - "rr_ratio": 0.18, - "action_note": "⚠️盈亏比偏低(1:1.6),谨慎买入", - "timing_signal": "弱势持有" + "stop_loss": 55.29, + "take_profit": 59.97, + "entry_low": 55.86, + "entry_high": 57.16, + "action": "盈利持有 | ⚠️盈亏比偏低(1:1.7),谨慎买入 | 损55.29 | 盈59.97 | 买55.86~57.16 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:十字星/neutral 量价:数据不足 强撑:54.27 弱撑:56.08 弱压:58.03 强压:59.97", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 1.74, + "action_note": "⚠️盈亏比偏低(1:1.7),谨慎买入", + "timing_signal": "接近支撑位,关注" }, - "trigger": { - "stop_loss": 45.44, - "entry_zone": "46.98~47.89", - "take_profit_zone": "0~51.57" - }, - "currency": "CNY" + "strategy_action": "观望", + "stop_loss": 48.0, + "entry_low": 49.0, + "entry_high": 53.0, + "take_profit": 58.0, + "current_signal": "弱势观望" }, { "code": "688795", "name": "摩尔线程-U", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 695.0, - "take_profit": 769.86, - "entry_low": 702.16, - "entry_high": 724.94, - "action": null, + "price": 610.55, "analysis": { - "stop_loss": 695.0, - "take_profit": 769.86, - "entry_low": 702.16, - "entry_high": 724.94, - "action": "盈利持有 | 目标769.86 | 止损695.0 | 买入区702.16~724.94 | 信号:观望", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:主动卖盘占优 强撑:651.88 弱撑:698.95 弱压:728.41 强压:769.86 | MA5=699.45 MA10=681.68 MA20=654.65 MA60=657.69", - "multi_tf_context": "震荡/无明显方向 | MA20=654.65 | MA60=657.69 | 长撑:MA20=654.65 | 长压:周强阻=755.51", - "reassessed_at": "2026-07-01 09:02", + "stop_loss": 592.23, + "take_profit": 673.56, + "entry_low": 598.34, + "entry_high": 622.76, + "action": "盈利持有 | 损592.23 | 盈673.56 | 买598.34~622.76 | 信号:放量下跌,等企稳再入", + "tech_snapshot": "形态:带上影阴线/neutral 量价:主动卖盘占优 强撑:575.87 弱撑:596.38 弱压:638.88 强压:673.56", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 2.48, + "rr_ratio": 3.44, "action_note": "", - "timing_signal": "观望" + "timing_signal": "放量下跌,等企稳再入" }, - "trigger": { - "stop_loss": 695.0, - "entry_zone": "702.16~724.94", - "take_profit_zone": "0~769.86" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 610.0, + "entry_low": 620.0, + "entry_high": 650.0, + "take_profit": 700.0, + "current_signal": "观望" }, { "code": "688802", "name": "沐曦股份-U", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 803.45, - "take_profit": 932.42, - "entry_low": 811.73, - "entry_high": 844.87, - "action": null, + "price": 694.89, "analysis": { - "stop_loss": 803.45, - "take_profit": 932.42, - "entry_low": 811.73, - "entry_high": 844.87, - "action": "盈利持有 | 目标932.42 | 止损803.45 | 买入区811.73~844.87 | 信号:买入", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:数据不足 强撑:698.42 弱撑:793.55 弱压:849.88 强压:932.42 | MA5=778.59 MA10=765.11 MA20=736.75 MA60=714.48", - "multi_tf_context": "震荡/无明显方向 | MA20=736.75 | MA60=714.48 | 长撑:MA20=736.75 | 长压:周强阻=850.0", - "reassessed_at": "2026-07-01 09:02", + "stop_loss": 674.04, + "take_profit": 766.4, + "entry_low": 680.99, + "entry_high": 708.79, + "action": "盈利持有 | 损674.04 | 盈766.4 | 买680.99~708.79", + "tech_snapshot": "形态:带上影阴线/neutral 量价:数据不足 强撑:655.22 弱撑:678.97 弱压:726.74 强压:766.4", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 3.0, + "rr_ratio": 3.43, "action_note": "", - "timing_signal": "买入" + "timing_signal": "neutral" }, - "trigger": { - "stop_loss": 803.45, - "entry_zone": "811.73~844.87", - "take_profit_zone": "0~932.42" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 700.0, + "entry_low": 710.0, + "entry_high": 740.0, + "take_profit": 790.0, + "current_signal": "观望" }, { "code": "02359", "name": "药明康德", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 142.47, - "take_profit": 169.6, - "entry_low": 150.63, - "entry_high": 156.77, - "action": null, + "price": 126.7, "analysis": { - "stop_loss": 142.47, - "take_profit": 169.6, - "entry_low": 150.63, - "entry_high": 156.77, - "action": "盈利持有 | 目标169.6 | 止损142.47 | 买入区150.63~156.77 | 信号:关注", - "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:数据不足 强撑:139.6 弱撑:150.8 弱压:157.5 强压:169.6 | MA5=147.74 MA10=137.78 MA20=130.76 MA60=130.08", - "multi_tf_context": "震荡/无明显方向 | MA20=130.76 | MA60=130.08 | 长撑:MA20=130.76 | 长压:日强阻=158.4", - "reassessed_at": "2026-07-01 09:01", + "stop_loss": 121.64, + "take_profit": 131.07, + "entry_low": 122.89, + "entry_high": 125.41, + "action": "盈利持有 | ⚠️盈亏比偏低(1:1.5),谨慎买入 | 损121.64 | 盈131.07 | 买122.89~125.41", + "tech_snapshot": "形态:带上影阳线/neutral 量价:数据不足 强撑:118.27 弱撑:121.83 弱压:128.23 强压:131.07", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 3.01, - "action_note": "", - "timing_signal": "关注" + "rr_ratio": 1.51, + "action_note": "⚠️盈亏比偏低(1:1.5),谨慎买入", + "timing_signal": "neutral" }, - "trigger": { - "stop_loss": 142.47, - "entry_zone": "150.63~156.77", - "take_profit_zone": "0~169.6" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 145.0, + "entry_low": 148.0, + "entry_high": 155.0, + "take_profit": 170.0, + "current_signal": "关注" }, { "code": "02628", "name": "中国人寿", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 24.7, - "take_profit": 28.96, - "entry_low": 26.13, - "entry_high": 27.02, - "action": null, + "price": 30.12, "analysis": { - "stop_loss": 24.7, - "take_profit": 28.96, - "entry_low": 26.13, - "entry_high": 27.02, - "action": "盈利持有 | 目标28.96 | 止损24.7 | 买入区26.13~27.02 | 信号:关注", - "tech_snapshot": "形态:小阴线/bearish 量价:数据不足 强撑:24.48 弱撑:26.26 弱压:27.12 强压:28.96 | MA5=29.79 MA10=30.92 MA20=31.8 MA60=31.12", - "multi_tf_context": "震荡/无明显方向 | MA20=31.8 | MA60=31.12 | 长撑:日强支撑=26.32 | 长压:月强阻=36.16", - "reassessed_at": "2026-07-01 09:01", - "status": "updated", - "rr_ratio": 2.47, - "action_note": "", - "timing_signal": "关注" - }, - "trigger": { - "stop_loss": 24.7, - "entry_zone": "26.13~27.02", - "take_profit_zone": "0~28.96" - }, - "currency": "CNY" + "stop_loss": 28.26, + "take_profit": 31.6, + "entry_low": 29.3, + "entry_high": 29.6, + "action": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 损28.26 | 盈31.6 | 买29.3~29.6 | 信号:阳线企稳,可买入", + "tech_snapshot": "形态:光头光脚阳线/bullish 量价:数据不足 强撑:27.44 弱撑:28.26 弱压:30.74 强压:31.6", + "reassessed_at": "2026-06-12 16:00", + "status": "review", + "rr_ratio": 1.04, + "action_note": "⚠️盈亏比不足1:1.5,不建议买入", + "timing_signal": "阳线企稳,可买入" + } }, { "code": "00968", "name": "信义光能", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 1.94, - "take_profit": 2.21, - "entry_low": 2.02, - "entry_high": 2.05, - "action": null, + "price": 2.41, "analysis": { - "stop_loss": 1.94, - "take_profit": 2.21, - "entry_low": 2.02, - "entry_high": 2.05, - "action": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 目标2.21 | 止损1.94 | 买入区2.02~2.05 | 信号:弱势持有", - "tech_snapshot": "形态:光头光脚阳线/bullish 量价:数据不足 强撑:1.89 弱撑:2.02 弱压:2.09 强压:2.21 | MA5=3.11 MA10=3.14 MA20=3.22 MA60=3.33", - "multi_tf_context": "震荡/无明显方向 | MA20=3.22 | MA60=3.33 | 长撑:日弱支撑=1.94 | 长压:日强阻=2.73", - "reassessed_at": "2026-07-01 09:00", - "status": "review", - "rr_ratio": 1.25, - "action_note": "⚠️盈亏比不足1:1.5,不建议买入", - "timing_signal": "弱势持有" + "stop_loss": 2.34, + "take_profit": 2.63, + "entry_low": 2.36, + "entry_high": 2.46, + "action": "盈利持有 | 损2.34 | 盈2.63 | 买2.36~2.46 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:带上影阴线/bearish 量价:数据不足 强撑:2.24 弱撑:2.37 弱压:2.47 强压:2.63", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 3.14, + "action_note": "", + "timing_signal": "接近支撑位,关注" }, - "trigger": { - "stop_loss": 1.94, - "entry_zone": "2.02~2.05", - "take_profit_zone": "0~2.21" + "strategy_action": "弱势持有", + "stop_loss": 1.92, + "entry_low": 1.96, + "entry_high": 2.15, + "take_profit": 2.4, + "current_signal": "弱势持有" + }, + { + "code": "06869", + "name": "长飞光纤", + "price": 226.6, + "analysis": { + "stop_loss": 219.27, + "take_profit": 254.33, + "entry_low": 224.81, + "entry_high": 233.29, + "action": "盈利持有 | 损219.27 | 盈254.33 | 买224.81~233.29", + "tech_snapshot": "形态:带下影阴线/neutral 量价:数据不足 强撑:209.13 弱撑:219.27 弱压:241.87 强压:254.33", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 2.46, + "action_note": "", + "timing_signal": "neutral" }, - "currency": "CNY" + "strategy_action": "深套持有", + "stop_loss": 195.46, + "entry_low": 181.08, + "entry_high": 211.26, + "take_profit": 230.0 }, { "code": "02318", "name": "中国平安", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 49.36, - "take_profit": 53.76, - "entry_low": 50.08, - "entry_high": 51.12, - "action": null, + "price": 57.15, "analysis": { - "stop_loss": 49.36, - "take_profit": 53.76, - "entry_low": 50.08, - "entry_high": 51.12, - "action": "盈利持有 | ⚠️盈亏比偏低(1:2.0),谨慎买入 | 止损参考49.36 | 买入区50.08~51.12 | 信号:关注", - "tech_snapshot": "形态:光头光脚阴线/neutral 量价:数据不足 强撑:48.87 弱撑:50.33 弱压:52.18 强压:54.07 | MA5=68.73 MA10=69.1 MA20=68.89 MA60=67.28", - "multi_tf_context": "震荡/无明显方向 | MA20=68.89 | MA60=67.28 | 长撑:日强支撑=50.0 | 长压:周强阻=66.5", - "reassessed_at": "2026-07-01 09:01", - "status": "manual", - "rr_ratio": 1.6, - "action_note": "⚠️盈亏比偏低(1:2.0),谨慎买入", - "timing_signal": "关注" + "stop_loss": 55.39, + "take_profit": 60.16, + "entry_low": 55.96, + "entry_high": 57.3, + "action": "盈利持有 | ⚠️盈亏比偏低(1:1.8),谨慎买入 | 损55.39 | 盈60.16 | 买55.96~57.3 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:数据不足 强撑:54.45 弱撑:56.15 弱压:58.25 强压:60.16", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 1.79, + "action_note": "⚠️盈亏比偏低(1:1.8),谨慎买入", + "timing_signal": "接近支撑位,关注" }, - "trigger": { - "stop_loss": 49.36, - "entry_zone": "50.08~51.12", - "take_profit_zone": "0~53.76" - }, - "currency": "CNY" + "strategy_action": "弱势持有", + "stop_loss": 49.5, + "entry_low": 50.0, + "entry_high": 53.0, + "take_profit": 58.0, + "current_signal": "弱势观望" + }, + { + "code": "688639", + "name": "华恒生物", + "price": 21.2, + "analysis": { + "stop_loss": 20.56, + "take_profit": 22.66, + "entry_low": 20.78, + "entry_high": 21.4, + "action": "盈利持有 | 损20.56 | 盈22.66 | 买20.78~21.4 | 信号:量价齐升,可买入", + "tech_snapshot": "形态:带下影阳线/bullish 量价:主动买盘占优 强撑:19.26 弱撑:20.6 弱压:21.46 强压:22.66", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 2.28, + "action_note": "", + "timing_signal": "量价齐升,可买入" + } + }, + { + "code": "300548", + "name": "博创科技", + "price": 224.03, + "analysis": { + "buy_low": 224.25, + "buy_high": 234.13, + "position_recommend": "2%", + "reason": "+7.26%站稳230确认可追,回调至230~235建仓。", + "updated_at": "2026-06-02T14:42:56.675237", + "tech_levels": { + "strong_support": 217.39, + "weak_support": 224.25, + "weak_resist": 234.13, + "strong_resist": 240.34 + }, + "stop_loss": 211.19, + "take_profit": 263.98, + "entry_low": 225.0, + "entry_high": 229.55, + "action": "盈利持有,止损211.19 目标263.98", + "tech_snapshot": "形态:光头光脚阴线/neutral 量价:主动买盘占优 强撑:227.53 弱撑:225.0 弱压:231.57 强压:233.59", + "reassessed_at": "2026-06-12 09:30" + } }, { "code": "300124", "name": "汇川技术", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 64.34, - "take_profit": 71.31, - "entry_low": 65.0, - "entry_high": 67.13, - "action": null, + "price": 67.16, "analysis": { - "stop_loss": 64.34, - "take_profit": 71.31, - "entry_low": 65.0, - "entry_high": 67.13, - "action": "盈利持有 | 目标71.31 | 止损64.34 | 买入区65.0~67.13 | 信号:观望", - "tech_snapshot": "形态:锤子线/T字线/neutral 量价:主动卖盘占优 强撑:60.69 弱撑:64.72 弱压:67.61 强压:71.31 | MA5=74.88 MA10=76.52 MA20=77.68 MA60=73.65", - "multi_tf_context": "震荡/无明显方向 | MA20=77.68 | MA60=73.65 | 长撑:日弱支撑=63.13 | 长压:周强阻=83.55", - "reassessed_at": "2026-07-01 09:01", + "stop_loss": 65.15, + "take_profit": 73.7, + "entry_low": 65.82, + "entry_high": 68.5, + "action": "盈利持有 | 损65.15 | 盈73.7 | 买65.82~68.5 | 信号:放量下跌,等企稳再入", + "tech_snapshot": "形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:62.95 弱撑:65.99 弱压:69.49 强压:73.7", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 2.5, + "rr_ratio": 3.25, "action_note": "", - "timing_signal": "观望" + "timing_signal": "放量下跌,等企稳再入" }, - "trigger": { - "stop_loss": 64.34, - "entry_zone": "65.0~67.13", - "take_profit_zone": "0~71.31" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 67.5, + "entry_low": 68.5, + "entry_high": 72.0, + "take_profit": 78.0, + "current_signal": "关注" }, { "code": "01070", "name": "TCL电子", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 12.5, - "take_profit": 13.72, - "entry_low": 12.63, - "entry_high": 12.77, - "action": null, + "price": 14.04, "analysis": { - "stop_loss": 12.5, - "take_profit": 13.72, - "entry_low": 12.63, - "entry_high": 12.77, - "action": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 目标13.72 | 止损12.5 | 买入区12.63~12.77", - "tech_snapshot": "形态:长影星线/neutral 量价:数据不足 强撑:12.08 弱撑:12.58 弱压:13.21 强压:13.72 | MA5=12.84 MA10=13.3 MA20=13.67 MA60=14.33", - "multi_tf_context": "多周期分化 | MA20=13.67 | MA60=14.33 | 长撑:日弱支撑=12.41 | 长压:周强阻=16.19", - "reassessed_at": "2026-07-01 09:00", - "status": "review", - "rr_ratio": 1.09, - "action_note": "⚠️盈亏比不足1:1.5,不建议买入", - "timing_signal": "信号不充分" - }, - "trigger": { - "stop_loss": 12.5, - "entry_zone": "12.63~12.77", - "take_profit_zone": "0~13.72" - }, - "currency": "CNY" + "stop_loss": 13.59, + "take_profit": 15.3, + "entry_low": 13.73, + "entry_high": 14.27, + "action": "盈利持有 | 损13.59 | 盈15.3 | 买13.73~14.27 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:数据不足 强撑:13.06 弱撑:13.75 弱压:14.42 强压:15.3", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 3.07, + "action_note": "", + "timing_signal": "接近支撑位,关注" + } }, { "code": "09988", "name": "阿里巴巴-W", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 89.74, - "take_profit": 100.64, - "entry_low": 90.99, - "entry_high": 93.04, - "action": null, + "price": 110.2, "analysis": { - "stop_loss": 89.74, - "take_profit": 100.64, - "entry_low": 90.99, - "entry_high": 93.04, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.6),谨慎买入 | 目标100.64 | 止损89.74 | 买入区90.99~93.04 | 信号:弱势持有", - "tech_snapshot": "形态:锤子线/T字线/bullish 量价:数据不足 强撑:84.76 弱撑:91.45 弱压:94.1 强压:100.64 | MA5=148.26 MA10=151.36 MA20=158.48 MA60=147.61", - "multi_tf_context": "震荡/无明显方向 | MA20=158.48 | MA60=147.61 | 长撑:日强支撑=88.65 | 长压:月强阻=174.2", - "reassessed_at": "2026-07-01 09:01", - "status": "updated", - "rr_ratio": 1.6, - "action_note": "⚠️盈亏比偏低(1:1.6),谨慎买入", - "timing_signal": "弱势持有" + "buy_low": 107.4, + "buy_high": 111.4, + "position_recommend": null, + "reason": "大涨+5.86%可关注", + "updated_at": "2026-06-02T11:01:41.539017", + "tech_levels": { + "strong_support": 102.72, + "weak_support": 107.4, + "weak_resist": 111.4, + "strong_resist": 117.48 + }, + "stop_loss": 99.18, + "take_profit": 123.97, + "entry_low": 94.86, + "entry_high": 107.8, + "action": "盈利持有,止损99.18 目标123.97", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } + }, + { + "code": "00700", + "name": "腾讯控股", + "price": 463.6, + "analysis": { + "buy_low": 457.2, + "buy_high": 467.93, + "position_recommend": null, + "reason": "暴涨+7.75%逼近止盈470", + "updated_at": "2026-06-02T11:01:41.539014", + "tech_levels": { + "strong_support": 440.4, + "weak_support": 457.2, + "weak_resist": 467.93, + "strong_resist": 486.94 + }, + "stop_loss": 432.58, + "take_profit": 540.73, + "entry_low": 413.78, + "entry_high": 470.2, + "action": "盈利持有,止损432.58 目标540.73", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } + }, + { + "code": "02202", + "name": "万科企业", + "price": 2.64, + "analysis": { + "buy_low": 2.62, + "buy_high": 2.69, + "position_recommend": "", + "reason": "万科企业2.83微涨+1.8%,深套持有", + "updated_at": "2026-06-02T10:46:11.973869", + "tech_levels": { + "strong_support": 2.47, + "weak_support": 2.62, + "weak_resist": 2.69, + "strong_resist": 2.83 + }, + "stop_loss": 2.42, + "take_profit": 3.02, + "entry_low": 2.31, + "entry_high": 2.63, + "action": "盈利持有,止损2.42 目标3.02", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } + }, + { + "code": "688981", + "name": "中芯国际", + "price": 124.88, + "analysis": { + "buy_low": 124.58, + "buy_high": 132.34, + "position_recommend": "0%", + "reason": "浮盈+0.0%,止损112.53 目标137.0", + "updated_at": "2026-06-09T10:46:18", + "tech_levels": { + "strong_support": 121.97, + "weak_support": 124.58, + "weak_resist": 132.34, + "strong_resist": 137.49 + }, + "stop_loss": 121.46, + "take_profit": 151.82, + "entry_low": 127.4, + "entry_high": 132.02, + "action": "盈利持有,止损121.46 目标151.82", + "tech_snapshot": "形态:倒锤头/bullish 量价:买卖均衡 强撑:130.86 弱撑:127.4 弱压:132.8 强压:133.58", + "reassessed_at": "2026-06-12 09:30" }, - "trigger": { - "stop_loss": 89.74, - "entry_zone": "90.99~93.04", - "take_profit_zone": "0~100.64" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 132.0, + "entry_low": 135.0, + "entry_high": 142.0, + "take_profit": 155.0, + "current_signal": "关注" + }, + { + "code": "01478", + "name": "丘钛科技", + "price": 8.13, + "analysis": { + "buy_low": 7.98, + "buy_high": 8.51, + "position_recommend": "", + "reason": "丘钛科技10.07跌-1.66%,深套-26.48%暂持", + "updated_at": "2026-06-02T10:46:11.973872", + "tech_levels": { + "strong_support": 7.92, + "weak_support": 7.98, + "weak_resist": 8.51, + "strong_resist": 8.74 + }, + "stop_loss": 7.47, + "take_profit": 9.34, + "entry_low": 7.15, + "entry_high": 8.12, + "action": "盈利持有,止损7.47 目标9.34", + "tech_snapshot": "形态:unknown/neutral 量价:数据不足 强撑:None 弱撑:None 弱压:None 强压:None", + "reassessed_at": "2026-06-12 09:30" + } }, { "code": "001309", "name": "德明利", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 810.0, - "take_profit": 1153.26, - "entry_low": 873.18, - "entry_high": 908.82, - "action": null, + "price": 617.43, "analysis": { - "stop_loss": 810.0, - "take_profit": 1153.26, - "entry_low": 873.18, - "entry_high": 908.82, - "action": "盈利持有 | 止损参考810.0 | 买入区873.18~908.82", - "tech_snapshot": "形态:锤子线/T字线/neutral 量价:主动买盘占优 强撑:873.0 弱撑:907.94 弱压:970.0 强压:1040.65 | MA5=912.98 MA10=813.43 MA20=720.19 MA60=612.25", - "multi_tf_context": "多周期看多 | MA20=720.19 | MA60=612.25 | 长撑:MA20=720.19 | 长压:日强阻=980.0", - "reassessed_at": "2026-07-01 09:00", - "status": "manual", - "rr_ratio": 1.58, + "stop_loss": 598.91, + "take_profit": 682.35, + "entry_low": 605.08, + "entry_high": 629.78, + "action": "盈利持有 | 损598.91 | 盈682.35 | 买605.08~629.78", + "tech_snapshot": "形态:光头光脚阴线/neutral 量价:主动买盘占优 强撑:583.56 弱撑:601.91 弱压:648.48 强压:682.35", + "reassessed_at": "2026-06-12 16:00", + "status": "updated", + "rr_ratio": 3.51, "action_note": "", "timing_signal": "neutral" }, - "trigger": { - "stop_loss": 810.0, - "entry_zone": "873.18~908.82", - "take_profit_zone": "0~1153.26" - }, - "currency": "CNY" + "strategy_action": "盈利持有", + "stop_loss": 795.0, + "entry_low": 820.0, + "entry_high": 870.0, + "take_profit": 950.0, + "current_signal": "关注" }, { "code": "06160", "name": "百济神州", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 164.16, - "take_profit": 183.83, - "entry_low": 166.99, - "entry_high": 172.03, - "action": null, + "price": 158.9, + "prev_close": 163.1, + "market": "HK", "analysis": { - "stop_loss": 164.16, - "take_profit": 183.83, - "entry_low": 166.99, - "entry_high": 172.03, - "action": "盈利持有 | 目标183.83 | 止损164.16 | 买入区166.99~172.03", - "tech_snapshot": "形态:带上影阴线/bearish 量价:数据不足 强撑:159.23 弱撑:166.87 弱压:175.07 强压:183.83 | MA5=189.84 MA10=195.82 MA20=199.25 MA60=193.25", - "multi_tf_context": "震荡/无明显方向 | MA20=199.25 | MA60=193.25 | 长撑:日弱支撑=154.5 | 长压:月强阻=222.0", - "reassessed_at": "2026-07-01 09:01", + "stop_loss": 152.78, + "take_profit": 170.7, + "entry_low": 154.35, + "entry_high": 159.95, + "action": "盈利持有 | 损152.78 | 盈170.7 | 买154.35~159.95 | 信号:接近支撑位,关注", + "tech_snapshot": "形态:锤子线/T字线/neutral 量价:数据不足 强撑:143.3 弱撑:155.0 弱压:159.5 强压:170.7", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 2.15, + "rr_ratio": 2.8, "action_note": "", - "timing_signal": "信号不充分" - }, - "trigger": { - "stop_loss": 164.16, - "entry_zone": "166.99~172.03", - "take_profit_zone": "0~183.83" - }, - "currency": "CNY" + "timing_signal": "接近支撑位,关注" + } }, { - "code": "000711", - "name": "ST京蓝", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 4.25, - "take_profit": 4.99, - "entry_low": 4.45, - "entry_high": 4.55, - "action": null, + "code": "688411", + "name": "海博思创", + "price": 264.66, "analysis": { - "stop_loss": 4.25, - "take_profit": 4.99, - "entry_low": 4.45, - "entry_high": 4.55, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.6),谨慎买入 | 目标4.99 | 止损4.25 | 买入区4.45~4.55 | 信号:观望", - "tech_snapshot": "形态:光头光脚阳线/neutral 量价:主动卖盘占优 强撑:4.1 弱撑:4.25 弱压:4.69 强压:4.99 | MA5=4.65 MA10=5.0 MA20=5.38 MA60=5.11", - "multi_tf_context": "震荡/无明显方向 | MA20=5.38 | MA60=5.11 | 长撑:日弱支撑=4.32 | 长压:日弱阻=6.28", - "reassessed_at": "2026-07-01 09:00", + "stop_loss": 239.18, + "take_profit": 264.86, + "entry_low": 241.3, + "entry_high": 259.89, + "action": "盈利持有 | 止损239.18 | 目标264.86 | 买入区241.3~259.89", + "tech_snapshot": "形态:光头光脚阳线/bullish 量价:买卖均衡 强撑:239.18 弱撑:241.3 弱压:259.89 强压:264.86", + "reassessed_at": "2026-06-12 10:02", "status": "updated", - "rr_ratio": 1.55, - "action_note": "⚠️盈亏比偏低(1:1.6),谨慎买入", - "timing_signal": "观望" - }, - "trigger": { - "stop_loss": 4.25, - "entry_zone": "4.45~4.55", - "take_profit_zone": "0~4.99" - }, - "currency": "CNY" + "buy_low": 241.3, + "buy_high": 277.96, + "tech_levels": { + "strong_support": 235.42, + "weak_support": 241.3, + "weak_resist": 277.96, + "strong_resist": 286.68 + } + } }, { - "code": "688630", - "name": "芯碁微装", - "added_at": "2026-06-21 02:51:30", - "stop_loss": 532.91, - "take_profit": 595.43, - "entry_low": 538.4, - "entry_high": 554.48, - "action": null, + "code": "000700", + "name": "模塑科技", + "price": 14.4, "analysis": { - "stop_loss": 532.91, - "take_profit": 595.43, - "entry_low": 538.4, - "entry_high": 554.48, - "action": "盈利持有 | 目标595.43 | 止损532.91 | 买入区538.4~554.48", - "tech_snapshot": "形态:锤子线/T字线/neutral 量价:主动买盘占优 强撑:486.75 弱撑:527.18 弱压:563.3 强压:595.43 | MA5=538.96 MA10=508.73 MA20=449.83 MA60=323.87", - "multi_tf_context": "多周期看多 | MA20=449.83 | MA60=323.87 | 长撑:MA20=449.83 | 长压:日强阻=563.89", - "reassessed_at": "2026-07-01 09:01", + "stop_loss": 13.97, + "take_profit": 15.27, + "entry_low": 14.11, + "entry_high": 14.49, + "action": "盈利持有 | 损13.97 | 盈15.27 | 买14.11~14.49", + "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动买盘占优 强撑:13.75 弱撑:14.08 弱压:14.84 强压:15.27", + "reassessed_at": "2026-06-12 16:00", "status": "updated", - "rr_ratio": 2.07, + "rr_ratio": 2.02, "action_note": "", - "timing_signal": "信号不充分" - }, - "trigger": { - "stop_loss": 532.91, - "entry_zone": "538.4~554.48", - "take_profit_zone": "0~595.43" - }, - "currency": "CNY" + "timing_signal": "neutral" + } }, { - "code": "000657", - "name": "中钨高新", - "added_at": "2026-06-20T15:44:00", - "stop_loss": 93.06, - "take_profit": 105.13, - "entry_low": 93.81, - "entry_high": 97.63, - "action": null, - "analysis": { - "stop_loss": 93.06, - "take_profit": 105.13, - "entry_low": 93.81, - "entry_high": 97.63, - "action": "盈利持有 | 目标105.13 | 止损93.06 | 买入区93.81~97.63", - "tech_snapshot": "形态:带上影阳线/neutral 量价:主动买盘占优 强撑:86.01 弱撑:93.06 弱压:97.84 强压:105.13 | MA5=99.31 MA10=98.79 MA20=86.3 MA60=66.5", - "multi_tf_context": "多周期看多 | MA20=86.3 | MA60=66.5 | 长撑:MA20=86.3 | 长压:日强阻=113.99", - "reassessed_at": "2026-07-01 09:00", - "status": "updated", - "rr_ratio": 3.54, - "action_note": "", - "timing_signal": "信号不充分" - }, - "trigger": { - "stop_loss": 93.06, - "entry_zone": "93.81~97.63", - "take_profit_zone": "0~105.13" - }, - "currency": "CNY" + "code": "301308", + "name": "江波龙", + "added_at": "2026-07-05", + "source": "position_analyst", + "currency": "CNY", + "strategy_action": "盈利持有", + "stop_loss": 590.45, + "buy_zone_low": 618.38, + "buy_zone_high": 630.83, + "take_profit": 691.4, + "sector": "半导体/存储芯片" }, { - "code": "600519", - "name": "贵州茅台", - "added_at": "2026-06-22 09:22:26", - "stop_loss": 1149.93, - "take_profit": 1249.71, - "entry_low": 1161.78, - "entry_high": 1189.84, - "action": null, + "code": "000850", + "name": "华茂股份", + "price": 3.81, "analysis": { - "stop_loss": 1149.93, - "take_profit": 1249.71, - "entry_low": 1161.78, - "entry_high": 1189.84, - "action": "盈利持有 | ⚠️盈亏比偏低(1:1.8),谨慎买入 | 目标1249.71 | 止损1149.93 | 买入区1161.78~1189.84 | 信号:关注", - "tech_snapshot": "形态:长影星线/neutral 量价:主动买盘占优 强撑:1121.73 弱撑:1175.77 弱压:1195.44 强压:1249.71 | MA5=1373.78 MA10=1383.48 MA20=1362.71 MA60=1396.98", - "multi_tf_context": "多周期看多 | MA20=1362.71 | MA60=1396.98 | 长撑:日强支撑=1151.01 | 长压:月强阻=1539.98", - "reassessed_at": "2026-07-01 09:01", - "status": "updated", - "rr_ratio": 1.81, - "action_note": "⚠️盈亏比偏低(1:1.8),谨慎买入", - "timing_signal": "关注" + "buy_low": 3.7, + "buy_high": 3.9, + "position_recommend": "2%", + "reason": "纺织是壳核心是金融资产, PB0.78破净提供安全垫, 7/16股东会催化", + "updated_at": "2026-07-06", + "tech_levels": { + "strong_support": 3.57, + "weak_support": 3.8, + "weak_resist": 4.04, + "strong_resist": 4.16 + }, + "stop_loss": 3.5, + "take_profit": 4.3, + "entry_low": 3.7, + "entry_high": 3.9, + "action": "3.70~3.90区间可建仓1~2%", + "tech_snapshot": "形态:弱势回调 量价:利好出尽缩量回调 强撑:3.57 弱撑:3.80 弱压:4.04 强压:4.16", + "reassessed_at": "2026-07-07" }, - "trigger": { - "stop_loss": 1149.93, - "entry_zone": "1161.78~1189.84", - "take_profit_zone": "0~1249.71" - }, - "currency": "CNY" - }, - { - "code": "601318", - "name": "中国平安", - "added_at": "2026-06-22 11:48:54", - "stop_loss": 46.31, - "take_profit": 50.6, - "entry_low": 46.79, - "entry_high": 48.03, - "action": null, - "analysis": { - "stop_loss": 46.31, - "take_profit": 50.6, - "entry_low": 46.79, - "entry_high": 48.03, - "action": "盈利持有 | 目标50.6 | 止损46.31 | 买入区46.79~48.03 | 信号:观望", - "tech_snapshot": "形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:45.42 弱撑:47.08 弱压:48.67 强压:50.6 | MA5=68.37 MA10=68.78 MA20=66.54 MA60=62.97", - "multi_tf_context": "多周期看多 | MA20=66.54 | MA60=62.97 | 长撑:日弱支撑=47.2 | 长压:周强阻=59.36", - "reassessed_at": "2026-07-01 09:01", - "status": "updated", - "rr_ratio": 2.0, - "action_note": "", - "timing_signal": "观望" - }, - "trigger": { - "stop_loss": 46.31, - "entry_zone": "46.79~48.03", - "take_profit_zone": "0~50.6" - }, - "currency": "CNY" + "sector": "纺织/金融投资" } - ] + ], + "updated_at": "2026-07-06" } \ No newline at end of file diff --git a/data/xiaoguo_insights.json b/data/xiaoguo_insights.json index fcfa633f..0211ee4b 100644 --- a/data/xiaoguo_insights.json +++ b/data/xiaoguo_insights.json @@ -1,62 +1,54 @@ { - "timestamp": "2026-07-06 16:10", + "timestamp": "2026-07-07 16:14", "source": "xiaoguo_fallback", "stocks": [ { "code": "300308", "name": "中际旭创", "sentiment": "positive", - "confidence": 0.70, - "reason": "公司今日在互动易澄清两大市场传闻:康宁玻璃桥技术非光模块替代方案、上游炫光片封锁传言不实。主动辟谣有助于缓解FUD,稳定股价", - "priority": "距止损2.49%" - }, - { - "code": "300035", - "name": "中科电气", - "sentiment": "neutral", - "confidence": 0.50, - "reason": "今日无公司重大新闻,仅有主力资金流向统计提及。消息面平淡,股价-1.82%正常波动", - "priority": "距止损3.70%" - }, - { - "code": "01088", - "name": "中国神华", - "sentiment": "positive", - "confidence": 0.65, - "reason": "7月2日公告定州三期、沧东三期5号机组正式投运,产能扩张利好。南向资金持续大比例持有。今日+1.65%,走势稳健", - "priority": "距止损4.69%" + "confidence": 0.72, + "reason": "今日成交额超300亿元,市场交投活跃。公司主动辟谣物料封锁传言,澄清康宁玻璃桥技术非光模块替代方案。利好消除不确定性,尾盘+2.09%走强", + "priority": "距止损2.8%" }, { "code": "688981", "name": "中芯国际", "sentiment": "neutral", - "confidence": 0.55, - "reason": "今日无公司层面新消息(最新公开新闻为6月底)。但当日涨幅+5.2%创阶段新高,半导体板块情绪偏暖。机构持仓稳定", - "priority": "距止损4.72%" + "confidence": 0.58, + "reason": "消息面多空交织:牵头半导体企业获超10亿元股权融资(利好),但中芯国际概念板块近期下跌5.66%。今日股价+0.99%企稳,距止损仅3.1%需关注", + "priority": "距止损3.1%" }, { "code": "000700", "name": "模塑科技", "sentiment": "negative", - "confidence": 0.60, - "reason": "7月2日公司公告明确机器人外覆盖件订单占比不足0.1%,主动降温机器人炒作预期。前期涨幅过大,当前买入区需警惕回调风险", - "priority": "买入区15.5~18.08" + "confidence": 0.65, + "reason": "公司公告机器人外覆盖件订单金额占比不足0.1%,主动降温概念炒作预期。股价已跌破止损16.70元至16.15元,距止损3.4%需警惕进一步下行", + "priority": "距止损3.4%" + }, + { + "code": "300548", + "name": "长芯博创", + "sentiment": "positive", + "confidence": 0.68, + "reason": "发布2025年度权益分派方案(每10股派2.90元),股息回报利好。此前收购子公司少数股权完成,基本面改善。股价-1.52%属正常回调,距止损5.0%", + "priority": "距止损5.0%" }, { "code": "01888", "name": "建滔积层板", "sentiment": "negative", - "confidence": 0.85, - "reason": "今日港股PCB概念大幅波动,建滔积层板从早盘+5.39%急转暴跌至-12.37%,日振幅超17%。南向资金此前持续加仓,但今日出现恐慌性抛售", - "priority": "买入区67.27~78.48" + "confidence": 0.70, + "reason": "今日PCB概念反弹+公司再发涨价函,但昨日暴跌超10%后今日仍跌-5.48%。南向资金此前持续加仓但近期板块恐慌性抛售明显,短期压力未释放完毕", + "priority": "涨跌幅-5.48%" }, { - "code": "300548", - "name": "博创科技", - "sentiment": "neutral", - "confidence": 0.50, - "reason": "今日无公司重大新闻。通信行业整体偏弱(行业-7.36%),F5G概念下跌1.86%,板块环境不佳但公司层面无利空", - "priority": "买入区205.41~239.64" + "code": "06869", + "name": "长飞光纤光缆", + "sentiment": "negative", + "confidence": 0.75, + "reason": "港股算力硬件股持续暴跌,今日再度走低跌超9%。公司已连续多日大幅下挫(昨日-15%,今日-7.70%),距止损195.46已远跌至157.8,属系统性杀跌需警惕", + "priority": "涨跌幅-7.70%" } ] } diff --git a/docs/analyst-knowledge-log.md b/docs/analyst-knowledge-log.md index 468fd8c8..e3bc33d7 100644 --- a/docs/analyst-knowledge-log.md +++ b/docs/analyst-knowledge-log.md @@ -1,21 +1,9 @@ +# analyst-knowledge-log.md -## 2026-07-06 DB锁+市值错误修复 +## [2026-07-08 10:40] 自愈执行器升级处理:KOSDAQ-4.1% HIGH信号 -### 发现的问题 -1. **市值错误(586,191 vs 实际619,880+)** — `mo_data.read_portfolio()` 从 portfolio_summary 表读取 total_mv,该字段可能未及时更新(holdings变化后summary未同步),导致报告输出过时市值。 - -2. **DB并发锁** — mo_data.py 的 _get_db() 和多个脚本使用 bare sqlite3.connect(),无 timeout/WAL/busy_timeout。多个 cron 并发写入时触发 "database is locked" 错误。 - -### 修改内容 - -**mo_data.py:** -- _get_db(): sqlite3.connect 加 timeout=15, PRAGMA journal_mode=WAL, PRAGMA busy_timeout=15000 -- read_portfolio(): 从 holdings 实时计算 total_mv (shares×price,港股×汇率),不信任 summary 存储值 - -**stale_push_wlin.py (2处), multi_timeframe.py (2处), strategy_lifecycle.py (3处), strategy_tree.py (1处):** -- 替换 bare sqlite3.connect() 为 mofin_db.get_conn()(已有 WAL+timeout+退避重试) - -### 效果预期 -- 市值永远从 holdings 实时计算,不再出现 stale summary 导致的错报 -- DB "database is locked" 错误消除(WAL + timeout=15 + busy_timeout=15000) -- 所有高频脚本统一走 get_conn(),连接参数一致 +- **发现**: 自愈执行器上报盘中自检发现宏观风险HIGH(KOSDAQ-4.1% + 中信证券研报) +- **判断**: 系统无故障,已知市场条件延续。KOSDAQ-4.1%是昨日KOSPI暴跌的延续,非新事件。中信证券研报是采集器误报(正常行业研报)。 +- **数据验证**: 中国各指数全面脱钩改善(上证-1.26%→+0.02%,恒生-0.51%→+1.69%,科创50+0.28%→+0.62%)。持仓止损距均>3%。 +- **操作**: 标记已知状态,无需新推送。08:30 LLM深度分析已覆盖并修正。 +- **涉及文件**: macro_risk_state.json(已由consumer更新为processed) diff --git a/mo_alphasift_bridge.py b/mo_alphasift_bridge.py index 75c321b5..fe4989bf 100644 --- a/mo_alphasift_bridge.py +++ b/mo_alphasift_bridge.py @@ -18,8 +18,8 @@ from pathlib import Path DSA_API = "http://127.0.0.1:8001" MOFIN_DATA = Path("/home/hmo/web-dashboard/data") -WATCHLIST_PATH = MOFIN_DATA / "watchlist.json" -PORTFOLIO_PATH = MOFIN_DATA / "portfolio.json" +# 已迁移到 DB — watchlist.json / portfolio.json 不再使用 +# 保留路径仅用于兼容 import,实际数据从 mofin.db 读取 DEFAULT_STRATEGIES = "balanced_alpha,dual_low,quality_value" DEFAULT_MARKET = "cn" @@ -54,14 +54,18 @@ def api(endpoint, method="GET", body=None): return None def get_existing_codes(): + """从 DB 读取已有持仓+自选。JSON 已废弃。""" codes = set() - for path in [WATCHLIST_PATH, PORTFOLIO_PATH]: - data = load_json(path) - if not data: continue - key = "stocks" if "watchlist" in str(path) else "holdings" - for item in data.get(key, []): - c = str(item.get("code", "")).strip() - if c: codes.add(c) + try: + import sqlite3 + db = sqlite3.connect(str(MOFIN_DATA / "mofin.db")) + for row in db.execute("SELECT code FROM watchlist_stocks WHERE is_active=1"): + codes.add(str(row[0]).strip()) + for row in db.execute("SELECT code FROM holdings"): + codes.add(str(row[0]).strip()) + db.close() + except Exception as e: + print(f"WARN: DB读取失败: {e}") return codes @@ -205,12 +209,19 @@ def run_all(strategies_str, market, max_results, dry_run=False): print("\n[DRY RUN] 未写入") return - # 写入 - wl = load_json(WATCHLIST_PATH) or {"stocks": []} - wl["stocks"].extend(new_stocks) - wl["updated_at"] = time_str - save_json(WATCHLIST_PATH, wl) - print(f"\n已写入 {WATCHLIST_PATH}") + # 写入 DB + try: + sys.path.insert(0, str(MOFIN_DATA.parent)) + from mofin_db import get_conn, write_watchlist_stock + conn = get_conn() + for s in new_stocks: + s.setdefault('currency', 'CNY') + write_watchlist_stock(conn, s) + conn.close() + print(f"\n已写入 {len(new_stocks)} 只到 DB watchlist_stocks") + except Exception as e: + print(f"WARN: DB写入失败: {e}") + return # 策略生成 print("\n调用 regenerate_all()...") diff --git a/mo_config.py b/mo_config.py index 1199092c..ca825d21 100644 --- a/mo_config.py +++ b/mo_config.py @@ -10,7 +10,7 @@ mo_config.py — MoFin 统一配置管理(单例模式) 用法: from mo_config import config - portfolio_path = config.data_dir / "portfolio.json" + config.data_dir / "somedata.json" """ import os @@ -28,7 +28,7 @@ class MoConfig: # 项目根目录 project_dir: Path = field(default_factory=lambda: Path(__file__).parent.resolve()) - # 数据目录(portfolio.json, decisions.json 等) + # 数据目录(SQLite 为主) data_dir: Path = field(default_factory=lambda: Path( os.environ.get("MOFIN_DATA_DIR", "/home/hmo/web-dashboard/data") )) @@ -42,22 +42,26 @@ class MoConfig: # Hermes 状态目录 hermes_dir: Path = field(default_factory=lambda: Path.home() / ".hermes") - # ── 关键数据文件路径 ────────────────────────────────────────── - + # ── 关键数据文件路径(已废弃,仅保留为检查逻辑。新代码勿用) ────── + @property def portfolio_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db holdings + portfolio_summary 表。""" return self.data_dir / "portfolio.json" - + @property def decisions_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db holding_strategies 表。""" return self.data_dir / "decisions.json" - + @property def watchlist_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db watchlist_stocks 表。""" return self.data_dir / "watchlist.json" - + @property def price_events_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db price_events 表。""" return self.data_dir / "price_events.json" @property @@ -144,11 +148,9 @@ class MoConfig: if not self.data_dir.exists(): issues.append(f"数据目录不存在: {self.data_dir}") - if not self.portfolio_path.exists(): - issues.append(f"portfolio.json 不存在: {self.portfolio_path}") - - if not self.decisions_path.exists(): - issues.append(f"decisions.json 不存在: {self.decisions_path}") + # 检查 DB 文件(数据源) + if not self._get_db_path().exists(): + issues.append(f"mofin.db 数据库不存在: {self._get_db_path()}") return issues diff --git a/mofin_db.py b/mofin_db.py index ca765a4c..9fd53cc9 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -1097,7 +1097,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool signal_factors_json, time_horizon, decision_type) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now','localtime'), - ?,?,?,?,?,?,?,?,?) + ?,?,?,?,?,?,?,?,?,?) """, ( code, name, data.get('version', 1), data.get('price'), data.get('cost'), @@ -1187,7 +1187,7 @@ def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]: def write_watchlist_stock(conn, stock: dict) -> tuple[bool, str]: - """写入自选股(替代 watchlist.json)""" + """写入自选股(写入 watchlist_stocks 表)""" try: conn.execute(""" INSERT INTO watchlist_stocks (code, name, price, entry_low, entry_high, diff --git a/price_monitor.py b/price_monitor.py index 017a4e15..bd5eb615 100644 --- a/price_monitor.py +++ b/price_monitor.py @@ -11,6 +11,8 @@ import time import sqlite3 from datetime import datetime +# ⚠️ 以下常量已废弃:数据在 mofin.db 的 holding_strategies / holdings / watchlist_stocks 表 +# 保留仅防止 import 报错,新代码勿用 DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" @@ -23,6 +25,7 @@ sys.path.insert(0, "/home/hmo/MoFin") try: from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_live_prices from mo_models import calc_total_mv, calc_total_assets + from mo_data import read_decisions HAS_DB = True except ImportError: HAS_DB = False @@ -307,12 +310,11 @@ def run_once(round_label=""): # === 第一步:一次性刷新所有价格 === refreshed = refresh_data_prices() - # === 第二步:检查触发条件 === + # === 第二步:检查触发条件(纯DB,不读JSON) === try: - with open(DECISIONS_PATH) as f: - dec = json.load(f) - except: - print(f"❌{label} 无法读取decisions.json", file=sys.stderr) + dec = read_decisions() + except Exception as e: + print(f"❌{label} 无法从DB读取决策数据: {e}", file=sys.stderr) return active = [d for d in dec.get("decisions", []) if d.get("status") == "active"] @@ -393,7 +395,7 @@ def run_once(round_label=""): if price == 0: continue - # 从 decisions.json 中读取 analysis 的买入区 + # 从 DB holding_strategies 读取买入区 entry_low = d.get("entry_low", 0) entry_high = d.get("entry_high", 0) if not entry_low or not entry_high: diff --git a/scripts/clean_watchlist.py b/scripts/clean_watchlist.py index be8d04e9..8d344cf6 100644 --- a/scripts/clean_watchlist.py +++ b/scripts/clean_watchlist.py @@ -9,8 +9,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mo_data import read_portfolio, read_decisions, read_watchlist from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy -WL = "/home/hmo/web-dashboard/data/watchlist.json" -DEC = "/home/hmo/web-dashboard/data/decisions.json" +WL = "/home/hmo/web-dashboard/data/watchlist.json" # 路径保留用于历史备份兼容,数据实际走DB +DEC = "/home/hmo/web-dashboard/data/decisions.json" # 同上 holding_codes = set() pf = read_portfolio() @@ -33,8 +33,7 @@ removed = [s for s in stocks if s.get("code") in holding_codes] after = len(new_stocks) wl["stocks"] = new_stocks -# Backup -os.rename(WL, WL + ".bak2") +# Backup — DB 版,不再碰JSON文件 # DB 写入 conn = get_conn() for s in wl.get("stocks", []): @@ -61,8 +60,7 @@ for d in dec.get("decisions", []): dec_changed += 1 if dec_changed: - os.rename(DEC, DEC + ".bak3") - # DB 写入 + # DB 写入(不再碰JSON文件) conn = get_conn() for d in dec.get("decisions", []): write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) diff --git a/scripts/data/format_error_library.json b/scripts/data/format_error_library.json new file mode 100644 index 00000000..5ce275e1 --- /dev/null +++ b/scripts/data/format_error_library.json @@ -0,0 +1,35 @@ +{ + "library_version": "1.0", + "total_records": 2, + "last_updated": "2026-07-08T01:14:29.611447", + "records": [ + { + "task_id": "test-task", + "timestamp": "2026-07-08T01:14:29.582665", + "attempt": 1, + "score": 85, + "errors": [ + "小问题" + ], + "is_failure": false, + "context": { + "code": "000001", + "name": "测试" + } + }, + { + "task_id": "test-task", + "timestamp": "2026-07-08T01:14:29.611356", + "attempt": 2, + "score": 60, + "errors": [ + "缺少字段: stop_loss", + "JSON语法错误" + ], + "is_failure": true, + "context": { + "code": "000001" + } + } + ] +} \ No newline at end of file diff --git a/scripts/data/mofin.db b/scripts/data/mofin.db deleted file mode 120000 index a9de2003..00000000 --- a/scripts/data/mofin.db +++ /dev/null @@ -1 +0,0 @@ -/home/hmo/MoFin/data/mofin.db \ No newline at end of file diff --git a/scripts/fix_portfolio_prices.py b/scripts/fix_portfolio_prices.py index 5c3e2c17..e175866f 100644 --- a/scripts/fix_portfolio_prices.py +++ b/scripts/fix_portfolio_prices.py @@ -1,84 +1,78 @@ #!/usr/bin/env python3 """ -fix_portfolio_prices.py — 一次性修复脚本:从 decisions.json 读取 price 字段 -更新两个 canonical portfolio.json,并重算 total_assets/position_pct。 +fix_portfolio_prices.py — 一次性修复脚本:从 decisions(DB) 读取 price 字段 +更新 holdings 表,并重算 total_assets/position_pct。 背景:strategy_lifecycle.regenerate_all() 在旧版本中会 从 DB query_holdings()(不含 price/change_pct)覆盖写入 portfolio.json,导致 price_monitor 维护的实时价丢失。 该 bug 已在 strategy_lifecycle.py:1790 修复(保留 price 字段), -此脚本用于修复已损坏的 portfolio.json。 +此脚本用于修复已损坏的 portfolio 数据。 用法: - python3 fix_portfolio_prices.py # 修复两个 canonical 文件 + python3 fix_portfolio_prices.py # 修复 DB python3 fix_portfolio_prices.py --check # 只检查,不修改 """ -import json import sys -from pathlib import Path -# Canonical paths -MOFIN_PORTFOLIO = Path("/home/hmo/MoFin/data/portfolio.json") -DASHBOARD_PORTFOLIO = Path("/home/hmo/web-dashboard/data/portfolio.json") -DECISIONS_PATH = Path("/home/hmo/web-dashboard/data/decisions.json") +from mo_data import read_decisions, read_portfolio +from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary -def load_decisions(): - """从 decisions.json 读取 {code: {price, ...}} 映射""" +def build_price_map(): + """从 decisions(DB) 读取 {code: {price, ...}} 映射""" try: - with open(DECISIONS_PATH) as f: - raw = json.load(f) + dec = read_decisions() except Exception as e: - print(f"❌ 无法读取 {DECISIONS_PATH}: {e}", file=sys.stderr) + print(f"❌ 无法读取 decisions(DB): {e}", file=sys.stderr) return {} - + price_map = {} - for d in raw.get("decisions", []): + for d in dec.get("decisions", []): code = d.get("code", "") price = d.get("price", 0) or d.get("current_price", 0) if code and price: price_map[code] = float(price) - print(f" decisions.json: {len(price_map)} 个股票的 price 已加载") + print(f" decisions(DB): {len(price_map)} 个股票的 price 已加载") return price_map -def fix_portfolio(path: Path, price_map: dict, check_only: bool): - """修复一个 portfolio.json 文件""" - if not path.exists(): - print(f"❌ {path} 不存在,跳过") +def fix_portfolio(check_only: bool) -> bool: + """从 DB 读持仓,修复 price,写回 DB""" + try: + pf = read_portfolio() + except Exception as e: + print(f"❌ 无法读取 portfolio(DB): {e}", file=sys.stderr) return False - try: - with open(path) as f: - pf = json.load(f) - except Exception as e: - print(f"❌ 无法读取 {path}: {e}", file=sys.stderr) + holdings = pf.get("holdings", []) + price_map = build_price_map() + if not price_map: + print("❌ decisions 无有效 price 数据,退出") return False changes = 0 errors = 0 - holdings = pf.get("holdings", []) - + for h in holdings: code = h.get("code", "") if not code: continue - + old_price = h.get("price", 0) decision_price = price_map.get(code) - + if decision_price and (not old_price or old_price == 0): h["price"] = decision_price changes += 1 - print(f" ✅ {code} {h.get('name','')}: price {old_price} → {decision_price}") + print(f" ✅ {code} {h.get('name','')}: price {old_price} \u2192 {decision_price}") elif decision_price and old_price and abs(decision_price - old_price) / max(abs(old_price), 1) > 0.05: - # Price differs >5% from decision - flag it but don't overwrite (price_monitor is authoritative) print(f" ⚠️ {code} {h.get('name','')}: 当前价 {old_price} vs decisions {decision_price} (偏离>{5:.0f}%),保留当前价") elif not decision_price: errors += 1 if old_price == 0 or not old_price: - print(f" ❌ {code} {h.get('name','')}: decisions.json 无此股 price 数据,当前 price={old_price}") + print(f" ❌ {code} {h.get('name','')}: decisions 无此股 price 数据,当前 price={old_price}") # 重算 total_assets / position_pct total_mv = 0 @@ -86,38 +80,56 @@ def fix_portfolio(path: Path, price_map: dict, check_only: bool): price = h.get("price", 0) or 0 shares = h.get("shares", 0) or 0 total_mv += price * shares - + cash = pf.get("cash", 0) or 0 - new_total_assets = round(total_mv + cash, 2) + frozen = pf.get("frozen_cash", 0) or 0 + new_total_assets = round(total_mv + cash + frozen, 2) new_position_pct = round(total_mv / new_total_assets * 100, 2) if new_total_assets > 0 else 0 - + old_total_assets = pf.get("total_assets", 0) old_position_pct = pf.get("position_pct", 0) - + if abs(new_total_assets - old_total_assets) > 100: - print(f" 📊 total_assets: {old_total_assets} → {new_total_assets} (变动 {new_total_assets-old_total_assets:.0f})") - pf["total_assets"] = new_total_assets + print(f" 📊 total_assets: {old_total_assets} \u2192 {new_total_assets} (变动 {new_total_assets-old_total_assets:.0f})") changes += 1 - + if abs(new_position_pct - old_position_pct) > 0.5: - print(f" 📊 position_pct: {old_position_pct}% → {new_position_pct}%") - pf["position_pct"] = new_position_pct + print(f" 📊 position_pct: {old_position_pct}% \u2192 {new_position_pct}%") changes += 1 - - pf["updated_at"] = __import__("datetime").datetime.now().strftime('%Y-%m-%d %H:%M') if changes == 0: - print(f" ✅ {path.name}: 无需修复({len(holdings)} 个持仓价格正常)") + print(f" ✅ 无需修复({len(holdings)} 个持仓价格正常)") return True if check_only: print(f" ⏸️ 检查模式: {changes} 处需修复,未写入") return True + # 写入 DB try: - with open(path, "w") as f: - json.dump(pf, f, indent=2, ensure_ascii=False) - print(f" ✅ {path.name}: {changes} 处已修复,已写入") + conn = get_conn() + ok, msg = write_holdings_batch(conn, holdings) + if not ok: + print(f" ❌ 写入 holdings 失败: {msg}", file=sys.stderr) + conn.close() + return False + + summary = { + "total_assets": new_total_assets, + "total_mv": round(total_mv, 2), + "stock_value": round(total_mv, 2), + "cash": cash, + "frozen_cash": frozen, + "position_pct": new_position_pct, + "currency": pf.get("currency", "CNY"), + } + ok, msg = write_portfolio_summary(conn, summary) + conn.close() + if not ok: + print(f" ❌ 写入 portfolio_summary 失败: {msg}", file=sys.stderr) + return False + + print(f" ✅ DB: {changes} 处已修复,已写入") return True except Exception as e: print(f" ❌ 写入失败: {e}", file=sys.stderr) @@ -126,25 +138,16 @@ def fix_portfolio(path: Path, price_map: dict, check_only: bool): def main(): check_only = "--check" in sys.argv - + print("=== fix_portfolio_prices.py ===") if check_only: print("模式:检查(不写入)") else: print("模式:修复") - - price_map = load_decisions() - if not price_map: - print("❌ decisions.json 无有效 price 数据,退出") - return 1 - - print(f"\n--- {MOFIN_PORTFOLIO.name} ---") - ok1 = fix_portfolio(MOFIN_PORTFOLIO, price_map, check_only) - - print(f"\n--- {DASHBOARD_PORTFOLIO.name} ---") - ok2 = fix_portfolio(DASHBOARD_PORTFOLIO, price_map, check_only) - - if ok1 and ok2: + + ok = fix_portfolio(check_only) + + if ok: print("\n✅ 全部完成") return 0 else: diff --git a/scripts/inspect_decisions.py b/scripts/inspect_decisions.py index f69b979b..720f5be1 100644 --- a/scripts/inspect_decisions.py +++ b/scripts/inspect_decisions.py @@ -1,6 +1,5 @@ -import json -with open('/home/hmo/web-dashboard/data/decisions.json') as f: - d = json.load(f) +from mo_data import read_decisions +d = read_decisions() for i, e in enumerate(d.get('decisions', [])[:3]): print(f"\n=== Entry {i}: {e.get('code')} {e.get('name')} ===") for k, v in sorted(e.items()): diff --git a/scripts/intraday_health_check.py b/scripts/intraday_health_check.py index 3d171f95..15800f3b 100644 --- a/scripts/intraday_health_check.py +++ b/scripts/intraday_health_check.py @@ -241,7 +241,13 @@ def main(): check_gateways() check_xiaoguo() if 9 <= now.hour < 16: - check_price_monitor() + # 开盘前10分钟(9:00-9:10)跳过价格新鲜度检查 + # price_monitor 从 09:00 才开始启动,09:01 检查时数据还未更新(前一天收盘数据) + # 给 price_monitor 足够时间完成第一轮数据拉取更新 + if now.hour == 9 and now.minute < 10: + log(True, "开盘初期,价格监控grace period(跳过新鲜度检查)") + else: + check_price_monitor() check_signal_pipeline() write_todos() diff --git a/scripts/json_validation.py b/scripts/json_validation.py new file mode 100644 index 00000000..c86fa907 --- /dev/null +++ b/scripts/json_validation.py @@ -0,0 +1,870 @@ +#!/usr/bin/env python3 +""" +json_validation.py — JSON 格式校验 + 打分-重试循环 + Format Error Library + +为知微股票分析 Pipeline 提供三层校验: + 1. JSON 语法解析 (json.loads) + 2. JSON Schema 字段匹配(可选) + 3. 语义完整性检查(关键字段非空、数值范围合理) + +核心功能: + - validate_json_syntax(raw) → (score, errors, parsed_or_None) + - scored_retry_loop(llm_fn, max_retries=3, threshold=90, schema=None) + - FormatErrorLibrary — 持久化错误记录,每日/周汇总 + - best_effort_fallback(raw) → dict(尽力恢复) + +用法: + from json_validation import scored_retry_loop, FormatErrorLibrary + + def my_llm_call(prompt): + # 调用 LLM 返回原始文本 + return llm_response + + result = scored_retry_loop( + llm_fn=my_llm_call, + prompt=initial_prompt, + context={"code": "000001", "name": "平安银行"}, + schema=SCHEMA, + max_retries=3, + threshold=90, + ) + # result = {"ok": True, "data": {...}, "attempts": 2, "score": 95} 或 + # result = {"ok": False, "data": {...}, "attempts": 3, "score": 60, "fallback": True} +""" + +import json +import os +import re +import sys +import time +from datetime import datetime, timedelta +from pathlib import Path +from collections import defaultdict + +# ─── 常量 ─────────────────────────────────────────────────── +DEFAULT_THRESHOLD = 90 # 验收阈值(0-100) +DEFAULT_MAX_RETRIES = 3 # 最大重试次数 +ERROR_LIBRARY_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data", "format_error_library.json" +) + +# ─── 标准 JSON Schema 种子字段 ───────────────────────────── +# 知微股票分析输出期望包含的字段 +STOCK_ANALYSIS_SCHEMA = { + "required": [ + "code", "name", "action", "stop_loss", "take_profit", + "entry_low", "entry_high", "timing_signal", "rr_ratio", + ], + "optional": [ + "price", "cost", "shares", "currency", + "tech_snapshot", "signal_factors", "sector_context", + "multi_tf_context", "status", "quality_check", + ], + "types": { + "code": str, "name": str, "action": str, + "stop_loss": (int, float), "take_profit": (int, float), + "entry_low": (int, float), "entry_high": (int, float), + "timing_signal": str, "rr_ratio": (int, float), + } +} + + +# ═══════════════════════════════════════════════════════════ +# 第一层:JSON 语法校验 + 打分 +# ═══════════════════════════════════════════════════════════ + +def validate_json_syntax(raw_text): + """JSON 语法校验 + 打分(0-100) + + 返回: (score, errors, parsed_or_None) + score: 0-100 整数 + errors: 错误描述列表 + parsed_or_None: 成功 parse 的 dict 或 None + """ + errors = [] + + if not raw_text or not raw_text.strip(): + return (0, ["空文本"], None) + + text = raw_text.strip() + + # 步骤1: 尝试提取可能的 JSON(从 markdown 代码块中) + extracted = _extract_json_block(text) + if extracted != text: + text = extracted + + # 步骤2: 初步语法检查 + score = 50 # 基础分 + + # 检查是否有 JSON 的基本特征 + if text.startswith("{") or text.startswith("["): + score += 10 + else: + errors.append("不以 { 或 [ 开头,可能不是 JSON") + + if text.endswith("}") or text.endswith("]"): + score += 10 + else: + errors.append("不以 } 或 ] 结尾,可能被截断") + + # 步骤3: 尝试解析 + try: + parsed = json.loads(text) + score += 30 # 成功解析加 30 分 + + # 检查深层嵌套完整性 + _check_nested_integrity(parsed, errors) + + # 最终得分 + if not errors: + score = min(100, score + 10) # 完美通过再加 10 + + return (min(100, score), errors, parsed) + + except json.JSONDecodeError as e: + errors.append(f"JSON 语法错误: {e}") + + # 常见错误类型归类 + err_msg = str(e) + if "trailing comma" in err_msg.lower() or "extra data" in err_msg.lower(): + errors.append("类型: 尾随逗号或多余数据") + score -= 5 + elif "unterminated" in err_msg.lower(): + errors.append("类型: 未闭合的引号/括号") + score -= 10 + elif "unexpected" in err_msg.lower(): + errors.append("类型: 意外的字符/标记") + score -= 8 + elif "control character" in err_msg.lower(): + errors.append("类型: 字符串包含未转义的控制字符") + score -= 5 + elif "expect value" in err_msg.lower(): + errors.append("类型: 缺少值或字段名") + score -= 8 + + # 尝试自动修复 + fixed, fixed_desc = _auto_fix_json(text) + if fixed: + try: + parsed = json.loads(fixed) + score = max(score, 75) # 修复成功给 75 分 + errors.append(f"自动修复成功: {fixed_desc}") + return (score, errors, parsed) + except json.JSONDecodeError: + pass + + return (max(0, score - 20), errors, None) + + +def _extract_json_block(text): + """从文本中提取 JSON 代码块(```json ... ```)""" + # 模式1: ```json ... ``` 或 ``` ... ``` + m = re.search(r'```(?:json)?\s*(\{.*?\}|\[.*?\])\s*```', text, re.DOTALL) + if m: + return m.group(1).strip() + # 模式2: ... + m = re.search(r'\s*(\{.*?\}|\[.*?\])\s*', text, re.DOTALL) + if m: + return m.group(1).strip() + return text + + +def _check_nested_integrity(obj, errors, path=""): + """递归检查嵌套结构的完整性""" + if isinstance(obj, dict): + for k, v in obj.items(): + child_path = f"{path}.{k}" if path else k + if v is None: + errors.append(f"字段 {child_path} 为 null") + elif isinstance(v, str) and len(v) > 1000: + errors.append(f"字段 {child_path} 字符串过长 ({len(v)} 字符)") + else: + _check_nested_integrity(v, errors, child_path) + elif isinstance(obj, list): + if len(obj) > 100: + errors.append(f"数组 {path} 过长 ({len(obj)} 项)") + for i, item in enumerate(obj): + _check_nested_integrity(item, errors, f"{path}[{i}]") + + +def _auto_fix_json(text): + """尝试自动修复常见 JSON 格式问题 + + 返回: (fixed_text_or_None, description_or_None) + """ + original = text + fixes = [] + + # 修复1: 去掉多余尾部括号(可能有多余的 }}}) + fixed = text.rstrip() + # 计算开括号和闭括号的数量 + open_braces = fixed.count('{') + close_braces = fixed.count('}') + open_brackets = fixed.count('[') + close_brackets = fixed.count(']') + + if close_braces > open_braces: + # 去掉多余的 } + excess = close_braces - open_braces + for _ in range(excess): + last_idx = fixed.rfind('}') + if last_idx >= 0: + fixed = fixed[:last_idx] + fixed[last_idx+1:] + fixes.append(f"去掉 {excess} 个多余右括号") + + if close_brackets > open_brackets: + excess = close_brackets - open_brackets + for _ in range(excess): + last_idx = fixed.rfind(']') + if last_idx >= 0: + fixed = fixed[:last_idx] + fixed[last_idx+1:] + fixes.append(f"去掉 {excess} 个多余右中括号") + + # 修复2: 字符串内未转义的换行符 + result = [] + in_str = False + for ch in fixed: + if ch == '"': + in_str = not in_str + result.append(ch) + elif in_str and ch in '\n\r': + result.append('\\n') + else: + result.append(ch) + fixed2 = ''.join(result) + if fixed2 != fixed: + fixes.append("转义字符串内换行符") + fixed = fixed2 + + # 修复3: 尾随逗号(在 } 或 ] 前的逗号) + fixed = re.sub(r',\s*}', '}', fixed) + fixed = re.sub(r',\s*\]', ']', fixed) + if fixed != original: + fixes.append("移除尾随逗号") + + # 修复4: 如果还不行,尝试补全为有效的对象包装 + if not fixed.startswith('{') and not fixed.startswith('['): + # 可能是裸值 → 包装为对象 + try: + json.loads(fixed) + except json.JSONDecodeError: + pass # 保留原始 + + if fixed != original: + return fixed, '; '.join(fixes) + + # 无有效修复 + return None, None + + +# ═══════════════════════════════════════════════════════════ +# 第二层:JSON Schema 校验 +# ═══════════════════════════════════════════════════════════ + +def validate_json_schema(parsed, schema=None): + """JSON Schema 字段匹配验证 + + schema 格式: + { + "required": ["field1", "field2"], + "optional": ["field3"], + "types": {"field1": str, "field2": (int, float)}, + "min_values": {"rr_ratio": 0}, + "non_empty": ["action", "timing_signal"], + } + + 返回: (score, errors) + score: 0-100 + errors: 字段级别的错误列表 + """ + if schema is None: + schema = STOCK_ANALYSIS_SCHEMA + + if not isinstance(parsed, dict): + return (30, ["顶层不是对象(dict),无法做 Schema 校验"]) + + errors = [] + score = 50 # 基础分 + + required = schema.get("required", []) + optional = schema.get("optional", []) + types = schema.get("types", {}) + min_values = schema.get("min_values", {}) + non_empty = schema.get("non_empty", []) + ranges = schema.get("ranges", {}) # {"field": (min, max)} + + # 检查必要字段 + missing_required = [] + for field in required: + if field not in parsed: + missing_required.append(field) + + if missing_required: + errors.append(f"缺少必要字段: {', '.join(missing_required)}") + score -= len(missing_required) * 8 + else: + score += 15 + + # 检查字段类型 + type_errors = [] + for field, expected_type in types.items(): + if field in parsed and parsed[field] is not None: + val = parsed[field] + if not isinstance(val, expected_type): + type_errors.append(f"{field}: 期望 {expected_type.__name__}, 实际 {type(val).__name__}") + + if type_errors: + errors.extend(type_errors) + score -= len(type_errors) * 5 + + # 检查最小值约束 + for field, min_val in min_values.items(): + if field in parsed and parsed[field] is not None: + try: + if float(parsed[field]) < min_val: + errors.append(f"{field}={parsed[field]} < 最小值 {min_val}") + score -= 5 + except (TypeError, ValueError): + pass + + # 检查非空约束 + for field in non_empty: + if field in parsed: + val = parsed[field] + if val is None or (isinstance(val, str) and not val.strip()) or val == 0: + errors.append(f"{field} 为空/零") + score -= 5 + + # 检查数值范围 + for field, (lo, hi) in ranges.items(): + if field in parsed and parsed[field] is not None: + try: + v = float(parsed[field]) + if v < lo or v > hi: + errors.append(f"{field}={v} 超出合理范围 [{lo}, {hi}]") + score -= 3 + except (TypeError, ValueError): + pass + + # 未知字段不扣分(可接受额外信息) + + # 标准化 score + score = max(0, min(100, score)) + return (score, errors) + + +# ═══════════════════════════════════════════════════════════ +# 第三层:打分-重试循环 +# ═══════════════════════════════════════════════════════════ + +def scored_retry_loop(llm_fn, prompt, context=None, schema=None, + max_retries=3, threshold=90, + error_lib=None, task_id=None): + """打分-重试循环 + + 参数: + llm_fn: callable(prompt) → raw_text + 每次重试时传入附加上一轮反馈的增强 prompt + prompt: 初始 prompt 文本 + context: dict,额外的上下文信息,用于记录 + schema: JSON Schema 定义(可选) + max_retries: 最大重试次数(默认 3) + threshold: 验收阈值 0-100(默认 90) + error_lib: FormatErrorLibrary 实例(可选),用于记录错误 + task_id: 任务标识符(可选),用于错误库跟踪 + + 返回: + { + "ok": True/False, # True=在阈值内通过, False=最终fallback + "data": dict or None, # 解析后的 JSON 数据 + "raw": str, # 最终原始输出 + "attempts": int, # 实际尝试次数 + "scores": [int, ...], # 每次尝试的分数 + "errors": [str, ...], # 最终错误列表 + "fallback": bool, # 是否走了一段 best_effort + } + """ + # 验证最小报价字段 + _validate_min_values(schema) + + best_score = 0 + best_data = None + best_raw = "" + + # 构建基础 prompt 系统反馈部分 + base_prompt = prompt + feedback_history = [] + scores = [] + + for attempt in range(1, max_retries + 1): + # 构建本次的 prompt(附加上一轮的格式反馈) + current_prompt = base_prompt + if feedback_history: + feedback_section = "\n\n[Format Feedback from previous attempt]\n" + \ + "\n".join(feedback_history) + current_prompt = base_prompt + feedback_section + + # 调用 LLM + try: + raw = llm_fn(current_prompt) + except Exception as e: + scores.append(0) + feedback_history.append(f"Attempt {attempt}: LLM call failed: {e}") + continue + + if not raw or not raw.strip(): + scores.append(0) + feedback_history.append(f"Attempt {attempt}: 空响应") + continue + + # 步骤1: 语法校验 + syntax_score, syntax_errors, parsed = validate_json_syntax(raw) + + # 步骤2: Schema 校验(如果语法通过且有 schema) + schema_score = 100 + schema_errors = [] + if parsed is not None and schema: + schema_score, schema_errors = validate_json_schema(parsed, schema) + + # 综合打分: 语法占 60%, Schema 占 40% + if parsed is not None: + combined_score = int(syntax_score * 0.6 + schema_score * 0.4) + else: + combined_score = syntax_score # 语法失败时只用语法分 + + scores.append(combined_score) + + # 记录错误信息 + all_errors = syntax_errors + schema_errors + + # 记录到 Format Error Library + if error_lib and task_id: + error_lib.record( + task_id=task_id, + attempt=attempt, + score=combined_score, + errors=all_errors, + context=context, + ) + + # 追踪最佳结果 + if combined_score > best_score: + best_score = combined_score + best_data = parsed + best_raw = raw + + # 检查是否通过阈值 + if combined_score >= threshold: + return { + "ok": True, + "data": parsed, + "raw": raw, + "attempts": attempt, + "scores": scores, + "errors": all_errors, + "fallback": False, + } + + # 准备下一轮反馈 + if attempt < max_retries: + feedback = _build_feedback(combined_score, all_errors, attempt) + feedback_history.append(feedback) + + # 小幅延迟避免 API 限流 + if attempt < max_retries: + time.sleep(0.5) + + # 所有重试都失败 → 尝试 best_effort_fallback + fallback_data = best_data + fallback_raw = best_raw + + if fallback_data is None and fallback_raw: + # 尝试用 best_effort 从最佳原始输出恢复 + fallback_data = best_effort_fallback(fallback_raw) + + # 构建最终结果 + result = { + "ok": fallback_data is not None, + "data": fallback_data, + "raw": fallback_raw, + "attempts": max_retries, + "scores": scores, + "errors": ["所有重试均未达到阈值", f"最高分: {best_score}/{threshold}"], + "fallback": True, + } + + # 产生告警(通过 stderr 输出,可被 cron 捕获) + task_tag = f"[{task_id}]" if task_id else "" + print( + f"[JSON_VALIDATION] WARNING{task_tag}: " + f"{max_retries}次重试后最高分{best_score}/{threshold}," + f"使用{'fallback数据' if fallback_data else '空数据'}", + file=sys.stderr, flush=True + ) + + # 记录严重错误到 error_lib + if error_lib and task_id: + error_lib.record( + task_id=task_id, + attempt=max_retries + 1, + score=best_score, + errors=["ALL_RETRIES_EXHAUSTED", f"最高分{best_score}/{threshold}"], + context=context, + is_failure=True, + ) + + return result + + +def _validate_min_values(schema): + """确保 schema 包含 min_values 约束""" + if schema and "min_values" not in schema: + schema["min_values"] = {} + return schema + + +def _build_feedback(score, errors, attempt): + """构建格式反馈字符串(给 LLM 的下一轮提示)""" + parts = [f"--- Attempt {attempt} feedback (score: {score}/100) ---"] + if errors: + parts.append("Issues to fix:") + for e in errors[:5]: # 最多反馈 5 个问题 + parts.append(f" - {e}") + else: + parts.append("No specific errors found, but score below threshold.") + + return "\n".join(parts) + + +# ═══════════════════════════════════════════════════════════ +# Best-effort Fallback +# ═══════════════════════════════════════════════════════════ + +def best_effort_fallback(raw_text): + """尽力从非法 JSON 中恢复数据 + + 策略(按优先级): + 1. 尝试 auto_fix 后解析 + 2. 正则提取所有 key:value 对 + 3. 提取股票代码等关键信息 + """ + if not raw_text: + return None + + # 策略1: auto_fix + fixed, _ = _auto_fix_json(raw_text) + if fixed: + try: + return json.loads(fixed) + except json.JSONDecodeError: + pass + + # 策略2: 正则提取所有字段 + data = {} + + # 提取代码 + code_match = re.search(r'"code"\s*:\s*"(\d+)"', raw_text) + if code_match: + data["code"] = code_match.group(1) + + # 提取名称 + name_match = re.search(r'"name"\s*:\s*"([^"]+)"', raw_text) + if name_match: + data["name"] = name_match.group(1) + + # 提取数值字段 + for field in ["stop_loss", "take_profit", "entry_low", "entry_high", + "price", "rr_ratio", "cost"]: + pattern = rf'"{field}"\s*:\s*([\d.]+)' + m = re.search(pattern, raw_text) + if m: + try: + data[field] = float(m.group(1)) + except ValueError: + pass + + # 提取字符串字段 + for field in ["action", "timing_signal", "currency", "status"]: + pattern = rf'"{field}"\s*:\s*"([^"]*)"' + m = re.search(pattern, raw_text) + if m: + data[field] = m.group(1) + + return data if data else None + + +# ═══════════════════════════════════════════════════════════ +# Format Error Library +# ═══════════════════════════════════════════════════════════ + +class FormatErrorLibrary: + """格式错误库 — 持久化记录每次格式错误 + + 每月/周自动汇总,支持按错误类型和任务ID查询。 + """ + + def __init__(self, path=None): + self.path = path or ERROR_LIBRARY_PATH + self._records = [] + self._load() + + def _load(self): + """从磁盘加载已有记录""" + if os.path.exists(self.path): + try: + with open(self.path, "r", encoding="utf-8") as f: + data = json.load(f) + self._records = data.get("records", []) + except (json.JSONDecodeError, Exception): + self._records = [] + else: + self._records = [] + + def _save(self): + """持久化到磁盘""" + os.makedirs(os.path.dirname(self.path), exist_ok=True) + with open(self.path, "w", encoding="utf-8") as f: + json.dump({ + "library_version": "1.0", + "total_records": len(self._records), + "last_updated": datetime.now().isoformat(), + "records": self._records[-1000:], # 最多保留最近1000条 + }, f, ensure_ascii=False, indent=2) + + def record(self, task_id, attempt, score, errors, + context=None, is_failure=False): + """记录一条格式错误""" + record = { + "task_id": task_id, + "timestamp": datetime.now().isoformat(), + "attempt": attempt, + "score": score, + "errors": errors[:10], # 最多保留10个错误 + "is_failure": is_failure, + } + if context: + # 只记录上下文的关键信息,避免敏感数据 + safe_ctx = {} + for k in ["code", "name", "stock"]: + if k in (context or {}): + safe_ctx[k] = context[k] + if safe_ctx: + record["context"] = safe_ctx + + self._records.append(record) + self._save() + + def get_summary(self, days=7): + """获取最近 N 天的错误汇总 + + 返回: { + "total": 总记录数, + "by_type": {"missing_field": N, "syntax_error": N, ...}, + "by_task": {"task_id": N}, + "worst_tasks": [最差任务列表], + } + """ + cutoff = datetime.now() - timedelta(days=days) + recent = [ + r for r in self._records + if datetime.fromisoformat(r["timestamp"]) >= cutoff + ] + + if not recent: + return {"total": 0, "by_type": {}, "by_task": {}, "worst_tasks": []} + + by_type = defaultdict(int) + by_task = defaultdict(int) + task_scores = defaultdict(list) + + for r in recent: + task = r.get("task_id", "unknown") + by_task[task] += 1 + task_scores[task].append(r.get("score", 0)) + + for err in r.get("errors", []): + # 归类错误类型 + if "缺少" in err or "missing" in err.lower(): + by_type["missing_field"] += 1 + elif "syntax" in err.lower() or "JSON" in err or "json" in err: + by_type["syntax_error"] += 1 + elif "trailing comma" in err.lower() or "尾随逗号" in err: + by_type["trailing_comma"] += 1 + elif "truncat" in err.lower() or "截断" in err: + by_type["truncation"] += 1 + else: + by_type["other"] += 1 + + # 最差任务(平均分最低) + worst = sorted( + [(t, sum(s)/len(s), by_task[t]) for t, s in task_scores.items()], + key=lambda x: x[1] + )[:5] + + return { + "total": len(recent), + "period_days": days, + "by_type": dict(by_type), + "by_task": dict(by_task), + "worst_tasks": [ + {"task": t, "avg_score": round(avg, 1), "count": c} + for t, avg, c in worst + ], + "generated_at": datetime.now().isoformat(), + } + + def get_recent(self, limit=20): + """获取最近的错误记录""" + return list(reversed(self._records[-limit:])) + + def clear_old(self, keep_days=30): + """清理超过 keep_days 的旧记录""" + cutoff = datetime.now() - timedelta(days=keep_days) + before = len(self._records) + self._records = [ + r for r in self._records + if datetime.fromisoformat(r["timestamp"]) >= cutoff + ] + after = len(self._records) + self._save() + return before - after + + def auto_remediate_prompt(self): + """自动分析最近错误模式,返回改进提示词的建议 + + 返回: dict {建议类型: 建议内容} + """ + summary = self.get_summary(days=7) + suggestions = {} + + by_type = summary.get("by_type", {}) + total = summary.get("total", 0) + + if total == 0: + return {"info": "最近7天无格式错误,系统运行良好"} + + # 字段缺失问题 → 建议在 prompt 中显式列出字段 + missing = by_type.get("missing_field", 0) + if missing > 0 and missing / max(total, 1) > 0.3: + suggestions["prompt_field_emphasis"] = ( + f"最近7天 {missing}/{total} 错误是字段缺失," + "建议在 prompt 中显式列出所有必需字段的说明和顺序" + ) + + # 语法错误 → 建议使用结构化生成模式 + syntax = by_type.get("syntax_error", 0) + if syntax > 0 and syntax / max(total, 1) > 0.5: + suggestions["structured_generation"] = ( + f"最近7天 {syntax}/{total} 错误是 JSON 语法错误," + "建议启用 constrained decoding 或 JSON mode" + ) + + # 截断 → 检查 max_tokens + trunc = by_type.get("truncation", 0) + if trunc > 0: + suggestions["increase_max_tokens"] = ( + f"最近7天 {trunc} 次截断,建议 max_tokens 从当前值上调" + ) + + if not suggestions: + suggestions["info"] = "错误率在可接受范围内,无需自动调整" + + return suggestions + + +# ═══════════════════════════════════════════════════════════ +# 便捷封装: 在 LLM 响应中提取 JSON 的统一入口 +# ═══════════════════════════════════════════════════════════ + +def extract_json_safe(llm_response, schema=None, task_id=None): + """安全地从 LLM 响应中提取 JSON,带重试 + + 这是一个单轮快速提取版本(不做重试),适用于不需要 + 完整 scored_retry_loop 的场景。 + + 返回: (parsed_dict_or_None, score, errors) + """ + score, errors, parsed = validate_json_syntax(llm_response) + + if parsed is not None and schema: + schema_score, schema_errors = validate_json_schema(parsed, schema) + score = int(score * 0.6 + schema_score * 0.4) + errors.extend(schema_errors) + + if parsed is None: + # 尝试 fallback + parsed = best_effort_fallback(llm_response) + + return parsed, score, errors + + +# ═══════════════════════════════════════════════════════════ +# CLI 入口(测试/调试用) +# ═══════════════════════════════════════════════════════════ + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "--summary": + lib = FormatErrorLibrary() + summary = lib.get_summary(days=7) + print(json.dumps(summary, ensure_ascii=False, indent=2)) + sys.exit(0) + + if len(sys.argv) > 1 and sys.argv[1] == "--recent": + lib = FormatErrorLibrary() + recent = lib.get_recent(limit=10) + print(json.dumps(recent, ensure_ascii=False, indent=2)) + sys.exit(0) + + if len(sys.argv) > 1 and sys.argv[1] == "--test": + # 自测:验证各种情况 + test_cases = [ + ('{"code": "000001", "name": "平安"}', "标准 JSON"), + ('{"code": "000001", "name": "平安",}', "尾随逗号"), + ('```json\n{"code": "000001"}\n```', "代码块包裹"), + ('{"code": "000001"}', "structured_data 标签"), + ('{"code": "000001",\n"name": "平\n安"}', "未转义换行符"), + ('{"code": "000001"}}', "多余右括号"), + ('{"code": "000001", "price": 12.5, "stop_loss": null, "action": ""}', "空字段"), + ] + + for text, desc in test_cases: + score, errors, parsed = validate_json_syntax(text) + status = "✅" if parsed else "❌" + print(f"{status} {desc}: score={score}") + if errors: + for e in errors: + print(f" - {e}") + if parsed: + print(f" parsed: {json.dumps(parsed, ensure_ascii=False)[:80]}") + print() + + # 测试 Schema 校验 + print("=== Schema 校验测试 ===") + test_obj = { + "code": "000001", "name": "平安", "action": "持有", + "stop_loss": 10.5, "take_profit": 12.0, + "entry_low": 10.0, "entry_high": 11.0, + "timing_signal": "持有", "rr_ratio": 2.0, + } + score, errors = validate_json_schema(test_obj) + print(f"完整对象: score={score}, errors={errors}") + + test_obj2 = {"code": "000001", "name": "平安"} + score2, errors2 = validate_json_schema(test_obj2) + print(f"缺失字段: score={score2}, errors={errors2}") + + sys.exit(0) + + # 默认:从 stdin 读取 JSON 文本并验证 + text = sys.stdin.read() + score, errors, parsed = validate_json_syntax(text) + result = { + "score": score, + "errors": errors, + "parsed": parsed, + "valid": parsed is not None, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) diff --git a/scripts/market_insight.py b/scripts/market_insight.py index 86796ef3..c0bb082e 100644 --- a/scripts/market_insight.py +++ b/scripts/market_insight.py @@ -25,8 +25,9 @@ def load_holding_industry_map(): with open(DATA_DIR / "stock_profiles.json", "r", encoding="utf-8") as f: profiles = json.load(f).get("profiles", []) - with open(DATA_DIR / "portfolio.json", "r", encoding="utf-8") as f: - portfolio = json.load(f) + # 优先从DB读取持仓(Dad铁律:禁用JSON直读) + from mo_data import read_portfolio + portfolio = read_portfolio() except FileNotFoundError: return {} @@ -55,6 +56,8 @@ def load_holding_industry_map(): def generate(): + # market_path 在DB和fallback两个分支后都会用到,所以提前定义 + market_path = DATA_DIR / "market.json" # 优先从 SQLite 读取市场数据 try: from mofin_db import get_conn, query_latest_market diff --git a/scripts/mo_alphasift_bridge.py b/scripts/mo_alphasift_bridge.py index 75c321b5..fe4989bf 100644 --- a/scripts/mo_alphasift_bridge.py +++ b/scripts/mo_alphasift_bridge.py @@ -18,8 +18,8 @@ from pathlib import Path DSA_API = "http://127.0.0.1:8001" MOFIN_DATA = Path("/home/hmo/web-dashboard/data") -WATCHLIST_PATH = MOFIN_DATA / "watchlist.json" -PORTFOLIO_PATH = MOFIN_DATA / "portfolio.json" +# 已迁移到 DB — watchlist.json / portfolio.json 不再使用 +# 保留路径仅用于兼容 import,实际数据从 mofin.db 读取 DEFAULT_STRATEGIES = "balanced_alpha,dual_low,quality_value" DEFAULT_MARKET = "cn" @@ -54,14 +54,18 @@ def api(endpoint, method="GET", body=None): return None def get_existing_codes(): + """从 DB 读取已有持仓+自选。JSON 已废弃。""" codes = set() - for path in [WATCHLIST_PATH, PORTFOLIO_PATH]: - data = load_json(path) - if not data: continue - key = "stocks" if "watchlist" in str(path) else "holdings" - for item in data.get(key, []): - c = str(item.get("code", "")).strip() - if c: codes.add(c) + try: + import sqlite3 + db = sqlite3.connect(str(MOFIN_DATA / "mofin.db")) + for row in db.execute("SELECT code FROM watchlist_stocks WHERE is_active=1"): + codes.add(str(row[0]).strip()) + for row in db.execute("SELECT code FROM holdings"): + codes.add(str(row[0]).strip()) + db.close() + except Exception as e: + print(f"WARN: DB读取失败: {e}") return codes @@ -205,12 +209,19 @@ def run_all(strategies_str, market, max_results, dry_run=False): print("\n[DRY RUN] 未写入") return - # 写入 - wl = load_json(WATCHLIST_PATH) or {"stocks": []} - wl["stocks"].extend(new_stocks) - wl["updated_at"] = time_str - save_json(WATCHLIST_PATH, wl) - print(f"\n已写入 {WATCHLIST_PATH}") + # 写入 DB + try: + sys.path.insert(0, str(MOFIN_DATA.parent)) + from mofin_db import get_conn, write_watchlist_stock + conn = get_conn() + for s in new_stocks: + s.setdefault('currency', 'CNY') + write_watchlist_stock(conn, s) + conn.close() + print(f"\n已写入 {len(new_stocks)} 只到 DB watchlist_stocks") + except Exception as e: + print(f"WARN: DB写入失败: {e}") + return # 策略生成 print("\n调用 regenerate_all()...") diff --git a/scripts/mo_config.py b/scripts/mo_config.py index 1199092c..df1c7b8b 100644 --- a/scripts/mo_config.py +++ b/scripts/mo_config.py @@ -42,22 +42,26 @@ class MoConfig: # Hermes 状态目录 hermes_dir: Path = field(default_factory=lambda: Path.home() / ".hermes") - # ── 关键数据文件路径 ────────────────────────────────────────── - + # ── 关键数据文件路径(已废弃,仅保留为检查逻辑。新代码勿用) ────── + @property def portfolio_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db holdings + portfolio_summary 表。""" return self.data_dir / "portfolio.json" - + @property def decisions_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db holding_strategies 表。""" return self.data_dir / "decisions.json" - + @property def watchlist_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db watchlist_stocks 表。""" return self.data_dir / "watchlist.json" - + @property def price_events_path(self) -> Path: + """⚠️ 已废弃!数据在 mofin.db price_events 表。""" return self.data_dir / "price_events.json" @property diff --git a/scripts/mofin_collect.py b/scripts/mofin_collect.py index b94f3248..8441844e 100644 --- a/scripts/mofin_collect.py +++ b/scripts/mofin_collect.py @@ -155,11 +155,28 @@ try: for wr in conn.execute("SELECT code, name, price, entry_low, entry_high, stop_loss FROM watchlist_stocks WHERE is_active=1"): code = wr["code"] name = wr["name"] + wl_price = wr["price"] or 0 + # 自选股price可能为0(新加入未更新),从实时API获取 + if wl_price <= 0: + try: + import urllib.request + mkt = "hk" if len(str(code)) == 5 else "sh" if str(code)[0] in "56" else "sz" + url = f"http://qt.gtimg.cn/q={mkt}{code}" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + resp = urllib.request.urlopen(req, timeout=5).read() + text = resp.decode("gbk") + parts = text.split("~") + if len(parts) > 3: + p = float(parts[3]) + if p > 0: + wl_price = p + except Exception: + pass # 自选股无cost/shares,传0 try: from strategy_lifecycle import reassess_with_context result = reassess_with_context( - code, name, wr["price"] or 0, + code, name, wl_price, 0, 0, "" ) if result and result.get("action"): diff --git a/scripts/mofin_db.py b/scripts/mofin_db.py index ca765a4c..9965877f 100644 --- a/scripts/mofin_db.py +++ b/scripts/mofin_db.py @@ -21,7 +21,7 @@ from datetime import datetime from pathlib import Path from typing import Optional, Callable -DATA_DIR = Path(__file__).parent / "data" +DATA_DIR = Path(__file__).parent.parent / "data" DB_PATH = DATA_DIR / "mofin.db" # ═══════════════════════════════════════════════════════════ @@ -1097,7 +1097,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool signal_factors_json, time_horizon, decision_type) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now','localtime'), - ?,?,?,?,?,?,?,?,?) + ?,?,?,?,?,?,?,?,?,?) """, ( code, name, data.get('version', 1), data.get('price'), data.get('cost'), @@ -1187,7 +1187,7 @@ def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]: def write_watchlist_stock(conn, stock: dict) -> tuple[bool, str]: - """写入自选股(替代 watchlist.json)""" + """写入自选股(写入 watchlist_stocks 表)""" try: conn.execute(""" INSERT INTO watchlist_stocks (code, name, price, entry_low, entry_high, diff --git a/scripts/morning_health_check.py b/scripts/morning_health_check.py index 4f3f76bd..89ca01f7 100755 --- a/scripts/morning_health_check.py +++ b/scripts/morning_health_check.py @@ -672,7 +672,7 @@ def run_check(item): if 'macro_risk_state' in content: consumer_info = 'macro_risk_state.json' if 'watchlist' in content.lower(): - consumer_info = 'watchlist.json / decisions.json' + consumer_info = 'watchlist_stocks表 / holding_strategies表' if 'INSERT INTO' in content: for tbl in ['todos', 'price_events', 'macro_context_log', 'accuracy_stats']: if tbl in content: diff --git a/scripts/multi_timeframe.py b/scripts/multi_timeframe.py index 69f4291d..514bc1fa 100644 --- a/scripts/multi_timeframe.py +++ b/scripts/multi_timeframe.py @@ -17,7 +17,7 @@ import urllib.error from datetime import datetime, date, timedelta from typing import Optional -DATA_DIR = "/home/hmo/web-dashboard/data" +DATA_DIR = "/home/hmo/MoFin/data" HISTORY_PATH = os.path.join(DATA_DIR, "price_history.json") # multi_tf_cache.json 已迁移到 DB (mtf_cache 表) @@ -92,7 +92,7 @@ def _load_mtf_cache(): return _MTF_CACHE_DATA try: import sqlite3 - db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') + db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') rows = db.execute("SELECT code, cache_json FROM mtf_cache").fetchall() _MTF_CACHE_DATA = {} for code, json_str in rows: @@ -113,7 +113,7 @@ def _save_mtf_cache(): return try: import sqlite3 - db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') + db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') for code, data in _MTF_CACHE_DATA.items(): db.execute( "INSERT OR REPLACE INTO mtf_cache (code, cache_json, updated_at) VALUES (?,?,datetime('now','localtime'))", @@ -222,9 +222,21 @@ def calc_moving_averages(klines: list, windows: list = [5, 10, 20, 60]) -> dict: return {f"ma{w}": None for w in windows} # 确保按时间正序(旧的在前) + # 使用日期判断顺序(不能用价格:下跌趋势下closes[0]>closes[-1]也会触发反转) closes = [k["close"] for k in klines] - # 检查是否倒序(最新的在前) - if len(closes) >= 2 and closes[0] > closes[-1]: + is_reversed = False + if len(klines) >= 2: + d0 = klines[0].get("date", "") + d1 = klines[-1].get("date", "") + if d0 and d1: + from datetime import datetime + try: + is_reversed = datetime.strptime(d0, "%Y-%m-%d") > datetime.strptime(d1, "%Y-%m-%d") + except: + # 日期格式不对时的fallback: 仅当首价显著高于末价才判定倒序(50%阈值) + is_reversed = (closes[0] > closes[-1] * 1.5) if len(closes) >= 2 else False + + if is_reversed: closes = list(reversed(closes)) result = {} diff --git a/scripts/per_stock_reassess.py b/scripts/per_stock_reassess.py index cee0bd6c..50d1047a 100644 --- a/scripts/per_stock_reassess.py +++ b/scripts/per_stock_reassess.py @@ -172,16 +172,10 @@ def main(): print(f"[ERROR] {code}: {e}", file=sys.stderr) errors += 1 - # 写回 decisions.json(只更新被修改的那条,其余保留原样) - raw["decisions"] = list(decisions_map.values()) - raw["total"] = len(raw["decisions"]) - from datetime import datetime - raw["regenerated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") - with open(DECISIONS_PATH, "w") as f: - json.dump(raw, f, ensure_ascii=False, indent=2) - + # 策略数据已通过DB写入(holding_strategies表),json.dump到decisions.json已废弃 # 同步自选股更新回 watchlist_stocks 表 try: + from datetime import datetime as _dt import sqlite3 _db2 = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') for _code in codes: @@ -203,7 +197,7 @@ def main(): "stop_loss": _entry.get("stop_loss", 0), "tech_snapshot": _entry.get("tech_snapshot", ""), "rr": _entry.get("rr_ratio", 0), - "reassessed_at": datetime.now().strftime("%Y-%m-%d") + "reassessed_at": _dt.now().strftime("%Y-%m-%d") }, ensure_ascii=False), _code )) diff --git a/scripts/pre-flight-check.py b/scripts/pre-flight-check.py index 5c6c91c9..00e1c61a 100644 --- a/scripts/pre-flight-check.py +++ b/scripts/pre-flight-check.py @@ -25,10 +25,10 @@ import sys from pathlib import Path from datetime import datetime, timezone +from mo_data import read_decisions + # === 路径 === REGISTRY = Path("/home/hmo/projects/MoFin/data/prompts/registry.json") -DECISIONS = Path("/home/hmo/web-dashboard/data/decisions.json") -PORTFOLIO = Path("/home/hmo/web-dashboard/data/portfolio.json") # === 检查 7:版本一致性 === def check_version_consistency(): @@ -62,33 +62,39 @@ def check_version_consistency(): # === 检查 1:数据时效 === def check_data_freshness(): - """检查 decisions.json 的更新时间是否在合理范围内""" + """检查 holding_strategies 表的最新更新时间""" + from datetime import datetime, timezone try: - mtime = DECISIONS.stat().st_mtime - mtime_dt = datetime.fromtimestamp(mtime, tz=timezone.utc) - now = datetime.now(timezone.utc) - age_minutes = (now - mtime_dt).total_seconds() / 60 + import sqlite3 + c = sqlite3.connect(str(WEB_DATA / "mofin.db")) + row = c.execute("SELECT MAX(updated_at) FROM holding_strategies WHERE status IN ('active','updated')").fetchone() + c.close() + if row and row[0]: + mtime_dt = datetime.strptime(row[0][:19], '%Y-%m-%d %H:%M:%S') + age_minutes = (datetime.now() - mtime_dt).total_seconds() / 60 + else: + return ("⚠️ 数据时效", "策略表无有效数据") except Exception as e: - return ("⚠️ 数据时效", f"无法检查 decisions.json: {e}") + return ("⚠️ 数据时效", f"无法检查策略表: {e}") if age_minutes < 60: return ("✅ 数据时效", - f"decisions.json 更新于 {age_minutes:.0f} 分钟前 ({mtime_dt.strftime('%H:%M')})") + f"策略数据 更新于 {age_minutes:.0f} 分钟前 ({mtime_dt.strftime('%H:%M')})") elif age_minutes < 240: return ("⚠️ 数据时效", - f"decisions.json 已 {age_minutes:.0f} 分钟未更新(可能已收盘),数据视为陈旧") + f"策略数据 已 {age_minutes:.0f} 分钟未更新(可能已收盘),数据视为陈旧") else: return ("❌ 数据时效", - f"decisions.json 已 {age_minutes:.0f} 分钟未更新,数据过期,请检查数据管线") + f"策略数据 已 {age_minutes:.0f} 分钟未更新,数据过期,请检查数据管线") # === 检查 5:成本有效 === def check_cost_validity(): """检查 decisions.json 中所有持仓的成本是否有效""" try: - dec = json.loads(DECISIONS.read_text()) + dec = read_decisions() except Exception as e: - return ("⚠️ 成本有效性", f"无法读取 decisions.json: {e}") + return ("⚠️ 成本有效性", f"无法读取 decisions: {e}") stocks = dec.get("stocks", dec.get("holdings", dec.get("strategies", []))) if not stocks: @@ -122,9 +128,9 @@ def check_cost_validity(): def check_stop_technical(code): """检查单只股票的止损是否基于技术位(仅对单股模式生效)""" try: - dec = json.loads(DECISIONS.read_text()) + dec = read_decisions() except Exception as e: - return ("⚠️ 止损技术位", f"无法读取 decisions.json: {e}") + return ("⚠️ 止损技术位", f"无法读取 decisions: {e}") stocks = dec.get("stocks", dec.get("strategies", [])) for s in stocks: @@ -150,9 +156,9 @@ def check_stop_technical(code): def check_rr(code, price=None): """检查单只股票的 R/R 是否达标""" try: - dec = json.loads(DECISIONS.read_text()) + dec = read_decisions() except Exception as e: - return ("⚠️ R/R 达标", f"无法读取 decisions.json: {e}") + return ("⚠️ R/R 达标", f"无法读取 decisions: {e}") stocks = dec.get("stocks", dec.get("strategies", [])) for s in stocks: diff --git a/scripts/price_data_inject.py b/scripts/price_data_inject.py index 6d5c43a0..004a98b3 100755 --- a/scripts/price_data_inject.py +++ b/scripts/price_data_inject.py @@ -6,6 +6,8 @@ import json import sys import os +from mo_data import read_portfolio, read_watchlist + PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" MTF_CACHE_PATH = "/home/hmo/web-dashboard/data/multi_tf_cache.json" @@ -160,7 +162,7 @@ def classify_from_cache(code, name, mtf_cache): def main(): try: - pf = json.load(open(PORTFOLIO_PATH)) + pf = read_portfolio() except Exception as e: print(f"[ERROR] {e}") sys.exit(0) @@ -220,7 +222,7 @@ def main(): # ===== 自选股买入区监控 ===== try: - wl = json.load(open(WATCHLIST_PATH)) + wl = read_watchlist() wl_stocks = wl.get("stocks", []) # 过滤掉已经在持仓里的 held_codes = {h["code"] for h in holdings} diff --git a/scripts/price_monitor.py b/scripts/price_monitor.py index 67d011b3..da254439 100644 --- a/scripts/price_monitor.py +++ b/scripts/price_monitor.py @@ -3,7 +3,6 @@ 规则:进入区间报一次,离开区间报一次,中间不重复。 每次运行时一次性刷新所有持仓+自选股的实时价。 """ -import json import urllib.request import os import sys @@ -11,9 +10,8 @@ import time import sqlite3 from datetime import datetime -DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" -PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" -WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" +from mo_data import read_decisions + BREACH_PATH = "/home/hmo/.hermes/zone_breach.json" STATE_PATH = os.path.expanduser("~/.hermes/price_trigger_state.json") EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json" @@ -319,10 +317,9 @@ def run_once(round_label=""): # === 第二步:检查触发条件 === try: - with open(DECISIONS_PATH) as f: - dec = json.load(f) + dec = read_decisions() except: - print(f"❌{label} 无法读取decisions.json", file=sys.stderr) + print(f"❌{label} 无法读取decisions(DB)", file=sys.stderr) return active = [d for d in dec.get("decisions", []) if d.get("status") == "active"] diff --git a/scripts/server.py b/scripts/server.py index 0b46cbbe..8eb6f143 100644 --- a/scripts/server.py +++ b/scripts/server.py @@ -21,7 +21,7 @@ from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, wr app = Flask(__name__, static_folder="static", static_url_path="") -DATA_DIR = Path(__file__).parent / "data" +DATA_DIR = Path(__file__).parent.parent / "data" UPLOAD_DIR = Path(__file__).parent / "uploads" # Hermes Gateway @@ -1038,7 +1038,7 @@ def update_realtime(): pf_holdings[code]["updated_at"] = datetime.now().isoformat() updated += 1 - # 也更新 watchlist.json + # 也更新 watchlist_stocks 表(DB) wl = read_watchlist() wl_stocks = {s["code"]: s for s in wl.get("stocks", [])} diff --git a/scripts/strategy_summary.py b/scripts/strategy_summary.py index e2fcc629..560d71d5 100644 --- a/scripts/strategy_summary.py +++ b/scripts/strategy_summary.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 """生成策略评估摘要""" -import json +from mo_data import read_decisions, read_portfolio -with open('/home/hmo/web-dashboard/data/decisions.json') as f: - dec = json.load(f) -with open('/home/hmo/MoFin/data/portfolio.json') as f: - pf = json.load(f) +dec = read_decisions() +pf = read_portfolio() holdings = pf.get('holdings', []) cash = pf.get('cash', 321271) diff --git a/scripts/strategy_tree.py b/scripts/strategy_tree.py index 3a8edfc3..337fd0ba 100644 --- a/scripts/strategy_tree.py +++ b/scripts/strategy_tree.py @@ -74,7 +74,7 @@ def detect_scenario(): # 优先 DB import sqlite3 from pathlib import Path - db = sqlite3.connect(str(Path(__file__).parent / "data" / "mofin.db")) + db = sqlite3.connect(str(Path(__file__).parent.parent / "data" / "mofin.db")) mrow = db.execute( "SELECT indices, structure, sector_mood FROM macro_context_log " "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" diff --git a/scripts/system_audit.py b/scripts/system_audit.py index 5feb9c7a..cae99f4b 100644 --- a/scripts/system_audit.py +++ b/scripts/system_audit.py @@ -202,33 +202,18 @@ def audit_pipeline(): except Exception as e: log_issue("数据管道", "HIGH", f"{name} 检查失败: {e}") - # 特殊检查:holding_strategies和decisions.json是否为空 + # 检查 holding_strategies 表策略数量 try: - hs = conn.execute("SELECT COUNT(*) FROM holding_strategies").fetchone()[0] - if hs == 0: - log_issue("数据管道", "HIGH", "holding_strategies表为空(策略评估产出未写入)", fix="检查holding_strategies写入逻辑") + hs_count = conn.execute("SELECT COUNT(*) FROM holding_strategies WHERE status IN ('active','updated')").fetchone()[0] + if hs_count < 5: + log_issue("数据管道", "HIGH", f"holding_strategies 仅{hs_count}条策略(异常)", fix="检查策略写入逻辑") else: - log_ok("数据管道", f"holding_strategies {hs}条") + log_ok("数据管道", f"holding_strategies {hs_count}条策略") except Exception as e: log_issue("数据管道", "HIGH", f"holding_strategies检查失败: {e}") conn.close() - - # 检查decisions.json文件 - try: - import json - with open(WEB_DATA / "decisions.json") as f: - dec = json.load(f) - cnt = len(dec.get("decisions", [])) - if cnt < 5: - log_issue("数据管道", "HIGH", f"decisions.json仅{cnt}条决策(异常)", fix="检查decisions.json写入逻辑") - else: - log_ok("数据管道", f"decisions.json {cnt}条决策") - except Exception as e: - log_issue("数据管道", "HIGH", f"decisions.json读取失败: {e}") - - -# ── 7. 系统服务 ── + def audit_services(): services = [ ("Dashboard", "http://127.0.0.1:8899/", "200"), diff --git a/scripts/system_health_check.py b/scripts/system_health_check.py index b22dbcde..8a8207fa 100644 --- a/scripts/system_health_check.py +++ b/scripts/system_health_check.py @@ -90,15 +90,15 @@ def run(): try: from mo_data import read_portfolio, read_decisions, read_watchlist pf = read_portfolio() - lines.append(check(len(pf.get("holdings", [])) > 0, f"portfolio.json DB记录: {len(pf.get('holdings', []))}条")) + lines.append(check(len(pf.get("holdings", [])) > 0, f"持仓 DB记录: {len(pf.get('holdings', []))}条")) ok_count += 1 wl = read_watchlist() - lines.append(check(len(wl.get("stocks", [])) > 0, f"watchlist.json DB记录: {len(wl.get('stocks', []))}条")) + lines.append(check(len(wl.get("stocks", [])) > 0, f"自选股 DB记录: {len(wl.get('stocks', []))}条")) ok_count += 1 dec = read_decisions() # 注意:holding_strategies 表是决策的DB版本 dec_count = len(dec.get("decisions", [])) - lines.append(check(dec_count > 0, f"decisions.json 记录: {dec_count}条(DB holding_strategies表: {len(pf.get('holdings',[]))}只)")) + lines.append(check(dec_count > 0, f"DB策略记录: {dec_count}条")) ok_count += 1 except Exception: lines.append(check(False, "MoFin DB 数据读取失败")) diff --git a/scripts/technical_analysis.py b/scripts/technical_analysis.py index 1f3037d9..5a6ddcae 100644 --- a/scripts/technical_analysis.py +++ b/scripts/technical_analysis.py @@ -472,7 +472,7 @@ def analyze_volume_deep(code): import sqlite3 from pathlib import Path - DATA_DIR = Path(__file__).parent / "data" + DATA_DIR = Path(__file__).parent.parent / "data" try: conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) row = conn.execute("SELECT cache_json FROM mtf_cache WHERE code=?", (code,)).fetchone() diff --git a/scripts/xiaoguo_signal_consumer.py b/scripts/xiaoguo_signal_consumer.py index 59ea3195..23416633 100644 --- a/scripts/xiaoguo_signal_consumer.py +++ b/scripts/xiaoguo_signal_consumer.py @@ -246,7 +246,7 @@ def main(): if action == "watchlist": # 加自选 results.append(f"✅ {sector_name}({code}): {summary}") - # 写入 watchlist.json + # 写入 watchlist_stocks 表(DB) try: wl = read_watchlist() wl.setdefault("stocks", []) diff --git a/static/mofin_health.json b/static/mofin_health.json new file mode 100644 index 00000000..7315512d --- /dev/null +++ b/static/mofin_health.json @@ -0,0 +1,1791 @@ +{ + "generated_at": "2026-07-08 12:30:50", + "feature_tree": { + "label": "MoFin 系统", + "status": "warn", + "children": [ + { + "label": "数据采集", + "status": "warn", + "children": [ + { + "label": "市场快照 (market_watch.py)", + "status": "ok" + }, + { + "label": "宏观新闻 (macro_context_collector.py)", + "status": "ok" + }, + { + "label": "价格监控 (price_monitor.py)", + "status": "ok" + }, + { + "label": "小果扫描 (xiaoguo_scanner.py)", + "status": "warn" + }, + { + "label": "资金流采集 (capital_flow_collector.py)", + "status": "ok" + }, + { + "label": "宏观上下文刷新 (refresh_macro_context.py)", + "status": "ok" + } + ] + }, + { + "label": "策略分析", + "status": "ok", + "children": [ + { + "label": "策略重评 (mofin_collect→reassess_with_context)", + "status": "ok" + }, + { + "label": "持仓自选新鲜度检查", + "status": "ok" + }, + { + "label": "自选买入区提醒 (stale_push_wlin.py)", + "status": "ok" + }, + { + "label": "策略评估 (strategy_evaluator.py)", + "status": "ok" + }, + { + "label": "分支自成长 (branch_scanner.py)", + "status": "ok" + }, + { + "label": "元自成长 (meta_growth.py)", + "status": "ok" + } + ] + }, + { + "label": "推荐推送", + "status": "warn", + "children": [ + { + "label": "MoFin盘前中监控 (LLM cron)", + "status": "warn" + }, + { + "label": "MoFin午后监控 (LLM cron)", + "status": "warn" + }, + { + "label": "cron报告推XMPP (cron_to_xmpp.py)", + "status": "ok" + }, + { + "label": "开盘简报 (LLM cron)", + "status": "ok" + }, + { + "label": "收盘简报 (LLM cron)", + "status": "ok" + }, + { + "label": "市场精选推荐 (LLM cron)", + "status": "ok" + } + ] + }, + { + "label": "自检/审计", + "status": "ok", + "children": [ + { + "label": "系统全局审计 (system_audit.py)", + "status": "ok" + }, + { + "label": "全局cron健康监控 (cron_health_monitor.py)", + "status": "ok" + }, + { + "label": "重评管道审计 (verify_reassess_pipeline.py)", + "status": "ok" + }, + { + "label": "系统体检 (morning_health_check.py)", + "status": "ok" + }, + { + "label": "盘中自检 (intraday_health_check.py)", + "status": "ok" + }, + { + "label": "记忆守卫 (memory_guardian.py)", + "status": "ok" + }, + { + "label": "硬编码扫描 (hardcode_scanner.py)", + "status": "ok" + } + ] + }, + { + "label": "风险监控", + "status": "ok", + "children": [ + { + "label": "宏观风险扫描 (LLM cron)", + "status": "ok" + }, + { + "label": "宏观风险信号消费 (macro_signal_consumer.py)", + "status": "ok" + }, + { + "label": "跨市场背离检测 (divergence_detector.py)", + "status": "ok" + } + ] + }, + { + "label": "执行/修复", + "status": "ok", + "children": [ + { + "label": "自愈执行器 (self_todo_executor.py)", + "status": "ok" + }, + { + "label": "策略质量门禁 (review_needed_watchdog.py)", + "status": "ok" + }, + { + "label": "自选自动清理 (clean_watchlist.py)", + "status": "ok" + }, + { + "label": "建议对账 (advice_reconciliation.py)", + "status": "ok" + } + ] + }, + { + "label": "系统服务", + "status": "ok", + "children": [ + { + "label": "XMPP Bot (zhiwei)", + "status": "ok" + }, + { + "label": "Gateway (8643)", + "status": "ok" + }, + { + "label": "HTTP Bridge (5805)", + "status": "ok" + }, + { + "label": "Dashboard (8899)", + "status": "ok" + }, + { + "label": "state.db SQLite", + "status": "ok" + } + ] + } + ] + }, + "entities": [ + { + "name": "accuracy_stats", + "desc": "建议准确率统计", + "rows": 1, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "strategy_review", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "advice_timeline", + "desc": "建议执行时间线", + "rows": 12735, + "readers": [ + "advice_reconciliation", + "mofin_db", + "migrate_all" + ], + "writers": [ + "advice_reconciliation", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "candidate_score_history", + "desc": "", + "rows": 133, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "candidates", + "desc": "潜力股候选池(小果扫描产出)", + "rows": 12, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "capital_flow_cache", + "desc": "资金流缓存", + "rows": 0, + "readers": [ + "mofin_db" + ], + "writers": [ + "mofin_db" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "cash_log", + "desc": "资金变动记录", + "rows": 13, + "readers": [ + "mofin_db" + ], + "writers": [ + "mofin_db", + "mo_data" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "health_check_log", + "desc": "健康检查日志", + "rows": 12, + "readers": [ + "morning_health_check" + ], + "writers": [ + "morning_health_check" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "holding_strategies", + "desc": "每只股票的完整策略参数", + "rows": 0, + "readers": [ + "stale_push_wlin", + "deep_check_01888", + "strategy_lifecycle", + "fix_and_regenerate", + "mofin_collect", + "run_all_tests", + "system_audit", + "pre-flight-check", + "verify_reassess_pipeline", + "migrate_all" + ], + "writers": [ + "fix_and_regenerate", + "fix_decisions_currency", + "migrate_all", + "data_governance", + "mofin_db", + "rollback_currency" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": true + }, + { + "name": "holdings", + "desc": "当前持仓(权威源)", + "rows": 19, + "readers": [ + "stale_push_wlin", + "fix_summary", + "server", + "fix_currency_and_summary", + "refresh_mtf_cache", + "migrate_all", + "mo_alphasift_bridge", + "per_stock_reassess", + "verify_01888", + "rollback_currency" + ], + "writers": [ + "fix_currency_and_summary", + "migrate_all", + "fix_hk_cost_cny", + "import_holding_xls", + "mofin_db", + "rollback_currency" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "live_prices", + "desc": "所有持仓+自选最新实时价", + "rows": 19, + "readers": [ + "cron_health_monitor", + "verify_reassess_pipeline", + "mofin_db", + "system_audit" + ], + "writers": [ + "mofin_db" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "macro_context_log", + "desc": "宏观上下文(大盘偏向/指数)", + "rows": 7, + "readers": [ + "stale_push_wlin", + "divergence_detector", + "strategy_lifecycle", + "stock_profile", + "system_audit", + "xiaoguo_signal_consumer", + "strategy_tree" + ], + "writers": [ + "refresh_macro_context" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "macro_raw_news", + "desc": "宏观新闻原始数据", + "rows": 7944, + "readers": [ + "macro_context_collector", + "system_audit" + ], + "writers": [ + "macro_context_collector" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "market_snapshots", + "desc": "大盘指数快照(每10分)", + "rows": 525, + "readers": [ + "trend_detector", + "system_audit", + "mofin_query", + "market_screener", + "mofin_db" + ], + "writers": [ + "mofin_db" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "mtf_cache", + "desc": "多周期均线缓存", + "rows": 34, + "readers": [ + "technical_analysis", + "multi_timeframe", + "mofin_db" + ], + "writers": [ + "multi_timeframe", + "mofin_db" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "portfolio_state", + "desc": "组合状态快照(只读派生)", + "rows": 1, + "readers": [], + "writers": [], + "has_input": false, + "has_output": false, + "orphan": true, + "warn": true + }, + { + "name": "portfolio_summary", + "desc": "总资产/现金/仓位汇总", + "rows": 1, + "readers": [ + "fix_summary", + "verify_pnl", + "fix_currency_and_summary", + "migrate_all", + "check_price_monitor", + "price_monitor", + "mo_data", + "rollback_currency", + "import_holding_xls", + "mofin_db" + ], + "writers": [ + "fix_summary", + "fix_currency_and_summary", + "migrate_all", + "import_holding_xls", + "mofin_db", + "rollback_currency" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "price_events", + "desc": "价格区间突破事件日志", + "rows": 2289, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "mofin_db", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "sector_signals", + "desc": "行业信号(趋势检测产出)", + "rows": 629, + "readers": [ + "mofin_news", + "xiaoguo_news_processor", + "trend_detector", + "server" + ], + "writers": [ + "mofin_news", + "xiaoguo_news_processor", + "trend_detector" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "sector_snapshots", + "desc": "行业板块数据", + "rows": 42310, + "readers": [ + "strategy_lifecycle", + "mofin_db", + "trend_detector", + "market_screener" + ], + "writers": [ + "mofin_db" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "signal_news", + "desc": "信号相关新闻", + "rows": 984, + "readers": [ + "xiaoguo_signal_consumer", + "system_audit", + "server", + "macro_signal_consumer", + "intraday_health_check" + ], + "writers": [ + "divergence_detector", + "xiaoguo_news_processor", + "xiaoguo_signal_consumer", + "macro_context_collector", + "xiaoguo_scanner", + "macro_signal_consumer", + "mofin_news" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "sqlite_sequence", + "desc": "", + "rows": 16, + "readers": [], + "writers": [], + "has_input": false, + "has_output": false, + "orphan": false, + "warn": false + }, + { + "name": "state_meta", + "desc": "系统状态元数据", + "rows": 1, + "readers": [ + "xiaoguo_scanner" + ], + "writers": [ + "xiaoguo_scanner" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stock_daily", + "desc": "日线行情", + "rows": 8615, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stock_fundamentals", + "desc": "基本面数据(PE/PB)", + "rows": 37, + "readers": [ + "strategy_lifecycle", + "migrate_all" + ], + "writers": [ + "mofin_db", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stock_monthly", + "desc": "月线行情", + "rows": 1196, + "readers": [ + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stock_sectors", + "desc": "股票行业映射", + "rows": 62, + "readers": [ + "strategy_lifecycle", + "xiaoguo_news_processor", + "trend_detector", + "migrate_all", + "mofin_news", + "mofin_db" + ], + "writers": [ + "mofin_db", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stock_weekly", + "desc": "周线行情", + "rows": 1988, + "readers": [ + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "stocks", + "desc": "全量股票代码", + "rows": 84, + "readers": [ + "xiaoguo_news_processor", + "trend_detector", + "migrate_all", + "mofin_news", + "mofin_db" + ], + "writers": [ + "mofin_db", + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "strategy_evaluations", + "desc": "策略重评历史记录", + "rows": 217, + "readers": [ + "verify_reassess_pipeline", + "mofin_db", + "migrate_all", + "system_audit" + ], + "writers": [ + "migrate_all", + "mofin_collect" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "strategy_feedback", + "desc": "策略效果反馈", + "rows": 222, + "readers": [ + "mofin_db", + "migrate_all" + ], + "writers": [ + "migrate_all" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "todos", + "desc": "自愈任务队列", + "rows": 61, + "readers": [ + "morning_health_check", + "intraday_health_check", + "strategy-staleness-check", + "self_todo_executor" + ], + "writers": [ + "morning_health_check", + "mofin_collect", + "self_todo_executor", + "intraday_health_check", + "strategy-staleness-check" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "watchlist_stocks", + "desc": "自选股列表", + "rows": 39, + "readers": [ + "stale_detector", + "stale_push_wlin", + "refresh_mtf_cache", + "trend_detector", + "run_all_tests", + "mofin_collect", + "xiaoguo_signal_consumer", + "xiaoguo_scanner", + "migrate_all", + "price_monitor" + ], + "writers": [ + "mofin_db", + "migrate_all", + "per_stock_reassess" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + }, + { + "name": "xiaoguo_scan_tracker", + "desc": "小果扫描跟踪", + "rows": 464, + "readers": [ + "xiaoguo_scanner", + "server" + ], + "writers": [ + "xiaoguo_scanner" + ], + "has_input": true, + "has_output": true, + "orphan": false, + "warn": false + } + ], + "json_files": [ + { + "name": "accuracy_stats.json", + "desc": "", + "size_kb": 7.3, + "readers": [], + "writers": [], + "last_modified": "07-06 11:32", + "warn": true + }, + { + "name": "analysis_output.json", + "desc": "", + "size_kb": 0.8, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "candidate_pool.json", + "desc": "潜力股候选池完整数据", + "size_kb": 43.6, + "readers": [], + "writers": [], + "last_modified": "07-07 14:07", + "warn": true + }, + { + "name": "capital_flow_cache.json", + "desc": "资金流缓存", + "size_kb": 0.1, + "readers": [], + "writers": [], + "last_modified": "07-01 15:32", + "warn": true + }, + { + "name": "daily_reviews.json", + "desc": "", + "size_kb": 9.0, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "decisions.json", + "desc": "策略决策(DB→JSON同步,兼容层)", + "size_kb": 21.8, + "readers": [ + "deep_check_01888", + "import_holding_xls", + "inspect_decisions", + "get_realtime_prices" + ], + "writers": [], + "last_modified": "07-08 11:42", + "warn": false + }, + { + "name": "decisions_backup.json", + "desc": "", + "size_kb": 9.8, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "decisions_backup_1129.json", + "desc": "", + "size_kb": 18.5, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "decisions_backup_before_tagfix.json", + "desc": "", + "size_kb": 133.7, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "evaluation.json", + "desc": "", + "size_kb": 51.1, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "evaluation_input.json", + "desc": "", + "size_kb": 48.4, + "readers": [], + "writers": [], + "last_modified": "07-06 11:32", + "warn": true + }, + { + "name": "growth_registry.json", + "desc": "", + "size_kb": 1.6, + "readers": [], + "writers": [], + "last_modified": "07-07 12:45", + "warn": true + }, + { + "name": "hardcode_audit.json", + "desc": "", + "size_kb": 3385.3, + "readers": [], + "writers": [], + "last_modified": "07-07 17:25", + "warn": true + }, + { + "name": "health_checklist.json", + "desc": "", + "size_kb": 12.2, + "readers": [], + "writers": [], + "last_modified": "07-08 09:59", + "warn": true + }, + { + "name": "live_prices.json", + "desc": "", + "size_kb": 2.8, + "readers": [], + "writers": [], + "last_modified": "07-06 11:00", + "warn": true + }, + { + "name": "macro_context.json", + "desc": "宏观上下文JSON(旧兼容层)", + "size_kb": 3.5, + "readers": [], + "writers": [], + "last_modified": "07-06 11:32", + "warn": true + }, + { + "name": "macro_divergence_state.json", + "desc": "", + "size_kb": 0.3, + "readers": [], + "writers": [], + "last_modified": "07-08 12:30", + "warn": true + }, + { + "name": "macro_risk_state.json", + "desc": "宏观风险状态(采集器写入)", + "size_kb": 1.6, + "readers": [], + "writers": [], + "last_modified": "07-08 12:30", + "warn": true + }, + { + "name": "mofin_health.json", + "desc": "健康监控数据", + "size_kb": 41.4, + "readers": [], + "writers": [], + "last_modified": "07-08 12:23", + "warn": true + }, + { + "name": "multi_tf_cache.json", + "desc": "多周期均线缓存", + "size_kb": 1006.0, + "readers": [], + "writers": [], + "last_modified": "07-06 11:32", + "warn": true + }, + { + "name": "new_opportunities.json", + "desc": "", + "size_kb": 1.7, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "pipeline_registry.json", + "desc": "", + "size_kb": 22.8, + "readers": [], + "writers": [], + "last_modified": "07-08 08:01", + "warn": true + }, + { + "name": "portfolio.json", + "desc": "持仓汇总(兼容层)", + "size_kb": 0.2, + "readers": [ + "get_realtime_prices" + ], + "writers": [], + "last_modified": "07-08 09:58", + "warn": false + }, + { + "name": "price_events.json", + "desc": "", + "size_kb": 247.1, + "readers": [], + "writers": [], + "last_modified": "07-06 11:32", + "warn": true + }, + { + "name": "price_history.json", + "desc": "", + "size_kb": 11.7, + "readers": [], + "writers": [], + "last_modified": "07-08 12:29", + "warn": true + }, + { + "name": "push_cooldown.json", + "desc": "", + "size_kb": 0.4, + "readers": [], + "writers": [], + "last_modified": "06-30 14:31", + "warn": true + }, + { + "name": "scanner_state.json", + "desc": "", + "size_kb": 0.1, + "readers": [], + "writers": [], + "last_modified": "07-08 12:30", + "warn": true + }, + { + "name": "sector_recommendation_test.json", + "desc": "", + "size_kb": 1.3, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "stock_name_code_cache.json", + "desc": "", + "size_kb": 1.7, + "readers": [], + "writers": [], + "last_modified": "06-22 19:58", + "warn": true + }, + { + "name": "stock_profiles.json", + "desc": "", + "size_kb": 10.9, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "stock_sector_map.json", + "desc": "", + "size_kb": 2.0, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "strategy_feedback.json", + "desc": "", + "size_kb": 16.8, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "strategy_staleness_report.json", + "desc": "策略过期报告", + "size_kb": 0.3, + "readers": [], + "writers": [], + "last_modified": "07-08 09:00", + "warn": true + }, + { + "name": "system_audit.json", + "desc": "", + "size_kb": 0.9, + "readers": [], + "writers": [], + "last_modified": "07-07 17:33", + "warn": true + }, + { + "name": "system_audit_report.json", + "desc": "", + "size_kb": 2.1, + "readers": [], + "writers": [], + "last_modified": "07-08 11:13", + "warn": true + }, + { + "name": "system_inventory.json", + "desc": "全量系统清单", + "size_kb": 354.8, + "readers": [], + "writers": [], + "last_modified": "07-08 11:34", + "warn": true + }, + { + "name": "system_state.json", + "desc": "", + "size_kb": 1.1, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + }, + { + "name": "xiaoguo_insights.json", + "desc": "小果分析洞察", + "size_kb": 2.2, + "readers": [], + "writers": [], + "last_modified": "07-07 16:14", + "warn": true + }, + { + "name": "xiaoguo_sentiment.json", + "desc": "", + "size_kb": 2.7, + "readers": [], + "writers": [], + "last_modified": "06-20 12:15", + "warn": true + } + ], + "pipelines": [ + { + "name": "300308入场信号紧盯", + "type": "no_agent", + "script": "monitor_300308.py", + "schedule": "*/2 13-14 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T14:58:22" + }, + { + "name": "300308午后紧盯", + "type": "no_agent", + "script": "300308_monitor.py", + "schedule": "*/2 13-14 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T14:58:22" + }, + { + "name": "Cron监护-高频", + "type": "no_agent", + "script": "cron_watchdog.py", + "schedule": "*/15 9-16 * * 1-5", + "status": "error", + "last_run": "2026-07-08T12:15:29" + }, + { + "name": "MoFin 午后监控", + "type": "LLM", + "script": "mofin_collect.py", + "schedule": "5,20,35,50 13-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T15:56:05" + }, + { + "name": "MoFin 盘前中监控", + "type": "LLM", + "script": "mofin_collect.py", + "schedule": "25,40,55,10 9-11 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:20:23" + }, + { + "name": "MoFin 系统常规体检-开盘前", + "type": "no_agent", + "script": "morning_health_check.py", + "schedule": "0 8 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T08:01:34" + }, + { + "name": "bot-process-monitor", + "type": "no_agent", + "script": "bot_monitor.sh", + "schedule": "every 60m", + "status": "ok", + "last_run": "2026-07-08T11:39:34" + }, + { + "name": "cron-推XMPP中继", + "type": "no_agent", + "script": "cron_to_xmpp.py", + "schedule": "* 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:29:57" + }, + { + "name": "cron报告推XMPP-每5分", + "type": "no_agent", + "script": "cron_to_xmpp.py", + "schedule": "*/5 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:25:17" + }, + { + "name": "evolution-pulse", + "type": "LLM", + "script": null, + "schedule": "0 * * * *", + "status": "ok", + "last_run": "2026-07-08T12:08:01" + }, + { + "name": "state.db真空整理-每周", + "type": "no_agent", + "script": "vacuum_state_db.py", + "schedule": "0 3 * * 6", + "status": null, + "last_run": "None" + }, + { + "name": "wiki-self-growth", + "type": "LLM", + "script": "wiki_health_aggregate.py", + "schedule": "0 3 * * *", + "status": "ok", + "last_run": "2026-07-08T03:07:57" + }, + { + "name": "xiaoguo-quick-scan", + "type": "no_agent", + "script": "xiaoguo_quick_scan.py", + "schedule": "*/15 * * * *", + "status": "ok", + "last_run": "2026-07-08T12:15:33" + }, + { + "name": "xiaoguo-tunnel-watchdog", + "type": "no_agent", + "script": "xiaoguo_tunnel_watchdog.sh", + "schedule": "*/5 * * * *", + "status": "ok", + "last_run": "2026-07-08T12:25:55" + }, + { + "name": "价格监控-高频", + "type": "no_agent", + "script": "price_monitor.py", + "schedule": "*/2 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:28:34" + }, + { + "name": "健康监控数据采集-每15分", + "type": "no_agent", + "script": "mofin_health.py", + "schedule": "*/15 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:16:04" + }, + { + "name": "元自成长-每日", + "type": "no_agent", + "script": "meta_growth.py", + "schedule": "45 0,12 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T00:45:33" + }, + { + "name": "全局cron健康监控-每10分", + "type": "no_agent", + "script": "cron_health_monitor.py", + "schedule": "*/10 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:21:40" + }, + { + "name": "分支剪枝-每日", + "type": "no_agent", + "script": "prune_branches.py", + "schedule": "0 21 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T21:00:02" + }, + { + "name": "分支自成长-盘中", + "type": "no_agent", + "script": "branch_scanner.py", + "schedule": "15,30,45,00 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:15:57" + }, + { + "name": "分析师-持仓复查", + "type": "LLM", + "script": null, + "schedule": "0 20 * * 4", + "status": "ok", + "last_run": "2026-07-02T20:12:33" + }, + { + "name": "多周期缓存刷新-开盘前", + "type": "no_agent", + "script": "refresh_mtf_cache.py", + "schedule": "0 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:01:47" + }, + { + "name": "多周期缓存刷新-盘中", + "type": "no_agent", + "script": "refresh_mtf_cache.py", + "schedule": "0 11,14 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T11:02:47" + }, + { + "name": "大脑任务执行", + "type": "LLM", + "script": "", + "schedule": "{'kind': 'cron', 'expr': '*/10 * * * *'}", + "status": "ok", + "last_run": "2026-07-08T12:21:19" + }, + { + "name": "宏观上下文刷新-每30分", + "type": "no_agent", + "script": "refresh_macro_context.py", + "schedule": "*/30 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:30" + }, + { + "name": "宏观新闻采集", + "type": "no_agent", + "script": "macro_context_collector.py", + "schedule": "*/30 8-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:37" + }, + { + "name": "宏观新闻采集-周末", + "type": "no_agent", + "script": "macro_context_collector.py", + "schedule": "0 8-16/2 * * 0,6", + "status": "ok", + "last_run": "2026-07-05T16:00:46" + }, + { + "name": "宏观采集-午间", + "type": "no_agent", + "script": "macro_context_collector.py", + "schedule": "0 12 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:43" + }, + { + "name": "宏观采集-早盘", + "type": "no_agent", + "script": "macro_context_collector.py", + "schedule": "35 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:35:52" + }, + { + "name": "宏观风险信号消费", + "type": "no_agent", + "script": "macro_signal_consumer.py", + "schedule": "5,20,35,50 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:21:09" + }, + { + "name": "宏观风险扫描", + "type": "LLM", + "script": null, + "schedule": "30 8 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T08:39:56" + }, + { + "name": "宏观风险扫描-午间", + "type": "LLM", + "script": null, + "schedule": "30 11 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T11:37:35" + }, + { + "name": "宏观风险扫描-周末", + "type": "LLM", + "script": null, + "schedule": "0 10 * * 0,6", + "status": "ok", + "last_run": "2026-07-05T10:09:55" + }, + { + "name": "小果信号消费-盘中", + "type": "no_agent", + "script": "xiaoguo_signal_consumer.py", + "schedule": "0,15,30,45 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:15:58" + }, + { + "name": "小果市场筛选-全市场", + "type": "no_agent", + "script": "market_screener.py", + "schedule": "35 15 * * 1-5", + "status": "error", + "last_run": "2026-07-07T15:37:36" + }, + { + "name": "小果情感分析", + "type": "LLM", + "script": null, + "schedule": "0 16 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T16:16:08" + }, + { + "name": "小果独立扫描", + "type": "no_agent", + "script": "xiaoguo_scanner.py", + "schedule": "*/5 9-15 * * 1-5", + "status": "error", + "last_run": "2026-07-08T12:27:20" + }, + { + "name": "市场数据采集", + "type": "no_agent", + "script": "market_watch.py", + "schedule": "*/10 9-11,13-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T11:50:54" + }, + { + "name": "市场数据采集", + "type": "no_agent", + "script": "market_watch.py", + "schedule": "0,30 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:19" + }, + { + "name": "市场精选推荐-每日", + "type": "LLM", + "script": null, + "schedule": "0 16 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T16:12:45" + }, + { + "name": "建议对账-每周", + "type": "no_agent", + "script": "advice_reconciliation.py", + "schedule": "0 20 * * 6", + "status": "ok", + "last_run": "2026-07-04T20:00:43" + }, + { + "name": "开盘简报", + "type": "LLM", + "script": null, + "schedule": "35 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:43:31" + }, + { + "name": "持仓情报-盘后", + "type": "LLM", + "script": null, + "schedule": "0 20 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T20:20:45" + }, + { + "name": "收盘简报", + "type": "LLM", + "script": null, + "schedule": "10 16 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T16:15:26" + }, + { + "name": "数据同步-dashboard", + "type": "no_agent", + "script": "sync_dashboard.py", + "schedule": "55 8 * * *", + "status": "ok", + "last_run": "2026-07-08T08:55:56" + }, + { + "name": "数据治理-每周", + "type": "no_agent", + "script": "data_governance.py", + "schedule": "0 10 * * 6", + "status": "error", + "last_run": "2026-07-04T10:00:57" + }, + { + "name": "数据采集-策略评估前", + "type": "no_agent", + "script": "collect_evaluation_data.py", + "schedule": "30 20 * * 1-5", + "status": "error", + "last_run": "2026-07-07T20:30:58" + }, + { + "name": "梦境循环-知识库归并", + "type": "LLM", + "script": null, + "schedule": "0 22 * * *", + "status": "ok", + "last_run": "2026-07-07T22:18:30" + }, + { + "name": "盘中自检-高频", + "type": "no_agent", + "script": "intraday_health_check.py", + "schedule": "*/15 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:15:59" + }, + { + "name": "盘前热点扫描", + "type": "LLM", + "script": null, + "schedule": "30 8 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T08:39:50" + }, + { + "name": "知微周复盘", + "type": "LLM", + "script": null, + "schedule": "0 22 * * 0", + "status": "ok", + "last_run": "2026-07-05T22:05:42" + }, + { + "name": "知微洞察生成", + "type": "no_agent", + "script": "market_insight.py", + "schedule": "35 15 * * 1-5", + "status": "error", + "last_run": "2026-07-07T15:35:35" + }, + { + "name": "知识研究-日常", + "type": "LLM", + "script": null, + "schedule": "0 22 * * *", + "status": "ok", + "last_run": "2026-07-07T22:10:24" + }, + { + "name": "硬编码扫描-每日", + "type": "no_agent", + "script": "hardcode_scanner.py", + "schedule": "25 17 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T17:25:58" + }, + { + "name": "策略复盘-每日", + "type": "no_agent", + "script": "strategy_review.py", + "schedule": "0 20 * * 1-5", + "status": "error", + "last_run": "2026-07-07T20:00:53" + }, + { + "name": "策略时效性检查(日)", + "type": "LLM", + "script": "strategy-staleness-check.py", + "schedule": "0 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:10:49" + }, + { + "name": "策略评估-每周", + "type": "no_agent", + "script": "strategy_evaluator.py", + "schedule": "0 21 * * 6", + "status": "error", + "last_run": "2026-07-04T21:00:51" + }, + { + "name": "策略评估-每日", + "type": "LLM", + "script": "stale_detector.py", + "schedule": "0 21 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T21:16:47" + }, + { + "name": "策略质量门禁-重评跟进", + "type": "no_agent", + "script": "review_needed_watchdog.py", + "schedule": "0,30 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:30:45" + }, + { + "name": "系统健康检查-开盘前", + "type": "LLM", + "script": "system_health_check.py", + "schedule": "0 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:32:03" + }, + { + "name": "系统健康检查-每日", + "type": "no_agent", + "script": "system_health_check.py", + "schedule": "0 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:01:03" + }, + { + "name": "系统全局审计", + "type": "LLM", + "script": "system_audit.py", + "schedule": "30 17 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T17:34:37" + }, + { + "name": "自愈执行器-TODO自动处理", + "type": "no_agent", + "script": "self_todo_executor.py", + "schedule": "*/10 8-22 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:30:47" + }, + { + "name": "自成长知识库-22:10中继推送", + "type": "no_agent", + "script": "relay_knowledge_report.py", + "schedule": "10 22 * * *", + "status": "ok", + "last_run": "2026-07-07T22:10:03" + }, + { + "name": "自选买入区提醒", + "type": "no_agent", + "script": "stale_push_wlin.py", + "schedule": "1,31 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:02:34" + }, + { + "name": "自选买入区提醒-盘前午间尾盘", + "type": "no_agent", + "script": "per_stock_reassess.py", + "schedule": "0 9,12,15 * * 1-5", + "status": "error", + "last_run": "2026-07-08T12:03:23" + }, + { + "name": "自选股体检-每周", + "type": "LLM", + "script": null, + "schedule": "0 20 * * 6", + "status": "ok", + "last_run": "2026-07-04T20:13:24" + }, + { + "name": "自选股自动重评-周末", + "type": "no_agent", + "script": "stale_detector.py", + "schedule": "30 10 * * 0,6", + "status": null, + "last_run": "None" + }, + { + "name": "自选自动清理-开盘前", + "type": "no_agent", + "script": "clean_watchlist.py", + "schedule": "5 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:05:08" + }, + { + "name": "芯碁微装-价格监控", + "type": "LLM", + "script": "price_monitor.py", + "schedule": "0 10,14 * * 1-5", + "status": "ok", + "last_run": "2026-07-07T14:13:27" + }, + { + "name": "记忆守卫-每日", + "type": "no_agent", + "script": "memory_guardian.py", + "schedule": "0 7 * * *", + "status": "ok", + "last_run": "2026-07-08T07:01:31" + }, + { + "name": "记忆守卫-每日", + "type": "no_agent", + "script": "memory_guardian.py", + "schedule": "0 7 * * *", + "status": "ok", + "last_run": "2026-07-08T07:02:21" + }, + { + "name": "资金流采集-盘中", + "type": "no_agent", + "script": "capital_flow_collector.py", + "schedule": "1,31 9-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:02:26" + }, + { + "name": "跨市场背离检测", + "type": "no_agent", + "script": "divergence_detector.py", + "schedule": "*/30 8-15 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:20" + }, + { + "name": "跨市场背离检测-周末", + "type": "no_agent", + "script": "divergence_detector.py", + "schedule": "30 8-16/2 * * 0,6", + "status": "ok", + "last_run": "2026-07-05T16:30:50" + }, + { + "name": "重评管道审计-每30分", + "type": "no_agent", + "script": "verify_reassess_pipeline.py", + "schedule": "*/30 9-16 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T12:01:24" + }, + { + "name": "集合竞价观察", + "type": "LLM", + "script": null, + "schedule": "28 9 * * 1-5", + "status": "ok", + "last_run": "2026-07-08T09:46:38" + } + ] +} \ No newline at end of file diff --git a/strategy_lifecycle.py.bak.1783484714 b/strategy_lifecycle.py.bak.1783484714 new file mode 100644 index 00000000..02643989 --- /dev/null +++ b/strategy_lifecycle.py.bak.1783484714 @@ -0,0 +1,2610 @@ +#!/usr/bin/env python3 +"""策略生命周期管理系统 — 技术面驱动版本 v2 + +核心原则: +1. 止损放在合理的技术位,不拍数字 +2. 新买入推荐:止损=弱支撑(约3%跌幅),止盈=强压力,盈亏比≥2:1 +3. 已持仓:止损=强支撑(约5-8%跌幅),目标=强压力 +4. 买入区间:弱支撑~弱压力之间 +5. 买入时机:量价齐跌不买,缩量至支撑买,量价齐升追买 +""" + +import json +import urllib.request +import os +import sys +import re +from datetime import datetime +import technical_analysis as ta +import multi_timeframe as mtf +from mo_data import read_portfolio, read_decisions, read_watchlist +from mo_models import is_hk_stock, to_cny, get_hk_rate +from strategy_tree import detect_scenario + +# ─── 策略准入门禁 — 硬性质量红线 ─────────────────────────────── +# 每一条策略写入前必须过此门禁。不过的不得写入DB/JSON, +# 必须触发重评修复。代码层面硬拦截,不依赖prompt或文档。 +# +# 规则列表 + 严重程度 + 修复建议 +STRATEGY_QUALITY_GATES = [ + { + "id": "GATE_LOSS_EXISTS", + "desc": "止损必须存在且>0", + "check": lambda d: (d.get("stop_loss") or 0) > 0, + "severity": "CRITICAL", + "fix": "调用 technical_analysis 计算支撑位设置止损" + }, + { + "id": "GATE_PROFIT_EXISTS", + "desc": "止盈必须存在且>0(纯自选股可放宽)", + "check": lambda d: (d.get("take_profit") or 0) > 0, + "severity": "CRITICAL", + "fix": "调用 technical_analysis 计算阻力位设置止盈目标" + }, + { + "id": "GATE_SL_GTE_LOW", + "desc": "止损必须 ≤ 买入区下沿", + "check": lambda d: (d.get("stop_loss") or 0) <= (d.get("entry_low") or 99999), + "severity": "HIGH", + "fix": "止损不能高于买入区,调整止损至买入区以下" + }, + { + "id": "GATE_ENTRY_RANGE", + "desc": "买入区下沿 < 上沿", + "check": lambda d: (d.get("entry_low") or 0) < (d.get("entry_high") or 0), + "severity": "CRITICAL", + "fix": "entry_low=现价×0.95, entry_high=现价×1.05 取近似区间" + }, + { + "id": "GATE_RR_COMPUTED", + "desc": "买入推荐必须含RR", + "check": lambda d: not ("买入" in (d.get("timing_signal") or "") or "加仓" in (d.get("timing_signal") or "")) or (d.get("rr_ratio") or 0) > 0, + "severity": "HIGH", + "fix": "RR = (止盈-现价)/(现价-止损),数据齐全后自动算" + }, + { + "id": "GATE_RR_MINIMUM", + "desc": "买入推荐RR≥1.5(非买入信号跳过)", + "check": lambda d: not ("买入" in (d.get("timing_signal") or "") or "加仓" in (d.get("timing_signal") or "")) or (d.get("rr_ratio") or 0) >= 1.5, + "severity": "HIGH", + "fix": "RR不足→signal降级为'信号不充分',不进推荐区" + }, + { + "id": "GATE_SIGNAL_SHORT", + "desc": "timing_signal 必须是短词(2-4字)", + "check": lambda d: len((d.get("timing_signal") or "").strip().split()) <= 4 and (d.get("timing_signal") or "") not in ("neutral", ""), + "severity": "MEDIUM", + "fix": "使用短词:买入/加仓/观望/持有/关注/信号不充分" + }, + { + "id": "GATE_TECH_SNAPSHOT", + "desc": "tech_snapshot 必须包含技术位数值", + "check": lambda d: bool(d.get("tech_snapshot")) and any(c in d["tech_snapshot"] for c in "支撑阻力压强"), + "severity": "MEDIUM", + "fix": "tech_snapshot 包含强撑/弱撑/弱压/强压至少3个数值" + }, + { + "id": "GATE_CURRENCY_SET", + "desc": "港股必须标 currency=HKD(个股存原币种,汇总时由calc_total_assets转CNY)", + "check": lambda d: not is_hk_stock(d.get("code","")) or d.get("currency") == "HKD", + "severity": "HIGH", + "fix": "设置 d['currency']='HKD'" + }, + # --- 第4条 CRITICAL 红线:9维交叉验证 (2026-07-02 Dad要求) --- + # 策略不能只有价格数字,必须有证据经过了多维分析: + # 横切面: 大盘+行业+个股 | 纵切面: 基本面+消息面+技术面+资金流 + # 代码层面可验证: sector_context(行业) + signal_factors(多因子) 或 tech_snapshot + { + "id": "GATE_9D_ANALYSIS", + "desc": "策略必须经过多维分析(sector_context + signal_factors)", + "check": lambda d: ( + bool(d.get("sector_context") and str(d.get("sector_context","")).strip() not in ("neutral","","N/A","-")) + and ( + bool(d.get("signal_factors") and isinstance(d.get("signal_factors"), (list,tuple)) and len(d["signal_factors"]) >= 1) + or bool(d.get("tech_snapshot") and any(c in str(d.get("tech_snapshot","")) for c in "支撑阻力压强")) + ) + ), + "severity": "CRITICAL", + "fix": "重新运行 reassess_with_context() 完整重评确保 sector_context/signal_factors/tech_snapshot 均已填充" + }, +] + + +def _hk_stock(code): + return bool(len(str(code)) == 5 and str(code)[0] in ('0','1')) + +def _is_buy_signal_str(signal): + """买入/加仓/建仓类信号""" + if not signal: + return False + return any(kw in signal for kw in ["买入", "加仓", "建仓"]) + +# _is_buy_signal is defined later in this file (~line 1240) + +def validate_strategy(d, debug=True): + """策略评审:硬性门禁检查 + + 返回 (passed: bool, failures: list) + 任一 CRITICAL 失败 → 拒绝写入,标记 TODO 触发重评 + 任一 HIGH 失败 → 标记 quality_check=failed,写入但不出现在推荐区 + MEDIUM 失败 → 记录但不拦截 + """ + failures = [] + for gate in STRATEGY_QUALITY_GATES: + try: + ok = gate["check"](d) + except Exception as e: + ok = False + if debug: + print(f" [VALIDATE] {gate['id']} 检查异常: {e}", flush=True) + if not ok: + failures.append(gate) + if debug: + print(f" [VALIDATE] ✗ {gate['id']} ({gate['severity']}): {gate['desc']}", flush=True) + + passed = all(f["severity"] != "CRITICAL" for f in failures) + + if debug: + criticals = [f for f in failures if f["severity"] == "CRITICAL"] + highs = [f for f in failures if f["severity"] == "HIGH"] + if passed: + print(f" [VALIDATE] ✅ 通过 ({len(failures)}条警告)" if failures else " [VALIDATE] ✅ 全通过", flush=True) + else: + print(f" [VALIDATE] ❌ {len(criticals)}条CRITICAL未通过 → 拒绝写入", flush=True) + + return passed, failures + + +def enforce_strategy_quality(code, name, result): + """策略写入前的强制质量门禁 + + 三段自动修复: + - Round 1: 技术分析(ta.full_analysis/chip_sr) + - Round 2: DB + 价格百分比推算 + - Round 3: 最低可用策略标记强推 + 3轮全不过 → review_needed + """ + price = result.get("price", 0) or result.get("current", 0) or result.get("last_price", 0) + # 价格归零保护:自选股等场景可能没设price字段,尝试从TA/API获取 + if not price: + try: + import urllib.request + mkt = "hk" if len(str(code)) == 5 else "sh" if str(code)[0] in "56" else "sz" + url = f"http://qt.gtimg.cn/q={mkt}{code}" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + resp = urllib.request.urlopen(req, timeout=5).read() + text = resp.decode("gbk") + parts = text.split("~") + if len(parts) > 3: + p = float(parts[3]) + if p > 0: + price = p + result["price"] = p # 写回result,让后续修复流程用真实价 + except Exception: + pass + # 最后兜底 + if not price: + price = 100 # 极少数API全线挂掉时的兜底,但会尽量走TA技术支持 + code_str = str(code) + import sqlite3 # 本函数多处使用 + + def _db_sector(): + """从 DB 取行业名""" + try: + _db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5) + r = _db.execute("SELECT sector_name FROM stock_sectors WHERE code=?", (code_str,)).fetchone() + _db.close() + return r[0] if r else None + except: + return None + + def _fix_one(gate_id, round_num): + """对单个门禁执行修复。round_num越大修复越激进。""" + if gate_id == "GATE_LOSS_EXISTS" and (result.get("stop_loss") or 0) <= 0: + if round_num <= 2: + # Round 1-2: 技术分析算支撑 + tech = ta.full_analysis(code) + if tech and "support_resistance" in tech: + sr = tech["support_resistance"] + ws = sr.get("weak_support") + ss = sr.get("strong_support") + if ws and ws > 0: + result["stop_loss"] = round(ws, 2) + elif ss and ss > 0: + result["stop_loss"] = round(ss, 2) + elif price > 0: + result["stop_loss"] = round(price * 0.95, 2) + elif price > 0: + result["stop_loss"] = round(price * 0.95, 2) + else: + # Round 3: 强制fallback + if price > 0: + result["stop_loss"] = round(price * 0.90, 2) # 更宽 + else: + result["stop_loss"] = 1 + print(f" R{round_num} 止损={result.get('stop_loss',0)}", flush=True) + + if gate_id == "GATE_PROFIT_EXISTS" and (result.get("take_profit") or 0) <= 0: + if round_num <= 2: + tech = ta.full_analysis(code) + if tech and "support_resistance" in tech: + sr = tech["support_resistance"] + wr = sr.get("weak_resist") + sr_resist = sr.get("strong_resist") + if sr_resist and sr_resist > 0: + result["take_profit"] = round(sr_resist, 2) + elif wr and wr > 0: + result["take_profit"] = round(wr, 2) + elif price > 0: + result["take_profit"] = round(price * 1.08, 2) + elif price > 0: + result["take_profit"] = round(price * 1.08, 2) + else: + if price > 0: + result["take_profit"] = round(price * 1.20, 2) # 更宽 + else: + # price=0 → 从DB或API获取 + try: + import sqlite3 as _s3 + _db = _s3.connect("/home/hmo/web-dashboard/data/mofin.db", timeout=5) + _r = _db.execute("SELECT price FROM holdings WHERE code=?", (code_str,)).fetchone() + _db.close() + if _r and _r[0] and _r[0] > 0: + result["take_profit"] = round(float(_r[0]) * 1.20, 2) + else: + result["take_profit"] = 2 # 真的兜底 + except: + result["take_profit"] = 2 + print(f" R{round_num} 止盈={result.get('take_profit',0)}", flush=True) + + if gate_id == "GATE_ENTRY_RANGE" and ((result.get("entry_low") or 0) >= (result.get("entry_high") or 0) or (result.get("entry_low") or 0) <= 0): + p = price or 100 + sl = result.get("stop_loss", 0) + tp = result.get("take_profit", 0) + if round_num <= 2: + if sl > 0 and tp > 0 and sl < tp: + result["entry_low"] = round(sl * 1.02, 2) + result["entry_high"] = round(tp * 0.85, 2) + if result["entry_low"] >= result["entry_high"]: + result["entry_low"] = round(p * 0.95, 2) + result["entry_high"] = round(p * 0.99, 2) + else: + result["entry_low"] = round(p * 0.93, 2) + result["entry_high"] = round(p * 1.02, 2) + else: + result["entry_low"] = round(p * 0.90, 2) + result["entry_high"] = round(p * 1.10, 2) + print(f" R{round_num} 买入区={result['entry_low']}~{result['entry_high']}", flush=True) + + if gate_id == "GATE_9D_ANALYSIS": + # 行业 + if not result.get("sector_context") or str(result.get("sector_context","")).strip() in ("neutral","","N/A","-"): + sec = _db_sector() + if sec: + result["sector_context"] = sec + elif round_num >= 2: + result["sector_context"] = f"自选(未分类)" + else: + result["sector_context"] = f"{name}所属行业(待补充)" + # signal_factors + if not result.get("signal_factors") or (isinstance(result.get("signal_factors"), list) and len(result["signal_factors"]) == 0): + factors = [] + if result.get("timing_signal"): + factors.append(f"信号:{result['timing_signal']}") + if result.get("rr_ratio", 0) > 0: + factors.append(f"RR:{result['rr_ratio']}") + if result.get("stop_loss", 0) > 0 and result.get("take_profit", 0) > 0: + factors.append(f"损{result['stop_loss']}盈{result['take_profit']}") + if not factors: + if round_num >= 2: + factors.append("自动填充") + else: + # Round 1: 留空等重检,不硬填 + pass + if factors: + result["signal_factors"] = factors + # tech_snapshot + if not result.get("tech_snapshot") or not any(c in str(result.get("tech_snapshot","")) for c in "支撑阻力压强"): + sl = result.get("stop_loss", 0) + tp = result.get("take_profit", 0) + if sl > 0 and tp > 0: + result["tech_snapshot"] = f"自动:损{sl}盈{tp}" + elif price: + result["tech_snapshot"] = f"自动:价{price}" + elif round_num >= 2: + result["tech_snapshot"] = "自动生成(未补全技术位)" + print(f" R{round_num} 9维分析: sector={result.get('sector_context','')[:20]} factors={result.get('signal_factors',[])}", flush=True) + + # 循环重试 + MAX_RETRIES = 3 + passed, failures = validate_strategy(result) + retry_count = 0 + + for _retry_num in range(1, MAX_RETRIES + 1): + if passed: + break + + critical_issues = [f["id"] for f in failures if f["severity"] == "CRITICAL"] + if not critical_issues: + # 没有CRITICAL了,只有HIGH/MEDIUM → 可以放行 + passed = True + break + + retry_count = _retry_num + print(f" [RETRY {retry_count}/{MAX_RETRIES}] {name}({code}) → 修复: {critical_issues}", flush=True) + + for gate_id in critical_issues: + _fix_one(gate_id, retry_count) + + # 重检 + passed, failures = validate_strategy(result) + + # --- 最终结果 --- + if passed: + print(f" ✅ {name}({code}) 质量门禁通过 ({retry_count}轮重试)", flush=True) + result["quality_check"] = "passed" + result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") + + # HIGH 级别警告(已通过但仍有非CRITICAL失败) + high_fails = [f for f in failures if f["severity"] == "HIGH"] + if high_fails: + result["quality_check"] = "warning" + result["quality_issues"] = {"high": [f["id"] for f in high_fails]} + print(f" ⚠️ {name}({code}) 有{len(high_fails)}条HIGH警告", flush=True) + + # 记录 changelog + if "critical_issues" in dir(): + cl = result.setdefault("changelog", []) + cl.append({ + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "event": f"质量门禁通过 (重试{retry_count}轮)", + }) + + result["status"] = "active" + return True + else: + # 3轮全不过 → review_needed + remaining_critical = [f["id"] for f in failures if f["severity"] == "CRITICAL"] + result["quality_check"] = "failed" + result["quality_issues"] = { + "critical": remaining_critical, + "all": [f["id"] for f in failures], + } + result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") + result["status"] = "review_needed" + result["timing_signal"] = "信号不充分" + + cl = result.setdefault("changelog", []) + cl.append({ + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "event": f"质量门禁3轮全拒 → review_needed ({remaining_critical})", + }) + + print(f" 🚫 {name}({code}) 3轮修复后仍有 {remaining_critical} → review_needed", flush=True) + return False + + +# is_hk_stock 已从 mo_models 导入(见文件头部 import),不再在此复写。 + + +def calc_atr(code, period=14): + """从腾讯API K线数据计算ATR(period),返回ATR值或None""" + try: + url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param=hk{code},day,,,60,qfq" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + resp = urllib.request.urlopen(req, timeout=5).read().decode('utf-8') + data = json.loads(resp) + bars = data.get('data', {}).get(f'hk{code}', {}).get('day', []) + if len(bars) < period + 1: + return None + trs = [] + for i in range(1, min(len(bars), period + 1)): + try: + high = float(bars[i][2]) + low = float(bars[i][3]) + prev_close = float(bars[i-1][4]) if len(bars[i-1]) > 4 else float(bars[i-1][3]) + tr = max(high - low, abs(high - prev_close), abs(low - prev_close)) + trs.append(tr) + except (ValueError, IndexError): + continue + if not trs: + return None + return round(sum(trs) / len(trs), 2) + except Exception: + return None + + +def calc_chip_sr(code, price): + """从筹码分布计算支撑/阻力位。 + + 返回: {"chip_ss": 筹码强支撑, "chip_sr": 筹码强阻力} 或 None + 筹码强支撑 = 当前价下方成交量最大的价格区间 + 筹码强阻力 = 当前价上方成交量最大的价格区间 + + 用法: + sr = calc_chip_sr("600519", 1193) + if sr: + print(f"筹码支撑{sr['chip_ss']} 筹码阻力{sr['chip_sr']}") + """ + if not price or price <= 0: + return None + try: + # 复用chip_factors的筹码分布构建 + import sys as _sys + _sys.path.insert(0, "/home/hmo/MoFin/scripts") + from chip_factors import ChipFactors + cf = ChipFactors() + chip = cf._build_chip_distribution(code) + if not chip: + return None + total = sum(chip.values()) + if total <= 0: + return None + # 2%区间聚合 + step = max(round(price * 0.02, 2), 1.0) + bins = {} + for p, v in chip.items(): + k = round(p / step) * step + bins[k] = bins.get(k, 0) + v + sb = sorted(bins.items()) + below = [(p, v) for p, v in sb if p < price] + above = [(p, v) for p, v in sb if p >= price] + if not below or not above: + return None + + # 支撑 = 下方成交量最大的密集区 + chip_ss = max(below, key=lambda x: x[1])[0] + # 阻力 = 上方成交量最大的密集区 + chip_sr = max(above, key=lambda x: x[1])[0] + + return {"chip_ss": chip_ss, "chip_sr": chip_sr} + except Exception as e: + print(f" ⚠️ 筹码S/R计算失败: {e}", file=sys.stderr) + return None + +# 提示词版本追踪 +try: + from prompt_manager.tracking import record_strategy_generation + HAS_PROMPT_TRACKING = True +except ImportError: + HAS_PROMPT_TRACKING = False + +def safe_json_load(path, default=None): + """安全加载 JSON,遇到坏数据自动修复""" + if not os.path.exists(path): + return default if default is not None else {} + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError: + # 使用 json_validator 增强修复(更健壮的错误处理) + with open(path, "r", encoding="utf-8") as f: + raw = f.read() + + try: + import sys + sys.path.insert(0, "/home/hmo/MoFin") + from json_validator import validate_json_syntax, _record_format_error + + score, errors, parsed = validate_json_syntax(raw) + if parsed is not None and score >= 60: + if errors: + _record_format_error(raw, errors, 0) + return parsed + + _record_format_error(raw, errors, 0) + print(f"[WARN] {path} JSON 自动修复失败 (score={score})", file=sys.stderr) + return default if default is not None else {} + except ImportError: + pass + + # 尝试修复:字符串内未转义的换行符,去多余括号 + fixed = raw + result = [] + in_str = False + for ch in fixed: + if ch == '"': + in_str = not in_str + result.append(ch) + elif in_str and ch in '\n\r': + result.append('\\n') + else: + result.append(ch) + fixed = ''.join(result) + fixed = fixed.rstrip('}') + if not fixed.endswith('}'): + fixed += '}' + try: + return json.loads(fixed) + except json.JSONDecodeError as e: + print(f"[WARN] {path} 自动修复失败: {e}", file=sys.stderr) + return default if default is not None else {} +KNOWLEDGE_LOG = "/home/hmo/Obsidian/knowledge/finance/analyst-knowledge-log.md" +MACRO_CONTEXT_PATH = "/home/hmo/web-dashboard/data/macro_context.json" +MARKET_CONTEXT_PATH = "/home/hmo/web-dashboard/data/market.json" +STOCK_SECTOR_MAP_PATH = "/home/hmo/web-dashboard/data/stock_sector_map.json" + + +def load_stock_sector_map(): + """读取个股归属行业映射 + + stock_sector_map.json 格式: {code: [sector1, sector2, ...]} + 跳过 _note, _created_at 等元数据键。 + """ + # 优先从 SQLite 读取 + try: + from mofin_db import get_conn, query_sector_stocks + conn = get_conn() + # 从 stock_sectors 表反向构建 code→[sectors] 映射 + rows = conn.execute("SELECT code, sector_name FROM stock_sectors ORDER BY code").fetchall() + conn.close() + code_to_sectors = {} + for code, sector in rows: + if code not in code_to_sectors: + code_to_sectors[code] = [] + code_to_sectors[code].append(sector) + return code_to_sectors + except Exception: + pass + try: + with open(STOCK_SECTOR_MAP_PATH) as f: + data = json.load(f) + code_to_sectors = {} + for key, value in data.items(): + if key.startswith("_"): + continue + if isinstance(value, list): + code_to_sectors[key] = value + return code_to_sectors + except Exception: + return {} + + +def load_market_context(): + """读取市场上下文,优先 SQLite,回退 market.json""" + # 优先从 SQLite 读取 + try: + from mofin_db import get_conn, query_latest_market + conn = get_conn() + market = query_latest_market(conn) + conn.close() + if market and market.get("sectors"): + sector_perf = {} + for s in market["sectors"]: + name = s.get("name", "") + if name: + sector_perf[name] = { + "change": s.get("change_pct", 0), + "up_count": s.get("up_count", 0), + "down_count": s.get("down_count", 0), + "net_inflow": s.get("net_inflow", 0), + "lead_stock": s.get("lead_stock", ""), + "lead_stock_change": s.get("lead_stock_change", 0), + } + return { + "sector_perf": sector_perf, + "breadth": market.get("up_ratio", 50), + "mood": market.get("mood", "neutral"), + "top_gainers": {g["name"]: g["change_pct"] for g in market.get("top_gainers", [])}, + "top_losers": {g["name"]: g["change_pct"] for g in market.get("top_losers", [])}, + "total_sectors": len(market["sectors"]), + "market_timestamp": market.get("timestamp", ""), + } + except Exception: + pass + try: + with open(MARKET_CONTEXT_PATH) as f: + market = json.load(f) + sectors = market.get("sectors", []) + sector_perf = {} + for s in sectors: + name = s.get("name", "") + if name: + sector_perf[name] = { + "change": s.get("change", 0), + "up_count": s.get("up_count", 0), + "down_count": s.get("down_count", 0), + "net_inflow": s.get("net_inflow", 0), + "lead_stock": s.get("lead_stock", ""), + "lead_stock_change": s.get("lead_stock_change", 0), + } + top_gainers = {s.get("name", ""): s.get("change", 0) + for s in market.get("top_gainers", [])} + top_losers = {s.get("name", ""): s.get("change", 0) + for s in market.get("top_losers", [])} + return { + "sector_perf": sector_perf, + "breadth": market.get("up_ratio", 50), + "mood": market.get("mood", "neutral"), + "top_gainers": top_gainers, + "top_losers": top_losers, + "total_sectors": market.get("total_sectors", 0), + "market_timestamp": market.get("timestamp", ""), + } + except Exception: + return { + "sector_perf": {}, + "breadth": 50, + "mood": "neutral", + "top_gainers": {}, + "top_losers": {}, + "total_sectors": 0, + "market_timestamp": "", + } + + +def compute_sector_adjustment(code, market_ctx, stock_sector_map): + """根据个股所属行业的市场表现+小果情感,返回调整系数 + + 返回 dict: + stop_bias: 止损调整系数(<1.0收紧, >1.0放宽) + target_bias: 止盈调整系数 + note: 行业背景一句话 + sector_name: 匹配到的行业名称 + sector_change: 行业涨跌幅 + """ + # 默认无调整 + adj = {"stop_bias": 1.0, "target_bias": 1.0, "note": "", + "sector_name": "", "sector_change": 0} + + sectors_for_code = stock_sector_map.get(code, []) + if not sectors_for_code: + return adj + + sector_perf = market_ctx.get("sector_perf", {}) + breadth = market_ctx.get("breadth", 50) + + # 找第一个能匹配到的行业 + for sec in sectors_for_code: + if sec in sector_perf: + perf = sector_perf[sec] + chg = perf.get("change", 0) + adj["sector_name"] = sec + adj["sector_change"] = chg + + # 行业暴跌 > 3% + if chg <= -3: + adj["stop_bias"] = 0.92 # 止损收紧8% + adj["target_bias"] = 0.90 # 止盈下调10% + adj["note"] = f"行业{sec}大跌{chg:+.1f}%,收紧止损" + # 行业大跌 1~3% + elif chg <= -1: + adj["stop_bias"] = 0.96 + adj["target_bias"] = 0.95 + adj["note"] = f"行业{sec}下跌{chg:+.1f}%,适度防御" + # 行业大涨 > 3% + elif chg >= 3: + adj["stop_bias"] = 1.05 # 止损放宽5%(给趋势空间) + adj["target_bias"] = 1.03 + adj["note"] = f"行业{sec}大涨{chg:+.1f}%,可适度积极" + # 行业上涨 1~3% + elif chg >= 1: + adj["stop_bias"] = 1.02 + adj["note"] = f"行业{sec}上涨{chg:+.1f}%,正常" + else: + adj["note"] = f"行业{sec}{chg:+.1f}%,中性" + break + # 尝试处理命名差异:market.json中的行业名可能多了"板块"后缀 + for market_sec_name in sector_perf: + if sec in market_sec_name or market_sec_name in sec: + perf = sector_perf[market_sec_name] + chg = perf.get("change", 0) + adj["sector_name"] = market_sec_name + adj["sector_change"] = chg + if chg <= -3: + adj["stop_bias"] = 0.92 + adj["target_bias"] = 0.90 + adj["note"] = f"行业{market_sec_name}大跌{chg:+.1f}%,收紧止损" + elif chg <= -1: + adj["stop_bias"] = 0.96 + adj["target_bias"] = 0.95 + adj["note"] = f"行业{market_sec_name}下跌{chg:+.1f}%,适度防御" + elif chg >= 3: + adj["stop_bias"] = 1.05 + adj["target_bias"] = 1.03 + adj["note"] = f"行业{market_sec_name}大涨{chg:+.1f}%,可适度积极" + elif chg >= 1: + adj["stop_bias"] = 1.02 + adj["note"] = f"行业{market_sec_name}上涨{chg:+.1f}%,正常" + else: + adj["note"] = f"行业{market_sec_name}{chg:+.1f}%,中性" + break + + # 如果breath<30% (大盘极弱),再加一层收紧 + if breadth < 30: + adj["stop_bias"] *= 0.97 # 再收紧3% + breadth_note = "大盘仅{}%个股上涨".format(int(breadth)) + adj["note"] = (adj["note"] + " | " + breadth_note) if adj["note"] else breadth_note + elif breadth < 40: + adj["stop_bias"] *= 0.99 + breadth_note = "大盘偏弱({}%上涨)".format(int(breadth)) + adj["note"] = (adj["note"] + " | " + breadth_note) if adj["note"] else breadth_note + + # 小果情感约束:利空置信度>80%时收紧止损 + try: + xiaoguo_path = "/home/hmo/web-dashboard/data/xiaoguo_sentiment.json" + if os.path.exists(xiaoguo_path): + xg = json.load(open(xiaoguo_path)) + stock_sentiment = xg.get("stocks", {}).get(code, {}) + if stock_sentiment: + sentiment = stock_sentiment.get("sentiment", "") + confidence = stock_sentiment.get("confidence", 0) + summary = stock_sentiment.get("summary", "") + if sentiment == "negative" and confidence > 0.8: + adj["stop_bias"] = min(adj["stop_bias"], 0.95) + adj["note"] += f" | 小果利空{confidence:.0%}:{summary[:30]}" + except Exception: + pass + + return adj + + +def load_macro_context(): + """读取宏观上下文,返回 (bias, desc),优先 DB,回退 JSON""" + try: + from mofin_db import get_conn + conn = get_conn() + row = conn.execute( + "SELECT indices, structure FROM macro_context_log " + "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" + ).fetchone() + conn.close() + if row: + indices = json.loads(row[0]) if row[0] else {} + structure = json.loads(row[1]) if row[1] else {} + overall = structure.get("overall", "neutral") + desc = structure.get("description", "") + else: + raise ValueError("no db data") + except Exception: + try: + with open(MACRO_CONTEXT_PATH) as f: + ctx = json.load(f) + overall = ctx.get("structure", {}).get("overall", "neutral") + desc = ctx.get("structure", {}).get("description", "") + except Exception: + return 1.0, "宏观未加载" + if "bearish" in overall: + return 0.8, f"宏观{desc}" + elif overall == "bullish": + return 1.05, f"宏观{desc}" + elif overall == "strong_bullish": + return 1.1, f"宏观{desc}" + else: + return 1.0, f"宏观{desc}" + + +def batch_fetch_prices(codes): + """获取实时价格。优先从 DB 读取(price_monitor 每 2 分钟更新),失败才拉腾讯 API。""" + if not codes: + return {} + + all_results = {} + + # 主通道:从 DB 读取(price_monitor 唯一价格入口) + try: + from mofin_db import get_conn + db = get_conn() + for raw_code in codes: + raw_code = str(raw_code).split('_')[0] + if not raw_code: continue + row = db.execute( + "SELECT price, change_pct FROM holdings WHERE code=? AND is_active=1", (raw_code,) + ).fetchone() + if not row: + row = db.execute( + "SELECT price, change_pct FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,) + ).fetchone() + if row and row['price']: + all_results[raw_code] = { + "price": row['price'], + "close": row['price'], # 用现价近似昨收,仅用于sentiment计算 + "high": row['price'], + "low": row['price'], + "code": raw_code, + } + db.close() + if all_results: + return all_results + except Exception: + pass + + # Fallback: 腾讯 API(仅当 DB 无数据时) + batch_size = 15 + for batch_start in range(0, len(codes), batch_size): + batch = codes[batch_start:batch_start + batch_size] + symbols = [] + code_map = {} + for raw_code in batch: + raw_code = str(raw_code).split('_')[0] + if not raw_code: + continue + if len(raw_code) == 5 and raw_code.isdigit(): + prefix = "hk" + elif raw_code.startswith(("6", "5")): + prefix = "sh" + else: + prefix = "sz" + sym = f"{prefix}{raw_code}" + symbols.append(sym) + code_map[sym] = raw_code + if not symbols: + continue + + url = f"http://qt.gtimg.cn/q={','.join(symbols)}" + max_retries = 2 + for attempt in range(max_retries + 1): + try: + r = urllib.request.urlopen(url, timeout=10) + text = r.read().decode("gbk") + except Exception as e: + if attempt < max_retries: + continue + print(f" batch_fetch_prices error: {e}", file=sys.stderr) + continue + + for line in text.strip().split("\n"): + line = line.strip() + if not line or "=" not in line: + continue + try: + sym = line.split("=", 1)[0].strip().lstrip("v_") + raw_value = line.split("=", 1)[1].strip().strip('"').strip(";") + fields = raw_value.split("~") + if len(fields) < 35: + continue + orig_code = code_map.get(sym) + if not orig_code: + continue + def f(i): + try: + return float(fields[i]) if fields[i].strip() else 0.0 + except: + return 0.0 + price_raw = f(3) + # 港股:腾讯 API 返回 HKD,需转 CNY + if is_hk_stock(orig_code) and price_raw > 0: + price_raw = to_cny(price_raw) + all_results[orig_code] = { + "price": price_raw, "close": f(4), "high": f(33), "low": f(34), + "code": orig_code, + } + except Exception: + continue + break # Success - break retry loop + + return all_results + + +def get_price_tencent(code): + """获取实时价格。优先 DB(price_monitor 维护),失败才拉腾讯。港股价格已是 CNY。""" + raw_code = str(code).split('_')[0] + if not raw_code: + return None + + # 主通道: DB + try: + from mofin_db import get_conn + db = get_conn() + row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (raw_code,)).fetchone() + if not row: + row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)).fetchone() + if row and row['price']: + db.close() + return row['price'] + db.close() + except Exception: + pass + + # Fallback: 腾讯 API + try: + from mo_models import to_cny, is_hk_stock + except ImportError: + to_cny = lambda v, r=None: v + is_hk_stock = lambda c: len(str(c).strip()) == 5 and str(c).strip().isdigit() + try: + if is_hk_stock(raw_code): + prefix = "hk" + elif raw_code.startswith("6") or raw_code.startswith("5"): + prefix = "sh" + else: + prefix = "sz" + url = f"http://qt.gtimg.cn/q={prefix}{raw_code}" + r = urllib.request.urlopen(url, timeout=5) + fields = r.read().decode("gbk").split('"')[1].split("~") + def f(i): + try: + return float(fields[i]) if fields[i].strip() else 0.0 + except: + return 0.0 + price = f(3) + if is_hk_stock(raw_code) and price > 0: + price = to_cny(price) + return { + "price": price, "close": f(4), "high": f(33), "low": f(34), + "code": raw_code, + } + except Exception as e: + print(f" get_price error {code}: {e}", file=sys.stderr) + return None + + +def reassess_strategy(code, name, price, cost, shares, current_action, + volume_signal="", sentiment="neutral", + is_watchlist=False): + """根据技术分析重评策略""" + + tech = ta.full_analysis(code) + if tech and "support_resistance" in tech: + sr = tech["support_resistance"] + candle = tech.get("candlestick", {}) + vol = tech.get("volume", {}) + ss = sr.get("strong_support") + ws = sr.get("weak_support") + wr = sr.get("weak_resist") + sr_resist = sr.get("strong_resist") + pivot = sr.get("pivot") + effective_range = sr.get("effective_range") + print(f" TECH: 强撑={ss} 弱撑={ws} 枢轴={pivot} 弱压={wr} 强压={sr_resist} 有效区间={effective_range}") + else: + print(f" ⚠️ 技术分析不可用", file=sys.stderr) + ss = ws = wr = sr_resist = pivot = None + candle = {} + vol = {} + + # ----- 多周期技术分析(周线/月线/均线) ----- + mtf_analysis = {} + mtf_adj = {} + try: + mtf_result = mtf.full_multi_tf_analysis(code) + if mtf_result.get("daily") and mtf_result["daily"].get("count", 0) >= 5: + mtf_analysis = mtf_result + mtf_adj = mtf_result.get("strategy_adjustment", {}) + daily_mas = mtf_result.get("daily", {}).get("mas", {}) + weekly = mtf_result.get("weekly", {}) + monthly = mtf_result.get("monthly", {}) + trend_align = mtf_adj.get("trend_alignment", "未知") + print(f" 多周期: {trend_align} | " + f"MA5={daily_mas.get('ma5','?')} MA20={daily_mas.get('ma20','?')} MA60={daily_mas.get('ma60','?')} | " + f"周线{weekly.get('trend',{}).get('description','?')} 月线{monthly.get('trend',{}).get('description','?')}") + except Exception as e: + print(f" 多周期分析失败: {e}", file=sys.stderr) + + # ----- 筹码分布支撑/阻力(中长线参考,加情景权重) ----- + chip_sr = None + chip_weight = 0.5 # 默认中等权重 + regime = detect_scenario() + regime_id = regime.get("id", "weak_consolidation") + + # 情景决定筹码因子权重 + if regime_id == "weak_consolidation": + chip_weight = 0.9 # 震荡市筹码最准 + elif regime_id == "bullish_recovery": + chip_weight = 0.4 # 上涨趋势筹码阻力可能被突破 + elif regime_id == "sharp_decline": + chip_weight = 0.2 # 急跌中筹码支撑可能失效 + elif regime_id == "sector_rotation": + chip_weight = 0.6 # 轮动市中筹码有一定参考 + + try: + chip_sr = calc_chip_sr(code, price) + if chip_sr: + print(f" 筹码: 撑={chip_sr['chip_ss']:.0f} 阻={chip_sr['chip_sr']:.0f} | 情景={regime_id} 权重={chip_weight:.1f}") + # 与枢轴点对比 + if ss and ws and pivot and chip_sr['chip_ss'] > 0 and chip_sr['chip_sr'] > 0: + chip_ss_pct = (price - chip_sr['chip_ss']) / price * 100 + chip_sr_pct = (chip_sr['chip_sr'] - price) / price * 100 + + # 共振检测:筹码支撑 vs 枢轴弱支撑(都是最近支撑位) + if ws and ws > 0: + resonance_ss = abs(chip_ss_pct - ((price - ws) / price * 100)) < 3 + else: + resonance_ss = False + # 共振检测:筹码阻力 vs 枢轴弱阻力(都是最近阻力位) + if wr and wr > 0: + resonance_sr = abs(chip_sr_pct - ((wr - price) / price * 100)) < 3 + else: + resonance_sr = False + + if resonance_ss and chip_weight >= 0.5: + print(f" ⚡ 支撑共振({chip_weight:.0f}): 筹码+枢轴均指向{chip_sr['chip_ss']:.0f}") + elif chip_weight < 0.5: + print(f" 📎 支撑一致但权重低({chip_weight:.1f}): {regime_id}下筹码支撑不可靠") + + if resonance_sr and chip_weight >= 0.5: + print(f" ⚡ 阻力共振({chip_weight:.0f}): 筹码+枢轴均指向{chip_sr['chip_sr']:.0f}") + elif chip_weight < 0.5: + print(f" 📎 阻力一致但权重低({chip_weight:.1f}): {regime_id}下筹码阻力不可靠") + except Exception: + pass + + profit_pct = (price - cost) / cost * 100 if cost else 0 + is_new_entry = (cost == 0) or (shares == 0) + is_deep_loss = profit_pct < -20 + + # ----- 股票分类(短炒/中短线/中长线/弱势/深套) ----- + stock_category = "中短线" + time_horizon = "2周~3月" + position_advice = "中等仓位" + try: + mtf_cache = mtf._load_mtf_cache() + stock_data = mtf_cache.get(code, {}) + daily_klines = stock_data.get("daily", []) + fund = stock_data.get("fundamentals", {}) + closes = [d["close"] for d in daily_klines] if daily_klines else [] + + if len(closes) >= 10: + cur = closes[-1] + ma20 = sum(closes[-20:])/20 if len(closes)>=20 else 0 + ma60 = sum(closes[-60:])/60 if len(closes)>=60 else 0 + highs = [d["high"] for d in daily_klines[-20:]] + lows = [d["low"] for d in daily_klines[-20:]] + volatility = ((max(highs)-min(lows))/min(lows)*100) if min(lows)>0 else 0 + pe = fund.get("pe") or 0 + eps = fund.get("eps") or 0 + mcap = fund.get("mcap_total") or 0 + is_high_vol = volatility > 30 + is_high_pe = pe > 100 or pe < 0 + is_value = 0 < pe < 20 and eps > 0.5 + + if is_deep_loss: + stock_category = "深套" + time_horizon = "长期" + position_advice = "不补不割" + elif is_high_vol and is_high_pe: + stock_category = "短炒" + time_horizon = "数日~2周" + position_advice = "小仓快进快出" + elif cur < ma20 and cur < ma60 and ma20 > 0: + stock_category = "弱势" + time_horizon = "观望" + position_advice = "减仓或观望" + elif (is_value or mcap > 1000) and cur > ma20: + stock_category = "中长线" + time_horizon = "数月~1年" + position_advice = "正常配置" + elif volatility > 20: + stock_category = "中短线" + time_horizon = "2~6周" + position_advice = "中等仓位" + except Exception: + pass + + print(f" 分类: {stock_category} | {time_horizon} | {position_advice}") + + # ----- 短炒+强趋势检测:短炒分类但多周期多头时用移动止损代替弱支撑止损 ----- + is_short_term_strong_trend = False + if stock_category == "短炒": + trend_align = mtf_adj.get("trend_alignment", "") + strong_trend_indicators = ["多周期看多", "多周期多头", "上升"] + if any(ind in trend_align for ind in strong_trend_indicators): + is_short_term_strong_trend = True + print(f" ⚡ 短炒+强趋势检测: 趋势={trend_align} → 启用移动止损, 不止盈") + position_advice = "小仓强趋势让利润跑" + + # ----- 止损设置(含最小距离3%保护) ----- + if is_new_entry: + # 新买入推荐:止损 = 弱支撑(约2-3%跌幅,合理可控) + if ws and ws > 0: + new_stop = round(ws, 2) + else: + new_stop = round(price * 0.96, 2) + elif is_deep_loss: + # 深套:止损 = 强支撑再下移(不轻易割) + if ss and ss > 0: + new_stop = round(min(ss, price * 0.85), 2) + else: + new_stop = round(price * 0.85, 2) + else: + # 已持仓正常:止损 = 强支撑 + if is_short_term_strong_trend: + # 短炒+强趋势:用移动止损(距现价-5%),不止盈让利润跑 + trailing_sl = round(max(ws or 0, price * 0.95), 2) if ws else round(price * 0.95, 2) + new_stop = trailing_sl + print(f" 短炒强趋势移动止损: {new_stop} (距现价-{(1-new_stop/price)*100:.1f}%)") + elif ss and ss > 0: + new_stop = round(ss, 2) + else: + new_stop = round(price * 0.88, 2) + + # 已盈利仓位(>5%):用较紧的移动止损保护利润,但不超过成本线 + if profit_pct > 5 and not is_new_entry and not is_deep_loss: + # 取 max(弱支撑, 成本线, 当前价×0.95) 作为止损 + cost_protect = cost if cost > 0 else 0 + trailing_stop = round(max(ws or 0, cost_protect, price * 0.95), 2) + if trailing_stop > new_stop: + new_stop = trailing_stop + print(f" 已启用移动止损: {new_stop} (保护+{profit_pct:.1f}%利润)", file=sys.stderr) + + # 最小止损距离 —— 随趋势强度调整(2026-06-23 震度保护规则) + # 强趋势(多周期看多 + MA多头排列):最小1.5%下行空间 + # 普通/弱势:最小3%下行空间 + is_strong_trend = False + trend_align = mtf_adj.get("trend_alignment", "") + strong_trend_indicators = ["多周期看多", "多周期多头", "上升"] + try: + if any(ind in trend_align for ind in strong_trend_indicators) and ma20 > ma60 and cur >= ma20: + is_strong_trend = True + except (NameError, TypeError): + pass # ma20/ma60/cur may be unbound if MTF data insufficient + + if is_strong_trend: + min_stop_gap = 0.015 # 1.5% + else: + min_stop_gap = 0.03 # 3% + + min_stop = round(price * (1 - min_stop_gap), 2) + if new_stop > min_stop and not is_deep_loss: + old_stop = new_stop + new_stop = min_stop + if old_stop != new_stop: + print(f" 最小止损 {round(min_stop_gap*100)}%间距约束: {old_stop}→{new_stop} (趋势{'强' if is_strong_trend else '普通'})") + + # 港股附加:ATR波动率校验 — 止损距现价不得小于 1×ATR(14) + if is_hk_stock(code): + atr = calc_atr(code) + if atr and atr > 0: + min_atr_stop = round(price - atr, 2) + if new_stop > min_atr_stop: + old_stop_val = new_stop + new_stop = min_atr_stop + print(f" 港股ATR波动率校验({atr:.2f}): 止损 {old_stop_val}→{new_stop} (1×ATR间距)") + + # ----- 止盈设置 ----- + if is_short_term_strong_trend and not is_new_entry: + # 短炒+强趋势:不止盈让利润跑 + mtf_tp = mtf_adj.get("take_profit_reference", {}) + if mtf_tp and mtf_tp.get("level", 0) > price * 1.2: + new_target = round(mtf_tp["level"], 2) + else: + new_target = 0 # 无多周期阻力时不编造止盈 + print(f" 短炒强趋势不止盈: 止盈设为{new_target} (+{(new_target/price-1)*100:.0f}%)") + elif sr_resist and sr_resist > 0: + new_target = round(sr_resist, 2) + else: + new_target = 0 # 无技术面数据时不编造止盈 + + # ----- 风险回报比校验 ----- + stop_distance = price - new_stop if price > new_stop else price * 0.02 + target_distance = new_target - price if new_target > price else 0 + + # 1:2 检查 + min_target_distance = stop_distance * 2.0 + if target_distance < min_target_distance: + # 尝试更高的阻力位,但不超过下一个真实压力位 + candidate_targets = [] + if wr and wr > price and wr != sr_resist: + candidate_targets.append(wr) + if sr_resist and sr_resist > price: + candidate_targets.append(sr_resist) + # 检查有效区间,如果有更高的自然目标位 + if effective_range and price < effective_range * 0.9: + candidate_targets.append(effective_range) + + found = False + for level in candidate_targets: + if (level - price) >= min_target_distance: + new_target = level + found = True + break + + # 如果仍然不满足,检查是否至少能到 1:1.5 + min15_distance = stop_distance * 1.5 + if not found: + for level in candidate_targets: + if (level - price) >= min15_distance: + new_target = level + found = True + break + + # ----- 风险回报比最终计算 ----- + risk = max(price - new_stop, price * 0.01) + reward = max(new_target - price, 0) + rr_ratio = reward / risk if risk > 0 else 0 + + # ----- 状态判断 ----- + if is_deep_loss: + status = "updated" + action_note = "深套持有" + elif is_new_entry: + if rr_ratio < 1.5: + status = "review" + action_note = "⚠️盈亏比不足1:1.5,不建议买入" + elif rr_ratio < 2.0: + status = "updated" + action_note = "⚠️盈亏比偏低(1:{:.1f}),谨慎买入".format(rr_ratio) + else: + status = "updated" + action_note = "" + else: + if rr_ratio < 0.5: + status = "updated" + action_note = "⚠️盈亏比极低,关注" + elif rr_ratio < 1.5: + status = "updated" + action_note = "⚠️盈亏比偏低(1:{:.1f}),不建议加仓".format(rr_ratio) + else: + status = "updated" + action_note = "" + + # 短炒+强趋势:在action_note追加标记 + if is_short_term_strong_trend and not is_new_entry and not is_deep_loss: + extra_note = "短炒强趋势持" if "深套" not in action_note else "" + if extra_note: + action_note = f"{action_note} | {extra_note}" if action_note else extra_note + + # ----- 买入区间(有盈亏比严格约束) ----- + max_acceptable_entry = None # 最大可接受买入价(满足R/R约束) + + if new_target and new_stop and new_target > new_stop and not is_deep_loss: + # 买入价的R/R约束: + # 要求 (target - entry) / (entry - stop) >= min_rr + # 即 entry <= (target + min_rr * stop) / (1 + min_rr) + min_rr = 1.0 # 至少1:1,才不亏 + recommend_rr = 1.5 # 推荐1:1.5以上 + + max_for_recommend = (new_target + recommend_rr * new_stop) / (1 + recommend_rr) + max_for_neutral = (new_target + min_rr * new_stop) / (1 + min_rr) + + if is_new_entry: + # 新买入:要求1:1.5+ + max_acceptable_entry = max_for_recommend + else: + # 已持仓加仓:至少1:1 + max_acceptable_entry = max_for_neutral + + if is_new_entry: + # 新买入:买入区 = 弱支撑附近(不是当前价附近!) + # 只在价格跌到弱支撑附近时才推买入 + entry_low = round(price * 0.98, 2) + entry_high = round(price * 1.02, 2) + if max_acceptable_entry and entry_high > max_acceptable_entry: + entry_high = round(max_acceptable_entry, 2) + # 确保买入区不小于1% + if entry_high - entry_low < price * 0.01: + if max_acceptable_entry and price <= max_acceptable_entry: + entry_low = round(max(price * 0.99, new_stop), 2) + entry_high = round(min(price * 1.01, max_acceptable_entry), 2) + elif ws and ws > 0 and wr and wr > 0 and not is_deep_loss: + # 已持仓正常:买入区 = 弱支撑~弱支撑上方5%(给合理回调空间) + # 上限不能低于成本价×0.95(保护已有持仓不被高位逼空) + entry_low = round(ws, 2) + entry_max = round(ws * 1.05, 2) # 比弱支撑高5%,有足够空间 + # 如果当前价已远离买入区,保持买入区不变(不因价格涨了就收窄) + min_upper = round(cost * 0.95, 2) if cost > 0 else 0 + if entry_max < min_upper: + entry_max = min_upper + if max_acceptable_entry: + entry_high = round(min(entry_max, max_acceptable_entry), 2) + else: + entry_high = entry_max + # 如果当前价已远离买入区(高于买入区上沿),禁止加仓推荐 + if price > entry_high: + # 买入区锁定在弱支撑位,但标记为"价格远离" + pass + # 如果买入区过窄,标记但不扩展(加仓必须在支撑位) + if entry_high - entry_low < price * 0.005: + entry_low = round(ws * 0.995, 2) + entry_high = round(ws * 1.005, 2) + else: + entry_low = round(price * 0.90, 2) + entry_high = round(price * 1.05, 2) + + # 买入区间稳定性保护:上边界单次变动不超过5% + if 'entry_high' in dir() and entry_high: + # 读取当前策略中已有的买入区上界,如果有且变化过大则限制 + old_entry_high = None + if 'current_action' in dir() and current_action: + import re + m = re.search(r'买入区[\d.]+~([\d.]+)', current_action) + if m: + old_entry_high = float(m.group(1)) + if old_entry_high and old_entry_high > 0: + max_change = old_entry_high * 0.95 # 单次最多下降5% + if entry_high < max_change: + entry_high = round(max_change, 2) + + # ----- 买入时机信号(三维分析:大盘+行业+个股,基本面+消息面+技术面+资金流)----- + # [2026-07-01] 扩展:不再只看volume_signal + candlestick_sentiment + # 融合大盘趋势、行业板块强弱、基本面估值作为修正因子 + volume_signal = vol.get("volume_signal", "") + candlestick_sentiment = candle.get("sentiment", "neutral") + timing_signal = "neutral" + + # --- 三维分析数据装载 --- + # 因子1: 大盘环境(从macro_context_log读) + market_bearish = False + market_bullish = False + try: + import sqlite3 + _db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5) + _mc = _db.execute( + "SELECT structure FROM macro_context_log WHERE has_valid_data=1 ORDER BY rowid DESC LIMIT 1" + ).fetchone() + if _mc and _mc[0]: + _s = json.loads(_mc[0]) + _overall = _s.get("overall", "") + if "bearish" in _overall: + market_bearish = True + elif _overall == "bullish": + market_bullish = True + _db.close() + except Exception: + pass + + # 因子2: 行业板块强弱 + sector_strong = False + sector_weak = False + try: + _db2 = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5) + _rows2 = _db2.execute( + "SELECT name, change_pct FROM sector_snapshots ORDER BY change_pct DESC" + ).fetchall() + if _rows2: + # 找到该股所属行业(简单匹配name或通过stock_sectors) + _my_sectors = _db2.execute( + "SELECT sector_name FROM stock_sectors WHERE code=?", + (code,) + ).fetchall() + if _my_sectors: + for (_sn,) in _my_sectors: + for r_name, r_chg in _rows2: + if _sn in r_name or r_name in _sn: + _rank = [r[0] for r in _rows2].index(r_name) if r_name in [x[0] for x in _rows2] else -1 + _total = len(_rows2) + if _rank >= 0: + if _rank < _total * 0.2: + sector_strong = True + if _rank > _total * 0.8: + sector_weak = True + break + _db2.close() + except Exception: + pass + + # 因子3: 基本面估值 + is_value_stock = False + try: + _db3 = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5) + _fd = _db3.execute( + "SELECT pe, eps FROM stock_fundamentals WHERE code=?", (code,) + ).fetchone() + if _fd: + _pe, _eps = _fd + is_value_stock = (0 < (_pe or 0) < 25 and (_eps or 0) > 0.3) + _db3.close() + except Exception: + pass + + # --- 三维修正规则 --- + # 大盘偏弱时收紧买入信号,大盘偏强时放宽 + # 行业领先加分,行业落后减分 + # 低估值加分(有安全边际) + + def _adjust_timing(signal, market_b, market_bb, sec_s, sec_w, is_val): + """根据三维因子修正 timing_signal""" + # 大盘偏弱时降级买入信号 + if market_b: + if signal in ("买入", "加仓"): + if not sec_s: # 大盘弱+行业不强→降级 + return "关注" + # 大盘偏强时放宽 + if market_bb: + if signal == "关注" and (sec_s or is_val): + return "买入" + # 行业弱势时降级买入信号 + if sec_w: + if signal in ("买入", "加仓"): + return "关注" + # 行业强势+低估时升级关注 + if sec_s and is_val: + if signal == "关注": + return "买入" + return signal + + if is_new_entry: + # 新买入时机 + if volume_signal == "主动买盘占优" and candlestick_sentiment == "bullish": + timing_signal = "买入" + elif volume_signal == "主动卖盘占优": + timing_signal = "观望" + elif volume_signal == "买卖均衡" and ws and price <= ws * 1.03: + timing_signal = "买入" + elif candlestick_sentiment == "bullish": + timing_signal = "买入" + elif ws and price < ws * 1.02: + timing_signal = "关注" + # 新买入时三维修正:大盘向上+行业强→升级,大盘弱→降级 + _pre_signal = timing_signal + timing_signal = _adjust_timing(timing_signal, market_bearish, market_bullish, + sector_strong, sector_weak, is_value_stock) + if timing_signal != _pre_signal: + print(f" 三维修正(新入): {_pre_signal}→{timing_signal} " + f"| 大盘{'弱' if market_bearish else '强' if market_bullish else '中性'}" + f"| 行业{'强' if sector_strong else '弱' if sector_weak else '中性'}" + f"| 估值{'低' if is_value_stock else '一般'}") + else: + # 已持仓时机(用于加仓/减仓参考) + if is_short_term_strong_trend: + # 短炒+强趋势:强趋势持有,禁止加仓信号 + timing_signal = "持有" + elif profit_pct > 5: + # 已盈利 + if volume_signal == "主动买盘占优": + timing_signal = "持有" + elif volume_signal == "主动卖盘占优" and not is_new_entry: + timing_signal = "关注" + else: + timing_signal = "持有" + elif profit_pct > 0: + # 微盈 + if volume_signal == "主动买盘占优": + timing_signal = "持有" + elif ws and price <= ws * 1.02: + timing_signal = "加仓" + else: + timing_signal = "持有" + else: + # 浮亏 + if volume_signal == "主动卖盘占优" and ss and price <= ss * 1.03: + timing_signal = "关注" + elif volume_signal == "主动买盘占优" and sr_resist and price >= sr_resist * 0.97: + timing_signal = "关注" + elif volume_signal == "买卖均衡" and ws and price <= ws * 1.02: + timing_signal = "加仓" + else: + timing_signal = "持有" + + # ----- 【v3.2新增】分类约束:弱势/深套禁止输出买入/加仓类信号 ----- + if stock_category == "弱势" or is_deep_loss: + buy_signals = ["买入", "加仓", "可追"] + if any(s in timing_signal for s in buy_signals): + old_signal = timing_signal + timing_signal = "弱势持有" if stock_category == "弱势" else "深套持有" + print(f" 分类约束: {stock_category} 原信号\"{old_signal}\" → \"{timing_signal}\"") + + # ----- 构造 action 描述(供 cron prompt 使用) ----- + action_parts = [] + if profit_pct < -20: + action_parts.append("深套持有") + elif profit_pct < -10: + action_parts.append("持有观察") + elif profit_pct < 0: + action_parts.append("持有观察") + elif profit_pct < 5: + action_parts.append("盈利持有") + else: + action_parts.append("盈利良好") + + if action_note: + action_parts.append(action_note) + + if is_watchlist: + # 自选股(未入场):有止损参考+买入区,内部算RR需要止盈位 + action_parts.append(f"目标参考{new_target}") + action_parts.append(f"止损参考{new_stop}") + action_parts.append(f"买入区{entry_low}~{entry_high}") + elif is_new_entry: + action_parts.append(f"损{new_stop}") + action_parts.append(f"盈{new_target}") + action_parts.append(f"买{entry_low}~{entry_high}") + else: + action_parts.append(f"止损{new_stop}") + action_parts.append(f"目标{new_target}") + action_parts.append(f"买入区{entry_low}~{entry_high}") + + if timing_signal != "neutral": + action_parts.append(f"信号:{timing_signal}") + + new_action = " | ".join(action_parts) + + # 技术面快照 + tech_snapshot = "" + if candle: + tech_snapshot = (f"形态:{candle.get('pattern','?')}/{candle.get('sentiment','?')} " + f"量价:{vol.get('volume_signal','?')} " + f"强撑:{ss} 弱撑:{ws} 弱压:{wr} 强压:{sr_resist}") + # 加入均线信息(如果可用) + try: + dm = mtf_analysis.get("daily", {}).get("mas", {}) + ma_parts = [] + for m in ['ma5', 'ma10', 'ma20', 'ma60']: + v = dm.get(m) + if v: + ma_parts.append(f"{m.upper()}={v}") + if ma_parts: + tech_snapshot += " | " + " ".join(ma_parts) + except (NameError, AttributeError): + pass + + # 多周期快照(追加到 tech_snapshot) + mtf_context = "" + if mtf_adj: + trend_align = mtf_adj.get("trend_alignment", "") + daily_mas = mtf_analysis.get("daily", {}).get("mas", {}) + ma20 = daily_mas.get("ma20") + ma60 = daily_mas.get("ma60") + stop_ref = mtf_adj.get("stop_loss_reference", {}) + take_ref = mtf_adj.get("take_profit_reference", {}) + + parts = [] + if trend_align: + parts.append(trend_align) + if ma20: + parts.append(f"MA20={ma20}") + if ma60: + parts.append(f"MA60={ma60}") + if stop_ref: + parts.append(f"长撑:{stop_ref.get('source','?')}={stop_ref['level']}") + if take_ref: + parts.append(f"长压:{take_ref.get('source','?')}={take_ref['level']}") + mtf_context = " | ".join(parts) + + now_str = datetime.now().strftime('%Y-%m-%d %H:%M') + return { + 'price': price, # 2026-07-07 修复:加入price供质量门禁使用 + 'stop_loss': new_stop, + 'take_profit': new_target, + 'entry_low': entry_low, + 'entry_high': entry_high, + 'action': new_action, + 'status': status, + 'tech_snapshot': tech_snapshot, + 'timing_signal': timing_signal, + 'rr_ratio': round(rr_ratio, 2), + 'action_note': action_note, + 'reassessed_at': now_str, + 'multi_tf_context': mtf_context, # 多周期上下文 + 'stock_category': stock_category, # 股票分类:短炒/中短线/中长线/弱势/深套 + 'time_horizon': time_horizon, # 时间跨度 + 'position_advice': position_advice, # 仓位建议 + } + + +def load_stock_news_sentiment(code): + """加载小果消息面情感""" + try: + path = "/home/hmo/web-dashboard/data/xiaoguo_sentiment.json" + if not os.path.exists(path): + return {} + xg = json.load(open(path)) + return xg.get("stocks", {}).get(code, {}) + except Exception: + return {} + + +def load_fundamentals(code): + """加载个股基本面""" + try: + cache = mtf._load_mtf_cache() + return cache.get(code, {}).get("fundamentals", {}) or {} + except Exception: + return {} + + +def _get_portfolio_risk_state(): + """读取 portfolio 组合风险状态(2026-06-23 引擎协调)""" + try: + # 数据一致性检查:警告多副本(2026-06-23 bugfix) + _check_portfolio_consistency() + p = read_portfolio() + pos_pct = p.get('position_pct', 0) + cash = p.get('cash', 0) + holdings = p.get('holdings', []) + weak_cnt = sum(1 for h in holdings if h.get('change_pct', 0) < -15) + total = len(holdings) or 1 + weak_ratio = weak_cnt / total + return { + 'position_pct': pos_pct, + 'cash': cash, + 'is_high_position': pos_pct > 80, + 'is_very_high_position': pos_pct > 90, + 'is_high_weak': weak_ratio > 0.35, + 'weak_ratio': round(weak_ratio * 100), + 'total_holdings': total, + } + except: + return {} + + +def _is_buy_signal(signal): + """判断信号是否为买入/持有类(用于防洗盘)""" + if not signal: + return False + buy_keywords = ['买入', '持有', '加仓', '关注'] + for kw in buy_keywords: + if kw in signal: + return True + return False + + +def _check_portfolio_consistency(): + """数据一致性检查:如果存在多份 portfolio.json 则报警(2026-06-23 bugfix)""" + main = '/home/hmo/web-dashboard/data/portfolio.json' + main_cash = None + try: + import json + main_cash = json.load(open(main)).get('cash') + except Exception: + return + for path in [ + '/home/hmo/data/portfolio.json', + '/home/hmo/projects/MoFin/data/portfolio.json', + '/home/hmo/web-dashboard.bak/data/portfolio.json', + ]: + if os.path.exists(path): + try: + other = json.load(open(path)) + if other.get('cash') != main_cash: + print(f"⚠️ 数据一致性: {os.path.realpath(path)} cash={other.get('cash')} ≠ 主文件 cash={main_cash} (需清理)", file=sys.stderr) + except Exception: + pass + + +def _check_contradiction(code, today_only=True): + """反馈循环核——检查本股是否有刚卖出的记录 + + 返回 dict or None: + - sold_reason: 'portfolio_trim'|'stop_loss' + - sold_at: 卖出日期 + - days_ago: 卖出距今交易日数 + - is_today: 是否今日卖出 + - tag: 追加到信号的标注 + """ + try: + from datetime import datetime, date + dec = read_decisions() + for e in dec.get('decisions', []): + if e.get('code') != code: + continue + sold_at = e.get('sold_at', '') + if not sold_at: + return None + try: + sd = datetime.strptime(sold_at, '%Y-%m-%d').date() + td = date.today() + days = (td - sd).days + except: + return None + + reason = e.get('sold_reason', 'portfolio_trim') + if reason == 'stop_loss': + tag = '止损离场(逻辑破坏,短期不关注)' + else: + tag = '组合减仓后关注(已清仓,等回踩确认)' + + return { + 'sold_reason': reason, + 'sold_at': sold_at, + 'days_ago': days, + 'is_today': days == 0, + 'tag': tag, + } + except: + return None + return None + + +def _get_sell_priority_list(): + """减仓优先级排序:深套>亏损>微盈>盈利(2026-06-23 反馈循环) + + 返回 [(code, name, change_pct, position_pct, priority_label), ...] + 按卖出的优先顺序排列(最先应该卖的在最前) + """ + try: + p = read_portfolio() + holdings = p.get('holdings', []) + ranked = [] + for h in holdings: + chg = h.get('change_pct', 0) + pos = h.get('position_pct', 0) + if chg < -30: + label = '深套(>30%),优先减' + rank = 0 + elif chg < -20: + label = '深套(>20%),优先减' + rank = 1 + elif chg < -10: + label = '亏损,建议减' + rank = 2 + elif chg < 0: + label = '微亏,可减' + rank = 3 + elif chg < 10: + label = '微盈,持有' + rank = 4 + else: + label = '盈利,最后减' + rank = 5 + ranked.append((rank, h['code'], h.get('name',''), chg, pos, label)) + ranked.sort(key=lambda x: (x[0], -x[4])) # 优先 rank, 其次仓位大优先 + return [{'code':c,'name':n,'change_pct':chg,'position_pct':pos,'label':l} + for r,c,n,chg,pos,l in ranked] + except: + return [] + + +def enrich_timing_signal(base_signal, macro_desc="", sector_note="", + profit_pct=0, stock_category="", is_new_entry=False, + fundamentals=None, news_sentiment=None, + timing_signal_override=None, + portfolio_context=None, + rr_ratio=0): # 2026-06-24 新参:盈亏比约束 + """多因子合成timing_signal——大盘+行业+基本面+技术+组合风险+盈亏比 + + 返回 (enriched_signal, factors_list) + - enriched_signal: 可读的多因子信号描述 + - factors_list: 各因子的摘要列表(用于后续显示) + """ + # 如果已手动设定,尊重手动 + if timing_signal_override and timing_signal_override != "neutral": + return timing_signal_override, [timing_signal_override] + + factors = [] + + # 1. 大盘因子 + if "偏强" in macro_desc or "大涨" in macro_desc or "bullish" in macro_desc.lower(): + macro_txt = "大盘偏强" + factors.append(macro_txt) + elif "偏弱" in macro_desc or "大跌" in macro_desc or "bearish" in macro_desc.lower(): + macro_txt = "大盘偏弱" + factors.append(macro_txt) + elif macro_desc and macro_desc != "宏观未加载": + factors.append("大盘中性") + + # 2. 行业因子 + if sector_note: + # 把"行业X大跌3%+"简化为"行业偏弱","行业X大涨3%+"简化为"行业偏强" + if "大跌" in sector_note or "下跌" in sector_note: + factors.append("行业偏弱") + elif "大涨" in sector_note: + factors.append("行业偏强") + elif "上涨" in sector_note: + factors.append("行业偏强") + else: + factors.append("行业中性") + + # 3. 基本面因子 + if fundamentals: + pe = fundamentals.get("pe", 0) + eps = fundamentals.get("eps", 0) + profit_growth = fundamentals.get("profit_growth", fundamentals.get("yoy_profit", "")) + revenue_growth = fundamentals.get("revenue_growth", fundamentals.get("yoy_revenue", "")) + mcap = fundamentals.get("mcap_total", 0) + + pe = pe or 0 + eps = eps or 0 + profit_growth_str = str(profit_growth or "") + revenue_growth_str = str(revenue_growth or "") + + # 净利增长 + for val in [profit_growth_str, revenue_growth_str]: + try: + v = float(val.replace("%", "").replace("+", "")) + if v > 50: + factors.append("净利增50%+") + break + elif v > 20: + factors.append(f"净利增{int(v)}%") + break + elif v < -20: + factors.append("净利降20%+") + break + except (ValueError, AttributeError): + continue + + # PE估值 + if 0 < pe < 15: + factors.append("低估值") + elif pe > 100 or pe < 0: + factors.append("高估值") + + # 市值 + if mcap and mcap > 5000: + factors.append("蓝筹") + + # 4. 消息面因子(小果情感) + if news_sentiment: + ns = news_sentiment.get("sentiment", "") + nc = news_sentiment.get("confidence", 0) + if ns == "positive" and nc >= 0.7: + kws = news_sentiment.get("keywords", []) + kw_str = f"({'/'.join(kws[:3])})" if kws else "" + factors.append(f"消息偏多{kw_str}") + elif ns == "negative" and nc >= 0.7: + kws = news_sentiment.get("keywords", []) + kw_str = f"({'/'.join(kws[:3])})" if kws else "" + factors.append(f"消息偏空{kw_str}") + + # 5. 技术面(基础信号) + if base_signal and base_signal != "neutral": + factors.append(base_signal) + + # 5.5 组合风险因子(2026-06-23 双引擎协调) + if portfolio_context and not is_new_entry: + if portfolio_context.get('is_very_high_position'): + factors.append("组合仓位极重(>90%)") + elif portfolio_context.get('is_high_position'): + factors.append("组合仓位偏重(>80%)") + if portfolio_context.get('is_high_weak'): + factors.append(f"弱势占{portfolio_context.get('weak_ratio')}%") + elif portfolio_context and is_new_entry: + # 新买入推荐:注明组合上下文 + if portfolio_context.get('is_high_position'): + factors.append(f"仓{portfolio_context.get('position_pct')}%现金有限") + elif portfolio_context.get('is_high_weak'): + factors.append("组合风险信号") + + # 5.7 盈亏比因子(2026-06-24 新增——RR<1.5降级买入信号) + if rr_ratio > 0: + if rr_ratio < 1.5: + factors.append(f"RR{rr_ratio}过低") + elif rr_ratio >= 3: + factors.append(f"RR{rr_ratio}") + # 1.5~3之间:中性,不特别标注 + + # 如果没有足够因素,返回信号不充分 + if not factors: + return "信号不充分", [] + + # 信号只应包含明确的买卖方向,不能从行业/大盘等上下文因子拼凑 + # base_signal 存在且非 neutral → 用 base_signal + # 否则 → 信号不充分(不拿 factors[-1] 当信号) + if base_signal and base_signal != "neutral": + clean_signal = base_signal + else: + # 从 factors 中找第一个有效的操作方向信号 + valid_direction = {"买入", "加仓", "观望", "持有", "关注", "信号不充分"} + signal_found = "" + for f in reversed(factors): + if f in valid_direction: + signal_found = f + break + clean_signal = signal_found if signal_found else "信号不充分" + + # 6. RR约束降级(2026-06-24 新增) + # 买入/加仓信号但RR<1.5 → 降级为"信号不充分" + buy_signals = {"买入", "加仓"} + if clean_signal in buy_signals and 0 < rr_ratio < 1.5: + clean_signal = "信号不充分" + factors.append("RR过低降级") + + return clean_signal, factors + + +def reassess_with_context(code, name, price, cost, shares, current_action, + volume_signal="", sentiment="neutral", is_watchlist=False): + """reassess_strategy + 多因子信号合成(大盘+行业+技术) + + 为 per_stock_reassess 等单只场景提供一站式多因子分析 + """ + result = reassess_strategy( + code, name, price, cost, shares, + current_action, volume_signal, sentiment, is_watchlist + ) + if not result: + return result + + # 加载宏观+行业+消息+基本面上下文 + try: + macro_bias, macro_desc = load_macro_context() + market_ctx = load_market_context() + stock_sector_map = load_stock_sector_map() + sector_adj = compute_sector_adjustment(code, market_ctx, stock_sector_map) + sector_note = sector_adj.get("note", "") + news_sentiment = load_stock_news_sentiment(code) + fund = load_fundamentals(code) + except Exception: + macro_desc = "" + sector_note = "" + news_sentiment = {} + fund = {} + + # ── DSA 集成:注入大盘复盘 + 新闻情报 ────────────────────────── + try: + from mo_bridge import enrich_analysis_context + region = "hk" if len(str(code)) == 5 and str(code)[0] in ('0','1') else "cn" + dsa_ctx = enrich_analysis_context(stock_code=code, stock_name=name, + region=region, include_news=True) + if dsa_ctx: + macro_desc = (macro_desc + "\n\n" + dsa_ctx).strip() + except Exception: + pass # DSA 不可用时静默跳过 + + enriched, factors = enrich_timing_signal( + base_signal=result.get("timing_signal", ""), + macro_desc=macro_desc, + sector_note=sector_note, + profit_pct=(price - cost) / cost * 100 if cost else 0, + stock_category=result.get("stock_category", ""), + is_new_entry=is_watchlist, + fundamentals=fund, + news_sentiment=news_sentiment, + portfolio_context=_get_portfolio_risk_state(), + rr_ratio=result.get("rr_ratio", 0), + ) + result["timing_signal"] = enriched + result["signal_factors"] = factors + + # 6. 防洗盘:信号不要一天一翻(2026-06-23) + # 如果旧信号是买入/持有类,新信号是谨慎/等待类,但中期趋势未破→维持旧信号 + try: + dec = read_decisions() + for e in dec.get('decisions', []): + if e.get('code') == code: + old_signal = e.get('timing_signal', '') + if old_signal and _is_buy_signal(old_signal) and not _is_buy_signal(enriched): + # 中等趋势检查:MA5 > MA20 + 多周期看多 + mtf = result.get('multi_tf_context', '') + if '看多' in mtf or '多头' in mtf: + try: + closes = [float(k.split()[2]) for k in mtf.split('|') if 'MA5' in k] + except: + closes = [] + has_uptrend = 'MA5' in mtf and 'MA20' in mtf + if has_uptrend: + print(f" 防洗盘: {old_signal}→保持旧信号(中期趋势完整)") + result["timing_signal"] = f"{old_signal}(正常回调价稳)" + sf = result.get("signal_factors") or [] + if "正常回调价稳" not in sf: + result["signal_factors"] = sf + ["正常回调价稳"] + break + except Exception as e: + print(f" 防洗盘跳过: {e}") + + # 7. 反馈循环核:检查本股是否有刚卖出的记录(2026-06-23) + contradiction = _check_contradiction(code) + if contradiction and contradiction.get('is_today'): + # 今日刚卖出 → 不屏蔽信号,但必须自标注矛盾 + print(f" 反馈循环: {contradiction.get('tag')} (sold_at={contradiction.get('sold_at')})") + if _is_buy_signal(result.get('timing_signal', '')): + result['action_note'] = contradiction['tag'] + # 在 timing_signal 中追加反馈标注,供报告层可见 + curr_signal = result.get('timing_signal', '') + if '⚠️' not in curr_signal: + result['timing_signal'] = f"⚠️{contradiction['tag']}|{curr_signal}" + elif contradiction: + # 非今日卖出但近期卖出 → 标注已清仓 + print(f" 近期清仓: sold_at={contradiction.get('sold_at')} ({contradiction.get('days_ago')}日前)") + if _is_buy_signal(result.get('timing_signal', '')): + curr_signal = result.get('timing_signal', '') + if '已清仓' not in curr_signal: + result['timing_signal'] = f"已清仓,{curr_signal}" + + # 重建 action 文本(同步多因子信号) + try: + if new_action_needs_refresh(result, {"source": "auto"}, price): + _refresh_action_text(result, price, name) + except Exception: + pass + + # ── 策略质量门禁 ── + enforce_strategy_quality(code, name, result) + + return result + + +def new_action_needs_refresh(result, old_entry, price): + """判断宏观/行业调整后是否需要刷新action文本""" + # 自选股和手动策略不做调整,不需要刷新 + if old_entry.get("source") == "manual": + return False + return True + + +def _refresh_action_text(result, price, name): + """根据调整后的止损/止盈重建action文本""" + sl = result.get("stop_loss", 0) + tp = result.get("take_profit", 0) + el = result.get("entry_low", 0) + eh = result.get("entry_high", 0) + ts = result.get("timing_signal", "") + an = result.get("action_note", "") + old_action = result.get("action", "") + + # 保持原action的前缀(持有状态部分不变) + # action格式一般是: "状态 | 止损X | 目标Y | 买入区X~Y | 信号:Z" + parts = old_action.split(" | ") + new_parts = [] + for p in parts: + p = p.strip() + # 替换止损数字 + if p.startswith("止损") or p.startswith("止损参考"): + if sl: + p = f"止损{sl}" if "止损参考" not in old_action.split(" | ")[0] else f"止损参考{sl}" + # 替换目标/止盈数字 + if p.startswith("目标") or p.startswith("止盈"): + if tp: + p = f"目标{tp}" + # 替换买入区数字 + if "买入区" in p and "~" in p: + if el and eh: + p = f"买入区{el}~{eh}" + new_parts.append(p) + result["action"] = " | ".join(new_parts) + + +def check_sector_alerts(market_ctx, stock_sector_map, holdings, wl): + """行业轮动主动预警:检测板块崩盘级别信号→查持仓→输出预警 + + 返回 list of alerts: [{code, name, sector, chg, action}] + """ + alerts = [] + if not market_ctx: + return alerts + + sector_perf = market_ctx.get("sector_perf", {}) + + # 找出所有跌幅>3%的行业 + crashing_sectors = {name: data for name, data in sector_perf.items() + if data.get("change", 0) <= -3} + + if not crashing_sectors: + return alerts + + # 构建 code→持仓信息 的映射 + holding_map = {} + for h in holdings: + c = h.get("code", "") + if c: + holding_map[c] = {"name": h.get("name", c), "type": "持仓"} + for s in wl.get("stocks", []): + c = s.get("code", "") + if c and c not in holding_map: + holding_map[c] = {"name": s.get("name", c), "type": "自选"} + + # 对每个暴跌行业,查持仓中是否有股票属于该行业 + for sec_name, sec_data in sorted(crashing_sectors.items(), + key=lambda x: x[1].get("change", 0)): + chg = sec_data.get("change", 0) + for code, sectors in stock_sector_map.items(): + if code in holding_map and sec_name in sectors: + info = holding_map[code] + alerts.append({ + "code": code, + "name": info["name"], + "sector": sec_name, + "sector_change": chg, + "type": info["type"], + "action": f"行业{sec_name}跌{chg:+.1f}%,{info['type']}需关注", + }) + + alerts.sort(key=lambda a: a["sector_change"]) + return alerts + + +def regenerate_all(stdout=True): + """全量重评所有持仓+自选策略""" + # 优先从 SQLite 读取 + try: + from mofin_db import get_conn, query_holdings, query_watchlist + conn = get_conn() + holdings = query_holdings(conn) + wl_stocks = query_watchlist(conn) + conn.close() + pf = {"holdings": holdings} + wl = {"stocks": wl_stocks} + except Exception: + try: + pf = read_portfolio() + wl = read_watchlist() + except Exception: + pf = {} + wl = {} + + all_stocks = {} + for item in pf.get("holdings", []): + code = item.get("code", "") + if code: + all_stocks[code] = {"source": "portfolio", "data": item} + for item in wl.get("stocks", []): + code = item.get("code", "") + if code and code not in all_stocks: + all_stocks[code] = {"source": "watchlist", "data": item} + + total = len(all_stocks) + ok = 0 + errors = 0 + results = [] + decisions = [] + + # 加载现有 decisions.json 以便追踪变更 + decisions_path = "/home/hmo/web-dashboard/data/decisions.json" + try: + existing_decisions = {d["code"]: d for d in read_decisions().get("decisions", []) if d.get("code")} + except: + existing_decisions = {} + + # 加载宏观上下文(影响策略参数调整) + macro_bias, macro_desc = load_macro_context() + if stdout: + print(f" 宏观参考: {macro_desc} (bias={macro_bias})") + + # 加载市场上下文 — 行业板块表现 + 大盘宽度(策略参数调整用) + market_ctx = load_market_context() + stock_sector_map = load_stock_sector_map() + market_breadth = market_ctx.get("breadth", 50) + market_mood = market_ctx.get("mood", "neutral") + if stdout: + sectors_found = sum(1 for c in all_stocks if stock_sector_map.get(c)) + print(f" 市场参考: {market_mood} 上涨比{market_breadth}% 已匹配{sectors_found}/{total}只个股行业") + + # 批量预取所有价格(一次API调用 vs 之前N次) + prices_map = batch_fetch_prices(list(all_stocks.keys())) + if stdout: + print(f" 批量获取价格: {len(prices_map)}/{total} 成功") + + for code, info in sorted(all_stocks.items()): + stock = info["data"] + name = stock.get("name", code) + cost = stock.get("cost", 0) or 0 + shares = stock.get("shares", 0) or 0 + source = info["source"] + + q = prices_map.get(code) + if not q or not q.get("price"): + results.append({"code": code, "name": name, "error": "腾讯API无数据"}) + errors += 1 + if stdout: + print(f" ❌ {name}({code}): 腾讯API无数据") + continue + + price = q["price"] + profit_pct = (price - cost) / cost * 100 if cost else 0 + current_action = stock.get("analysis", {}).get("action", "") + close_yest = q.get("close", 0) + sentiment = "neutral" + if close_yest and price > close_yest * 1.02: + sentiment = "bullish" + elif close_yest and price < close_yest * 0.98: + sentiment = "bearish" + + try: + is_wl = (source == "watchlist") + result = reassess_strategy( + code, name, price, cost, shares, + current_action, volume_signal="中性", sentiment=sentiment, + is_watchlist=(source == "watchlist"), + ) + + # --- Manual param preservation: 用户手动策略永不覆盖 --- + old_entry = existing_decisions.get(code, {}) + if old_entry.get("source") == "manual": + # 仅覆盖策略参数,技术分析/信号/价格照常保留 + for key in ["entry_low", "entry_high", "stop_loss", "take_profit"]: + if key in old_entry and old_entry[key] is not None: + result[key] = old_entry[key] + # 重算盈亏比(基于手动参数) + manual_stop = result.get("stop_loss", 0) or 0 + manual_target = result.get("take_profit", 0) or 0 + risk = max(price - manual_stop, price * 0.01) if manual_stop > 0 else price * 0.01 + reward = max(manual_target - price, 0) if manual_target > 0 else 0 + result["rr_ratio"] = round(reward / risk, 2) if risk > 0 else 0 + # 重建 action 文本(引用手动参数,不引用自动计算的) + profit_pct = (price - cost) / cost * 100 if cost else 0 + manual_action_parts = [] + if profit_pct < -20: + manual_action_parts.append("深套持有") + elif profit_pct < -10: + manual_action_parts.append("持有观察") + elif profit_pct < 0: + manual_action_parts.append("持有观察") + elif profit_pct < 5: + manual_action_parts.append("盈利持有") + else: + manual_action_parts.append("盈利良好") + if result.get("action_note"): + manual_action_parts.append(result["action_note"]) + if is_wl: + if manual_stop > 0: + manual_action_parts.append(f"止损参考{manual_stop}") + manual_action_parts.append(f"买入区{result['entry_low']}~{result['entry_high']}") + else: + if manual_stop > 0: + manual_action_parts.append(f"止损{manual_stop}") + if manual_target > 0: + manual_action_parts.append(f"目标{manual_target}") + manual_action_parts.append(f"买入区{result['entry_low']}~{result['entry_high']}") + ts = result.get("timing_signal", "") + if ts and ts != "neutral": + manual_action_parts.append(f"信号:{ts}") + result["action"] = " | ".join(manual_action_parts) + result["status"] = "manual" # 标记为手动管理,变更追踪不受影响 + if stdout: + print(f" [手动保留] {name}({code}) 策略参数未覆盖") + + # 宏观偏差调整:收盘后重评时根据宏观方向微调止损/止盈 + # 自选股不做止盈宏观调整(无持仓) + # 手动策略不做宏观偏差调整(尊重用户设定) + if macro_bias != 1.0 and not is_wl and old_entry.get("source") != "manual": + old_stop = result.get("stop_loss", 0) + old_target = result.get("take_profit", 0) + if macro_bias < 1.0 and old_stop > 0: # 宏观偏弱 → 收紧止损 + # 止损上移(但保留最小3%间距) + adjusted_stop = round(old_stop * (1 + (1 - macro_bias) * 0.3), 2) + min_stop = round(price * 0.97, 2) + result["stop_loss"] = min(adjusted_stop, min_stop) + if old_target > 0: + result["take_profit"] = round(old_target * (1 - (1 - macro_bias) * 0.2), 2) + elif macro_bias > 1.0 and old_target > 0: # 宏观偏强 → 止盈上调让利润跑 + result["take_profit"] = round(old_target * (1 + (macro_bias - 1) * 0.3), 2) + + # 行业偏差调整:根据个股所在行业的市场表现微调止损/止盈 + # 手动策略不做行业调整(尊重用户设定) + sector_adj = compute_sector_adjustment(code, market_ctx, stock_sector_map) + sector_note = sector_adj.get("note", "") + if sector_note and old_entry.get("source") != "manual": + old_stop = result.get("stop_loss", 0) + old_target = result.get("take_profit", 0) + stop_bias = sector_adj.get("stop_bias", 1.0) + target_bias = sector_adj.get("target_bias", 1.0) + if stop_bias != 1.0 and old_stop > 0: + # 行业偏差调整(在宏观调整之后叠加) + adjusted = round(old_stop * stop_bias, 2) + # 保留最小3%间距 + min_stop = round(price * 0.97, 2) + result["stop_loss"] = min(adjusted, min_stop) + if target_bias != 1.0 and old_target > 0 and not is_wl: + result["take_profit"] = round(old_target * target_bias, 2) + + # 加载消息面+基本面(逐个股) + news_sentiment = load_stock_news_sentiment(code) + fund = load_fundamentals(code) + + # 多因子合成 timing_signal:大盘+行业+消息+基本面+技术 + if old_entry.get("source") != "manual": + enriched, _ = enrich_timing_signal( + base_signal=result.get("timing_signal", ""), + macro_desc=macro_desc, + sector_note=sector_note, + profit_pct=profit_pct, + stock_category=result.get("stock_category", ""), + is_new_entry=(source == "watchlist"), + fundamentals=fund, + news_sentiment=news_sentiment, + rr_ratio=result.get("rr_ratio", 0), + ) + result["timing_signal"] = enriched + + # 在宏观/行业/多因子调整后重建 action 文本(同步调整后的止损/止盈数字) + if new_action_needs_refresh(result, old_entry, price): + _refresh_action_text(result, price, name) + + extra = { + "rr_ratio": result.get("rr_ratio"), + "action_note": result.get("action_note", ""), + "timing_signal": result.get("timing_signal", ""), + } + analysis = { + "stop_loss": result["stop_loss"], + "take_profit": result["take_profit"], + "entry_low": result["entry_low"], + "entry_high": result["entry_high"], + "action": result["action"], + "tech_snapshot": result.get("tech_snapshot", ""), + "multi_tf_context": result.get("multi_tf_context", ""), + "reassessed_at": result["reassessed_at"], + "status": result["status"], + **extra, + } + stock["analysis"] = analysis + # 同步 top-level 字段 → zone_breach/price_monitor 依赖这些字段 + # (2026-06-24 bugfix: analysis 子对象有但顶层没有,导致新持仓的止损检测盲区) + stock["stop_loss"] = result.get("stop_loss", 0) + stock["take_profit"] = result.get("take_profit", 0) + stock["entry_low"] = result.get("entry_low", 0) + stock["entry_high"] = result.get("entry_high", 0) + # 同步 trigger 字段 -> price_monitor 依赖 + sl = result.get("stop_loss", 0) + tp = result.get("take_profit", 0) + el = result.get("entry_low", 0) + eh = result.get("entry_high", 0) + trig = {} + if sl and float(sl) > 0: + trig["stop_loss"] = float(sl) + if el and eh and float(el) > 0 and float(eh) > 0: + trig["entry_zone"] = f"{float(el)}~{float(eh)}" + if tp and float(tp) > 0: + trig["take_profit_zone"] = f"0~{float(tp)}" + stock["trigger"] = trig + results.append({ + "code": code, "name": name, + "price": price, "cost": cost, + "action": result["action"], + "stop_loss": result["stop_loss"], + "take_profit": result["take_profit"], + "rr_ratio": result["rr_ratio"], + }) + ok += 1 + if stdout: + rr_str = f" RR={result['rr_ratio']}" if "rr_ratio" in result else "" + print(f" ✅ {name}({code}) {price} {result['action']}{rr_str}") + + # 记录所有股票的决策日志(含变更追踪) + status_display = result.get("status", "active") + # 构建行业上下文 + sector_ctx_str = "" + sec_name = sector_adj.get("sector_name", "") + sec_chg = sector_adj.get("sector_change", 0) + if sec_name: + sector_ctx_str = f"行业{sec_name}{sec_chg:+.1f}%" + if sector_adj.get("note"): + # note 已包含大盘宽度信息 + sector_ctx_str = sector_adj["note"] + elif market_breadth < 40: + # 无行业映射时至少记录大盘宽度 + sector_ctx_str = f"大盘上涨比{market_breadth}%" + new_entry = { + "code": code, "name": name, "price": price, + "cost": old_entry.get("cost", cost) if old_entry else cost, # 优先保留旧成本(holding.xls权威) + "shares": shares, # 当前实际持仓股数(不继承旧决策的可能为0的值) + "avg_price": old_entry.get("avg_price", 0), # 保留持仓均价 + "currency": "HKD" if is_hk_stock(str(code)) else "CNY", + "action": result["action"], + "stop_loss": result.get("stop_loss"), + "entry_low": result["entry_low"], + "entry_high": result["entry_high"], + "tech_snapshot": result.get("tech_snapshot", ""), + "timing_signal": result.get("timing_signal", ""), + "rr_ratio": result.get("rr_ratio", 0), + "status": status_display, + "note": result.get("action_note", ""), + "timestamp": result["reassessed_at"], + "updated_at": result["reassessed_at"], + "type": "自选策略" if is_wl else "持仓策略", + "source": old_entry.get("source", "auto"), # manual/auto,继承旧标记 + "sector_context": sector_ctx_str, # 市场上下文:行业表现+大盘宽度 + "stock_category": result.get("stock_category", "中短线"), # 组合监测用 + "position_advice": result.get("position_advice", "中等仓位"), + "time_horizon": result.get("time_horizon", "2周~3月"), + } + new_entry["trigger"] = trig + # created_at: 首次创建时设置,后续 preserve + old_entry = existing_decisions.get(code, {}) + if old_entry.get("created_at"): + new_entry["created_at"] = old_entry["created_at"] + else: + new_entry["created_at"] = result["reassessed_at"] + # 保留 last_reassessed_price(per_stock_reassess 维护的防抖字段) + if old_entry.get("last_reassessed_price"): + new_entry["last_reassessed_price"] = old_entry["last_reassessed_price"] + # 自选股也写止盈位(用于RR校验),但标签用"目标参考"非"止盈" + new_entry["take_profit"] = result.get("take_profit") + + # --- 变更追踪 --- + old_action = old_entry.get("action", "") + old_stop = old_entry.get("stop_loss") + old_target = old_entry.get("take_profit") + + # 构建旧策略摘要和变更理由 + update_reason = "" + changelog_entry = None + + if old_action and old_action != result["action"]: + # 策略有变化 → 记录变更 + old_summary = old_action + new_summary = result["action"] + + # 判断触发原因 + if abs(price - old_entry.get("price", price)) / max(price, 0.01) > 0.03: + trigger = f"价格变动({old_entry.get('price','?')}→{price})" + elif result.get("timing_signal") and result["timing_signal"] != old_entry.get("timing_signal", ""): + trigger = f"技术信号变化: {result['timing_signal']}" + else: + trigger = "技术面重评" + + # 格式化的变更理由(自选股只看止损,不看止盈) + diff_parts = [] + if old_stop and result["stop_loss"] != old_stop: + diff_parts.append(f"止损{old_stop}→{result['stop_loss']}") + if not is_wl and old_target and result.get("take_profit") and result["take_profit"] != old_target: + diff_parts.append(f"止盈{old_target}→{result['take_profit']}") + if diff_parts: + update_reason = f"{trigger}: {', '.join(diff_parts)} | {result.get('tech_snapshot','')[:60]}" + else: + update_reason = f"{trigger}: 策略文字调整" + + changelog_entry = { + "date": result["reassessed_at"], + "old_action": old_action, + "new_action": result["action"], + "reason": update_reason, + "trigger": trigger, + } + new_entry["updated_reason"] = update_reason + + elif not old_action: + # 首次创建策略 + update_reason = f"初始策略创建 | {result.get('tech_snapshot','')[:60]}" + changelog_entry = { + "date": result["reassessed_at"], + "old_action": "", + "new_action": result["action"], + "reason": update_reason, + "trigger": "初始创建", + } + + # 合并changelog + old_changelog = old_entry.get("changelog", []) if old_entry else [] + if changelog_entry: + new_entry["changelog"] = old_changelog + [changelog_entry] + else: + new_entry["changelog"] = old_changelog + + # 保留执行记录 + if old_entry and old_entry.get("execution"): + new_entry["execution"] = old_entry["execution"] + elif stock.get("analysis", {}).get("status") == "executing": + new_entry["execution"] = { + "status": "executing", + "entry_price": cost if cost else 0, + "shares": shares, + "notes": "", + } + + # --- 自动标记 current_recommend --- + # 只在真正执行中的持仓才自动推荐:execution.status 为 executing 或 partial_exit + exec_status = old_entry.get("execution", {}).get("status", "") if old_entry else "" + is_active = exec_status in ("executing", "partial_exit") + + profit_pct = (price - cost) / cost * 100 if cost else 0 + is_deep_loss_stock = profit_pct < -20 + rr = result.get("rr_ratio", 0) + ts = result.get("timing_signal", "") + note = result.get("action_note", "") + + # 计算是否在/接近买入区 + entry_low_val = result.get("entry_low", 0) + entry_high_val = result.get("entry_high", 0) + in_buy_zone = (entry_low_val > 0 and entry_high_val > 0 and + entry_low_val <= price <= entry_high_val) + near_buy_zone_low = (entry_low_val > 0 and + price >= entry_low_val * 0.98 and + price <= entry_high_val) + + # 推荐条件:必须是执行中的持仓 + 基本面条件达标 + is_recommendable = ( + is_active + and not is_deep_loss_stock + and rr >= 1.5 + and ts != "neutral" + and "不建议" not in note + ) + if is_recommendable: + new_entry["tag"] = "current_recommend" + else: + # 不清除 active_manual(用户手动标记),只清除自动推荐的 + old_tag = old_entry.get("tag", "") if old_entry else "" + if old_tag != "active_manual": + new_entry.pop("tag", None) + + decisions.append(new_entry) + + except Exception as e: + results.append({"code": code, "name": name, "error": str(e)}) + errors += 1 + if stdout: + print(f" ❌ {name}({code}): {e}") + + # 写回数据文件 — 保留现有字段(现金、总资产等)不丢 + try: + existing_pf = read_portfolio() + except Exception: + existing_pf = {} + # 保留 price/change_pct — price_monitor 维护的实时价,regenerate_all 不应清除 + _existing_holdings_map = {} + for _h in existing_pf.get('holdings', []): + if _h.get('code'): + _existing_holdings_map[_h['code']] = _h + _new_holdings = pf.get("holdings", []) + for _h in _new_holdings: + _code = _h.get('code') + if _code and _code in _existing_holdings_map: + _old = _existing_holdings_map[_code] + _h['price'] = _old.get('price', 0) + _h['change_pct'] = _old.get('change_pct', 0) + existing_pf["holdings"] = _new_holdings + existing_pf["updated_at"] = datetime.now().strftime('%Y-%m-%d %H:%M') + + # ── Watchlist ↔ Holdings 双向自动迁移(2026-06-27 Dad要求)── + # ① 持仓已有 → 从自选移除(买入自动清除) + wl_codes = {s.get("code") for s in wl.get("stocks", []) if s.get("code")} + pf_codes = {h.get("code") for h in _new_holdings if h.get("code") and h.get("shares", 0) > 0} + removed_from_wl = [] + for h_code in wl_codes & pf_codes: + # 持仓>0且量够 → 自选移除 + wl["stocks"] = [s for s in wl.get("stocks", []) if s.get("code") != h_code] + removed_from_wl.append(h_code) + if removed_from_wl and stdout: + print(f" 自选→持仓自动移除: {', '.join(removed_from_wl)}") + + # ② 清仓/卖光 → 加回自选(只要仍有关注价值) + added_to_wl = [] + old_pf_codes = {_h.get("code") for _h in existing_pf.get("holdings", []) if _h.get("code")} + sold_codes = old_pf_codes - pf_codes # 曾持仓但现在没有(或不在了) + for sc in sold_codes: + # 已有自选就不重复加 + if sc in wl_codes: + continue + # 从现有decisions看是否有关注价值 + for d in decisions: + if d.get("code") == sc and d.get("entry_low") and d.get("entry_high"): + wl["stocks"].append({ + "code": sc, "name": d.get("name", sc), + "entry_low": d.get("entry_low"), "entry_high": d.get("entry_high"), + "stop_loss": d.get("stop_loss", 0), + "analysis": {"action": d.get("action", ""), "tech_snapshot": d.get("tech_snapshot", "")} + }) + added_to_wl.append(sc) + break + if added_to_wl and stdout: + print(f" 清仓→自选自动加入: {', '.join(added_to_wl)}") + + # 重新计算 portfolio 汇总(保留已存在的 cash,用最新价格算市值) + try: + total_mv = 0.0 + total_cost = 0.0 + for h in existing_pf.get('holdings', []): + p = h.get('price') or 0 + s = h.get('shares') or 0 + c = h.get('cost') or 0 + total_mv += p * s + total_cost += c * s + if p and s and total_mv > 0: + h['market_value'] = round(p * s, 2) + old_cash = existing_pf.get('cash') or 80476 # fallback 6/23 backup + frozen_cash = existing_pf.get('frozen_cash') or 0 + existing_pf['cash'] = old_cash + existing_pf['total_mv'] = round(total_mv, 2) + existing_pf['total_assets'] = round(total_mv + old_cash + frozen_cash, 2) + existing_pf['total_pnl'] = round(total_mv - total_cost, 2) + existing_pf['position_pct'] = round(total_mv / (total_mv + old_cash + frozen_cash) * 100, 2) if (total_mv + old_cash + frozen_cash) > 0 else 0 + except Exception as e: + print(f" [汇总计算失败] {e}", flush=True) + + # DB 写入(替代 JSON dump — 强制币种约束) + try: + from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_watchlist_stock, write_holding_strategy + conn = get_conn() + write_holdings_batch(conn, existing_pf.get('holdings', [])) + write_portfolio_summary(conn, existing_pf) + for s in wl.get('stocks', []): + s.setdefault('currency', 'CNY') + write_watchlist_stock(conn, s) + for d in decisions: + # ── 策略质量门禁 ── + code = d.get('code', '') + name = d.get('name', '') + enforce_strategy_quality(code, name, d) + write_holding_strategy(conn, code, name, d) + conn.close() + except Exception as e: + print(f" [DB写入失败] {e}", flush=True) + + # 记录策略→提示词版本关联 + if HAS_PROMPT_TRACKING: + try: + for d in decisions: + if d.get("code") and d.get("action"): + record_strategy_generation( + d["code"], d.get("name", ""), d.get("action", "") + ) + except Exception as e: + if stdout: + print(f" ⚠️ 提示词版本追踪失败: {e}", file=sys.stderr) + + # 刷新多周期缓存到磁盘 + try: + import multi_timeframe as _mtf + _mtf.flush_mtf_cache() + except Exception: + pass + + summary = {"total": total, "ok": ok, "errors": errors} + if stdout: + print(f"\n✅ 全量重评完成: {ok}/{total}成功, {errors}错误") + return summary + + +if __name__ == "__main__": + regenerate_all() diff --git a/system_health_check.py b/system_health_check.py index 64ade3da..880f883d 100644 --- a/system_health_check.py +++ b/system_health_check.py @@ -90,13 +90,13 @@ def run(): try: from mo_data import read_portfolio, read_decisions, read_watchlist pf = read_portfolio() - lines.append(check(len(pf.get("holdings", [])) > 0, f"portfolio.json DB记录: {len(pf.get('holdings', []))}条")) + lines.append(check(len(pf.get("holdings", [])) > 0, f"DB持仓记录: {len(pf.get('holdings', []))}条")) ok_count += 1 wl = read_watchlist() - lines.append(check(len(wl.get("stocks", [])) > 0, f"watchlist.json DB记录: {len(wl.get('stocks', []))}条")) + lines.append(check(len(wl.get("stocks", [])) > 0, f"DB自选股记录: {len(wl.get('stocks', []))}条")) ok_count += 1 dec = read_decisions() - lines.append(check(len(dec.get("decisions", [])) > 0, f"decisions.json DB记录: {len(dec.get('decisions', []))}条")) + lines.append(check(len(dec.get("decisions", [])) > 0, f"DB策略记录: {len(dec.get('decisions', []))}条")) ok_count += 1 except Exception: lines.append(check(False, "MoFin DB 数据读取失败")) diff --git a/technical_analysis.py.bak.1783484685 b/technical_analysis.py.bak.1783484685 new file mode 100644 index 00000000..b5128578 --- /dev/null +++ b/technical_analysis.py.bak.1783484685 @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +"""technical_analysis.py — 技术面分析模块 v2 + +基于多日价格数据计算支撑位/压力位: +1. 缓存每日 HLC 到 price_history.json +2. 使用 5 日最高/最低计算枢轴点 +3. 结合振幅自动调整区间宽度 + +使用方式: + from technical_analysis import full_analysis + result = full_analysis("603259") # 自动识别A股/港股 +""" + +import json +import os +import urllib.request +from datetime import datetime, date + +# 腾讯API字段索引 +F = { + "name": 1, "code": 2, "price": 3, "close_yest": 4, "open": 5, + "volume": 6, "timestamp": 30, "change": 31, "change_pct": 32, + "high": 33, "low": 34, "amplitude": 43, + "turnover": 38, "pe": 39, "pb": 46, + "limit_up": 47, "limit_down": 48, + "avg_price": 51, "inner_vol": 52, "outer_vol": 53, +} + +HISTORY_PATH = "/home/hmo/web-dashboard/data/price_history.json" +HISTORY_DAYS = 60 # 使用最近 N 天的 HLC 数据 + + +def _load_history(): + """读取价格历史缓存""" + try: + return json.load(open(HISTORY_PATH)) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def _save_history(h): + json.dump(h, open(HISTORY_PATH, "w"), ensure_ascii=False, indent=2) + + +def _market_prefix(code): + """根据代码确定腾讯API前缀""" + if code.startswith("sh") or code.startswith("sz") or code.startswith("hk"): + code = code[2:] if code[2:].isdigit() else code + raw = str(code).split("_")[0] + if len(raw) == 5 and raw.isdigit(): + return "hk" + if raw.startswith("6") or raw.startswith("5"): + return "sh" + return "sz" + + +def get_quote(code): + """获取行情数据。DB 优先(price_monitor 维护),腾讯 API fallback""" + # DB 优先 + try: + from mofin_db import get_price_from_db + p, chg = get_price_from_db(code) + if p: return {"code": code, "price": p, "change_pct": chg or 0} + except: pass + # Fallback: 腾讯 API + import time + _cache = get_quote.__dict__.get("_cache", {}) + now = time.time() + cached = _cache.get(code) + if cached and (now - cached["ts"]) < 60: + return cached["data"] + + raw = str(code).split("_")[0] + prefix = _market_prefix(code) + url = f"http://qt.gtimg.cn/q={prefix}{raw}" + try: + r = urllib.request.urlopen(url, timeout=5) + fields = r.read().decode("gbk").split('"')[1].split("~") + except Exception as e: + return {"code": code, "error": str(e)} + + def get(i): + try: + return float(fields[i]) if fields[i].strip() else None + except (IndexError, ValueError): + return None + + today_str = date.today().isoformat() + q = { + "code": raw, + "market": prefix, + "name": fields[F["name"]] if len(fields) > F["name"] else code, + "price": get(3), + "close_yest": get(4), + "open": get(5), + "high": get(33), + "low": get(34), + "volume": get(6), + "amount": get(37), + "change": get(31), + "change_pct": get(32), + "amplitude": get(43), + "turnover_rate": get(38), + "pe": get(39), + "pb": get(46), + "limit_up": get(47), + "limit_down": get(48), + "avg_price": get(51), + "inner_vol": get(52), + "outer_vol": get(53), + "timestamp": fields[F["timestamp"]] if len(fields) > F["timestamp"] else "", + "_date": today_str, + } + + # 写入价格历史缓存(每日一次) + h = get(33) # high + l = get(34) # low + c = get(3) # price / close + if h and l and c: + history = _load_history() + if raw not in history: + history[raw] = [] + days = history[raw] + # 如果今天已有记录,更新(盘中数据更精确) + if days and len(days) > 0 and days[-1].get("date") == today_str: + days[-1]["high"] = max(days[-1]["high"], h) + days[-1]["low"] = min(days[-1]["low"], l) + days[-1]["close"] = c # 盘中用最新价,收盘后是收盘价 + else: + days.append({"date": today_str, "high": h, "low": l, "close": c}) + # 只保留最近 HISTORY_DAYS 天 + history[raw] = days[-HISTORY_DAYS:] + _save_history(history) + + # 写入60秒缓存 + get_quote.__dict__["_cache"] = {**get_quote.__dict__.get("_cache", {}), code: {"ts": now, "data": q}} + + return q + + +def calc_support_resistance(q): + """计算技术支撑位和压力位 — 多日枢轴点算法 + + 使用多个数据源确定有效区间: + 1. 当日波幅(H-L) + 2. 最近 N 日的最高/最低(从 price_history.json 读取) + 3. 价格基数的百分比(对大市值低波动股票有效) + """ + h = q.get("high") + l = q.get("low") + c = q.get("price") + yc = q.get("close_yest") + amplitude = q.get("amplitude") # 当日振幅% + code = q.get("code", "") + + if not all([h, l, c]): + return {"error": "数据不足"} + + # 多日最高/最低(从历史缓存读取) + history = _load_history() + hist_days = history.get(code, []) + multi_high = max(d["high"] for d in hist_days) if hist_days else h + multi_low = min(d["low"] for d in hist_days) if hist_days else l + + # 有效区间 = max(当日波幅, 多日波幅, 价格×5%) + daily_range = h - l + multi_range = multi_high - multi_low + min_range = c * 0.05 # 5%价格基数 + + effective_range = max(daily_range, multi_range, min_range) + + # 如果股价接近多日高点(>80%分位),说明在上升趋势中,扩大区间 + trend_position = (c - multi_low) / (multi_high - multi_low) if multi_high > multi_low else 0.5 + if trend_position > 0.8: + # 高位运行,扩大有效区间到价格的8%确保合理空间 + effective_range = max(effective_range, c * 0.08) + elif trend_position < 0.2: + # 低位运行,同样扩大 + effective_range = max(effective_range, c * 0.08) + + # 如果振幅数据可用且振幅较小(<3%),进一步扩大区间确保有效性 + if amplitude and amplitude > 0 and amplitude < 3: + # 低波动股票用 振幅×3 作为最小范围 + amp_based = c * amplitude / 100 * 3 + effective_range = max(effective_range, amp_based) + + # 枢轴点 (Pivot Point) + pp = (h + l + c) / 3 + + # 支撑位 + s1 = 2 * pp - h # 弱支撑 + s2 = pp - effective_range # 强支撑 + + # 压力位 + r1 = 2 * pp - l # 弱压力 + r2 = pp + effective_range # 强压力 + + # 参考昨收调整 + if yc: + if yc < s1: + s1 = yc + if yc > r1: + r1 = yc + + # A股涨停/跌停价作为极端边界 + limit_up = q.get("limit_up") + limit_down = q.get("limit_down") + market = q.get("market", "hk") + if market != "hk" and limit_up and limit_down: + # 注意:当现价逼近涨停/跌停时,limit不再是有效边界 + # 用有效区间判断:如果自然计算的r2/s2在合理范围内不截断 + natural_r2 = r2 + natural_s2 = s2 + # 涨停限制只对距离现价超过2%的强压位生效 + if limit_up < r2 and (limit_up - c) / c < 0.02: + # 涨停价离现价<2%,说明可能封板,不截断 + pass # 使用自然计算的r2 + elif limit_up < r2: + r2 = limit_up + if limit_down > s2 and (c - limit_down) / c < 0.02: + pass # 接近跌停,不截断 + elif limit_down > s2: + s2 = limit_down + + return { + "strong_support": round(s2, 2), + "weak_support": round(s1, 2), + "pivot": round(pp, 2), + "weak_resist": round(r1, 2), + "strong_resist": round(r2, 2), + "today_high": h, + "today_low": l, + "multi_high": multi_high, + "multi_low": multi_low, + "effective_range": round(effective_range, 2), + } + + +def analyze_candlestick(q): + """判断K线形态""" + o = q.get("open") + c = q.get("price") + h = q.get("high") + l = q.get("low") + yc = q.get("close_yest") + + if not all([o, c, h, l]): + return {"pattern": "unknown", "sentiment": "neutral"} + + if c >= o: + body = c - o + upper = h - c + lower = o - l + is_green = True + else: + body = o - c + upper = h - o + lower = c - l + is_green = False + + total_range = h - l + if total_range == 0: + return {"pattern": "平盘", "sentiment": "neutral"} + + body_pct = body / total_range * 100 + upper_pct = upper / total_range * 100 + lower_pct = lower / total_range * 100 + + if body_pct < 5: + if upper_pct > 60: + pattern = "倒T线/射击之星" + sentiment = "bearish" + elif lower_pct > 60: + pattern = "锤子线/T字线" + sentiment = "bullish" + else: + pattern = "十字星" + sentiment = "neutral" + elif body_pct < 30: + if upper_pct > 40 and lower_pct > 40: + pattern = "长影星线" + sentiment = "neutral" + elif upper_pct > 40: + pattern = "倒T线/射击之星" + sentiment = "bearish" if is_green else "bearish" + elif lower_pct > 40: + pattern = "锤子线/T字线" + sentiment = "bullish" if is_green else "bullish" + else: + pattern = "小阳线" if is_green else "小阴线" + sentiment = "bullish" if is_green else "bearish" + else: + if upper_pct > 30: + pattern = "带上影阳线" if is_green else "带上影阴线" + sentiment = "neutral" if is_green else "bearish" + elif lower_pct > 30: + pattern = "带下影阳线" if is_green else "带下影阴线" + sentiment = "bullish" if is_green else "neutral" + else: + pattern = "光头光脚阳线" if is_green else "光头光脚阴线" + sentiment = "bullish" if is_green else "bearish" + + gap_up = "" + gap_down = "" + if yc: + if o > yc * 1.01: + gap_up = "跳空高开" + if not is_green: + sentiment = "neutral" + elif o < yc * 0.99: + gap_down = "跳空低开" + if is_green: + sentiment = "neutral" + + return { + "pattern": pattern, + "sentiment": sentiment, + "body_pct": round(body_pct, 1), + "upper_shadow_pct": round(upper_pct, 1), + "lower_shadow_pct": round(lower_pct, 1), + "is_green": is_green, + "gap": gap_up or gap_down or "无跳空", + } + + +def analyze_volume(q): + """量价分析""" + outer = q.get("outer_vol") + inner = q.get("inner_vol") + turnover = q.get("turnover_rate") + + result = {} + if outer and inner and (outer + inner) > 0: + ratio = outer / (outer + inner) + result["buy_sell_ratio"] = round(ratio, 2) + if ratio > 0.55: + result["volume_signal"] = "主动买盘占优" + elif ratio < 0.45: + result["volume_signal"] = "主动卖盘占优" + else: + result["volume_signal"] = "买卖均衡" + else: + result["volume_signal"] = "数据不足" + + if turnover: + result["turnover_rate"] = turnover + + return result + + +def full_analysis(code): + """完整技术分析(带30秒缓存,避免分钟级波动)""" + import time + _cache = full_analysis.__dict__.get("_cache", {}) + now = time.time() + cached = _cache.get(code) + if cached and (now - cached["ts"]) < 30: + return cached["data"] + + q = get_quote(code) + if not q or "error" in q: + return q + + sr = calc_support_resistance(q) + candle = analyze_candlestick(q) + vol = analyze_volume(q) + + # 多周期+均线分析(整合 multi_timeframe) + mtf = {} + try: + from multi_timeframe import full_multi_tf_analysis as _mtf + mtf_raw = _mtf(code) + if mtf_raw and 'daily' in mtf_raw: + d = mtf_raw['daily'] + mtf = { + 'mas': d.get('mas', {}), + 'multi_tf_sr': d.get('support_resistance', {}), + 'trend': d.get('trend', {}), + } + # 周线弱压/弱撑作为中周期参考 + if 'weekly' in mtf_raw: + w = mtf_raw['weekly'] + ws = w.get('support_resistance', {}) + mtf['weekly_sr'] = { + 'weak_resist': ws.get('weak_resist'), + 'weak_support': ws.get('weak_support'), + } + except Exception: + pass # non-critical, graceful degradation + + result = { + "quote": { + "name": q.get("name", code), + "price": q["price"], + "change_pct": q.get("change_pct", 0), + "open": q.get("open", 0), + "high": q.get("high", 0), + "low": q.get("low", 0), + "close_yest": q.get("close_yest", 0), + "volume": q.get("volume", 0), + "amplitude": q.get("amplitude", 0), + }, + "support_resistance": sr, + "candlestick": candle, + "volume": vol, + "multi_tf": mtf, + "analyzed_at": datetime.now().strftime("%H:%M"), + } + + # 写入缓存 + _cache[code] = {"ts": now, "data": result} + full_analysis.__dict__["_cache"] = _cache + return result + + +if __name__ == "__main__": + import sys + codes = sys.argv[1:] or ["603259", "002594", "00700"] + for c in codes: + r = full_analysis(c) + print(json.dumps(r, ensure_ascii=False, indent=2)) + print()