From ea9fa6e5ff4e9aa7622eafc1ff508836ea4265ef Mon Sep 17 00:00:00 2001 From: aaron <> Date: Wed, 22 Apr 2026 22:44:48 +0800 Subject: [PATCH] update --- backend/app/__pycache__/main.cpython-313.pyc | Bin 4930 -> 4892 bytes .../api/__pycache__/market.cpython-313.pyc | Bin 11653 -> 9765 bytes backend/app/api/auth.py | 6 +- backend/app/api/debug.py | 4 +- backend/app/api/market.py | 37 --- .../app/db/__pycache__/tables.cpython-313.pyc | Bin 6733 -> 6504 bytes backend/app/db/tables.py | 8 - .../__pycache__/scheduler.cpython-313.pyc | Bin 7169 -> 5176 bytes backend/app/engine/scheduler.py | 73 ++--- backend/app/llm/analysis_agent.py | 252 ------------------ backend/app/llm/daily_review.py | 169 ------------ backend/app/llm/enhancer.py | 15 -- frontend/.next/server/app-paths-manifest.json | 2 +- frontend/src/lib/api.ts | 1 - 14 files changed, 23 insertions(+), 544 deletions(-) delete mode 100644 backend/app/llm/analysis_agent.py delete mode 100644 backend/app/llm/daily_review.py delete mode 100644 backend/app/llm/enhancer.py diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index 419c8fd453a3eae891c614d00d7e73204bb12750..82e9880d5ec618a59c63cd5938715e19f889c9af 100644 GIT binary patch delta 14 VcmX@4Hb-s4dQL{+&DS_{H~}jq1ug&p delta 52 zcmbQEc1Uf*dQNpq{m|mnqGJ8T#G?E>{o?%6qU6;068+5^ HICD4w4#X3# diff --git a/backend/app/api/__pycache__/market.cpython-313.pyc b/backend/app/api/__pycache__/market.cpython-313.pyc index 40b70f23224a83cdfe329b8255424e9b05b2b6f9..b8d34fb1b2c27e1aee31c564264e8e39e68e91ed 100644 GIT binary patch delta 1717 zcmZWqTWlLe6y4dE?e!y$->*2f6DQp!aY{l{C~ex%mrTK--B>|tq{y*1je}#iv)eQ+ zk3vwXR8_RqfCTDK5Cy3MA+?r3e4#Y_L`C9*AGC<7hz}lLX@bfRAKcl*MNL-PvuEx- z_w0CP_WI(9PhWJcIh_&%SJ(YN%1ZpYD})@kJKIq>q4>rEPqT_Y!c6niXeZUcg@e}@ zg3-WEv_4dV5$16!G}e5)pCI0gLZr%uqv5d#7(_N1L>sCj(Z?>{F+RnNxf^4sf|o0_ zo>yX<>SIbvguyLJ958;eRZiBwWbbjd@I;~bs#-1eesW&q$mh<##4bAB?c|25P1pf+ zPkqVdLt;OT_K;ti+MEMGWfq%}0pfIT6Lv%70NL$6W_JU72kx~8JXx?M>R-9vN93j_ zX^|pK)jBGYm%TyqnAQ?s%_L_X4Bq)cWYK6Id8AmxuwcZu6dI^E^uf% z;Rj3eYAkKumY4GOQGVM-ya0;4mE8A+)9%eqdErS_ipdM)n$MrwsuVXV$*-PJa;wbp zq>Nngg-5rlY>!ne!zj)0Lp=HaW6cor=4~!U@#XD%QA1k-ZGut!E~cM{iU1k#9~up2 zBqgMTm536}a0=}A28%1L8!VwD*I6&Gq%uM~V`Yx8xul5#AjCNiahUYICo zDjp0SDJ}9;{MWvM#5nB5$$PtIQ!bAjdbqIlMl@)x_szV4<2(H6M{nfe4bN^~rz;`~L z&7cL8MdHUCu$$4Wm-`lFd2x~H2D;ddvKDc%hw{5&l$FH05z3cDly!+TS1a|}G za0U>*ZVV4hTY&~gjY%YMk_JvR;61XMXh%Lnu~iu<;kD^eysHy2omWu)ol!$h~wL z&69`e6uMFOb^Ol8&QZ~M3a!xD_3<)TP3Ck%-~PeG~M9APhVeGb79gn z-vMxo!PL%Ncf-18rdB#rQ3vrYh|xb0ZHPSV(#d@HOU?Tc64y9}jjphit{f&0x~G~} OB|q}4?Fb={>GL0+$Zz)m delta 3262 zcmai0Z%|X&6~8y{<%J|KAt50NNeGV+P*j2of-H(Ch$~Tp%QJPh(509}WDSYlM3;5i zMQQEgpIv0{cGcQhvDMXWckJxancenF{r90`zvy5qrtS0-s5AW#q`OR~JJX)~ATFXa zy_xsB=bZaH=id8r?>X<%)}tN98G}KGpxtYD)GL*Y7#(=^zk#3996)n9p3({yL@4Mqx7X19MoWz|JKo8AQKu=Zt-`Xa z(*;4@i-Nkdo@)xTTseP{uZk^5@#kgFBLBjy|AwNh|03_~%+exHk6i3Tq)7GxN3eBzmwn`eu0U&CDTtoj-zS=2J{(^%5-+%@%$ShtNU% zQ!c0$QCt;LwQK1G(Y{=rT~oGiMe?&;t`f!h5ZA7!M?|O2f;MkKD6S5vshPJ5aUsOF z=hC~JWk8SxHB1^3WDV;Z@KQk5A}WL=B$cV%TcjFdfv{;!I1x`p<0;~1nl4PDK#X*Cr^-2nK>T1Ql?rt}bh$RIOYV<#$HIG} zeQ%NFRI=p{R49ySe>8j`6;0>tj;6wUx?{0)PFG*|zJ0y%p0ogcG#*KF$rLHqkmW3{ z;-!l0B1KGF$zpIM@n#y6qy`jeu90FkU{bctj@=Pg(rCx4IuOw9Y$XXNG<)H zFME)iKiq$^|4h|{z2NkrkwXte(;ai$xVU|WQ#o|ANR_7>5;6j^U6m7VFsQ;T} z2v$DmXtv^?;igr>IS!D=!Ft1wo9cucl>(D%F|*fUCf5ti zX3b56n>kT`6N^kSXqv55r~Rj}4x*C=`i-_k-3;RPF`v$gg-y)5p03lE7+OIrR}sb1 zO5^&aYB=3!Cq1WsRnP8(a(@r0$7c1v#q?E!3+rf?*no5BuMNd&y%WW?^sb@OBC-pW zy&`aB3~?Q`7++-9(-7E=&+$ci+F1NjF8G={@*$YgvKcj`3+Y4RPX6hY1x85gMIoLh zjQPdp#nD3QXPLApx1jWSmhdbK;t4F?3z(Ycn3@+ecji3D%wf#%^Rw%oGT~;+d9ZU! z=vy1SeAyb(8IhbHf)}3U&L=UO5mocxVehbI-&MPV9&x+ra#P{=9DcoAAUkE3?DlhV zq3oHLiezbCDwe%-QY9~!_|+^!y`l>4Rj^ILQa~L1kAim<)eQ! zb?L}5SO{4GFt*h^idBbN==aU`;{ThC=s)s0)MPdI(MHQUe3ZUz*`ii54gIs_DAv;s z>&K!k)}2hv-}kYvtc7@hyu_wcvPTt@loVov+~SYMdwSzhKZ%ADeSMH=qJ(T`jB0@S zjFgB+l^F>;Uqs6QsuCofI3yiyZ!)nvLHZ!~8NN+Vb1~`|O9P?lr?zE=w02jbn?$-G zRS}s^*qYP^7?-HW?$!Meq}Sm|UZes0cwI(BZqNDpv-OaIGAeAYp5ZuyafVa7Aq}Z* zx>+3(t;gz4)MxZan>SmnB#0;LF)eeX?Q-XZo-4ig{p;`g*I#qr^#|{jwcXhjq5pL> z3PmQDF5{yc3##^!n^6-Q6Q$wc~7J8+`m*aG9cKZ1v zA1qg;t?X>aVtxLFH;N3hk_S6fHa7?pC?)in9j5c2MtjEf$H4v)o+K-}*{WIdz~UXZ z`0g8ghc`{=&4;&5bI4fw@1zKvL9-iE?BqYtxN#Wtxi;42wyGQ_K}qxBL?oJ4hhvFk zlnjG~oT4W@6>3roGQH+0#qZN8j~8DX(-u8cxsNf%aRw&nmd??Z6YJ|6>katF5T;O3oI`AkiP4632Xu6eR|bfizn!$_iJMa48d|tTA(j4 zrjJY7@P7KTukCgAxj>o$(p)cm@UU`6*lkVo`x41inve8`Q*$RP#)jByBBcz<06sy4 z1)lp9$ywW%h#ZJT*Aey}*%3*eq<{0JY(s71*gdT>Vc!ggRD~HPWy)x4=??Q|j0I+N zWmHVee1~2w-O~W+L6!54>xk=)p=caS4=QW#tlm6c**cAQoOgx)7wec;aYhBw6JtK8 LD;+pbSgmzF^UZ(D7;dj4UBO0Wk9*| z$#I;P>>^-$DnRyVR{}*5_EZ6R)o>j(C~~!vA8^{Til*yn)@@ees$t!{jbD+GQEc)J nfkTtm3$}7k26}-Jh>Lf#Z?+ej&pNqRLVB~S1Pc?(M4%`DzVT49 delta 459 zcmaE1bk>CLGcPX}0}vEndy&a5J&{j>@!LlAQ*4v{Sp-Y7ypO${T?DMB5Tu7cCx$hcBbXCvW)a8){$ijWIk=vZ$+I{#^-EDqEkogz z$FK!6!qry*WoZ?Pxm=ar=9Z8qRnW@Hqf93pUtR~T8vWIn-O?rA`$ zFamKgJICf_g7aC~#bs`=2v3NZTqmlaVmTpYfzw46^_whQ9X2y0HmGdZ*rc&p>mrNo YXKprerV9+h9|bnMh$}F$OaU4J0AvJmRR910 diff --git a/backend/app/db/tables.py b/backend/app/db/tables.py index 18a3140c..ded446b6 100644 --- a/backend/app/db/tables.py +++ b/backend/app/db/tables.py @@ -113,14 +113,6 @@ users_table = Table( Column("updated_at", DateTime, server_default=func.now()), ) -daily_reviews_table = Table( - "daily_reviews", metadata, - Column("id", Integer, primary_key=True, autoincrement=True), - Column("trade_date", Text, nullable=False, unique=True), - Column("content", Text, default=""), - Column("created_at", DateTime, server_default=func.now()), -) - stock_diagnoses_table = Table( "stock_diagnoses", metadata, Column("id", Integer, primary_key=True, autoincrement=True), diff --git a/backend/app/engine/__pycache__/scheduler.cpython-313.pyc b/backend/app/engine/__pycache__/scheduler.cpython-313.pyc index 9f7731d81ee85bf6aecd17a529bff5ee058bbc9c..ee98e7492a12bd029abd89d55c868f20214c32f6 100644 GIT binary patch delta 1064 zcmZvb%TE(g6vprEi$10g3Z*S7)PTZNwA0cGHAsA*;(`SZM4~2>p)F;!ovC+*7^00_EnJ+iz`<-vjoyV)ujX_Vt?RFwq zBilPluXoiG!k<qHSEGaxdPhI{ zQuL*-D-=VbeHyE17ImP>C_+mXgi8KWlCJp@p}9WMELzg$SxXGhV$mkHrL7S(HAHv) z=`M$8+IK}~42#YK7lI?$PAk5S>kiR%0B~Ts)0PN&*c=?DK=<5Ck1x@ag4x)LpuCs& z(O(^J%e<4fRNIRhQ57{WX{DSh3F+Y!xdd3f52VVqn*V`Q!ake;n2dq|<2&prDp@Hm zB!oJftlr7;TrD17KEXoV7)!dsWE|u~TpZ$D%)Q!zc;p2mM^sHyrTB1MsI&7ogQ|`r z2a=9qNr9Aa0#_eo@B|1Dj)H9Dq<9iCJVzN^v#95EUD4DQ2ofe4beqZD{QzgCpy`m1 z5`?;!)lG+H-3Pm>tEB0MRFuh+oI(288LIwgvXNOVC{UMGx$s<9blyagtS<#Zp9apA z+em<+ZE`j%J=W$)b7m_;*hff0Ou`_%wd&S0vMTAB#awo&kgM7Nmy|4tF+^a(UWU$k zBW#fY*j2rH&R8lPbrB*$^)ds$fh<#B*Tq|VoGV~ko@#_pY^-9BzOk(=)a>ow?cs_& zT;l>YF8F?CL#c3+oBp1ao=tCH^UT>FF68KLAe+OvYlgO;o%BQS0{tDVVUccyPDf@S z{cr+9CPvEyb6{m?Fg%E7=(X^6|9yu3 vjWCA%lVcw%YV1LclRUWW7t!bG>Z$3l11oqh6vc_X2iT0yH#jqnHt+uj?)DGL delta 2540 zcmaJ@Z%kX)6~FgAKmUC;*v19}#x{_ElLasdO>oLOLRz(@I16uNX;5oD@H4o;GkwoA z>h{4+G_53C6{DAFOsQ0VNHnisX4Fdcq)2lTP1=WPvPT!_8{M?2CgEEM`7mjlx^oQ< zBy090U!Qx=@BGd=_uc!mbL|(7S+>k(BZB8q>-|`!X2DX2Z!WZbX5_5(NUEEl{I;ck z^e}BNc86c;(n2n%j)U)kEuOLn{Nh9RJZw;`l-bEJA!c zQvR;R=ngqT!zMz-7%DNxjTPL}F?tk=`Oqj^3DqI~6hb1k z%cU$)j-#d#>d8oGj2+a!Y7l8?jBZApncKmP)>OELoyym=8(WkI`i3OcPfchzjR@aS z97TP!2SxEwI*R>ObFB~&crF;|^4@>{XutPbYDqa34=v9pgiH`{32%iw10WDo$y8r17fa+!hS%%Z_4BTs_n-@#DT+l1o zA*JLXgOilVN_E6`!JyXgv$Gx3(=#0rJ~sXCPQlUv;&&6j?mRDvF>ylC8a+wPwu9`Q zMD|4@`z~u>@WLatfuTCXJMEJm_D}}ENoeKY!LWheLtkl(?+;!WEL0!+T+{o2GBQ#8 zrT*9#wfmQA+gGgZ^V(ID^-AzkFg3Ys@}!R}o7$3VtnJF+rNKh|k?fhz&MlqGv&%h0 zg(IOt-Kjsb-~9@_p-Z8H`)E%4TjOtx`QGJYLxrQELId$`8j#KZ&qqeYI<^oE$=39i zE9@n2p=lpjvd88~APLBL~2FCT;(1((3SJ2 z8akwy+1{v`$i`!grQ+qPfa`m}1!BRO%i;j1oHL)o93@gutQlafTBLVtI@`jT_f&Lf zx1yTUXRCLzS)n4^2(}>3Uam`1$vo7FkO?Gg=*;%_E-zGSc!GIPua1*7kDGM>we#s_ zzk#gwnPi^2$7C)btACn|?3|9}D5=PF`hKIOK74%FjvWtaDSjFKDGl3ZRXhM+0cwAJC#C)r4 zhc^xr5>l_qPJ>>BWzeG-fSB@ZqqX<9y%JQQGJ+Th&y?j#`9W2K=?%cY1Rr@HrKGF6l8;zqv8TFI(qiDQ**y;eVO`O? zih9=?>$v*E49@!Uu43)rs?A%l`ZD^gHA|<(eBuFv4D}!>2ID4;EKQ&=jAcdRC~6!) zaP@flMCR>WtZ09E)zVlnyVD){BM`wjH$jB7TaeN@k5}tli^Iulx`pQS>3Q9%vv#pt znOLCG-Sf2oLY6Zq{5l!b2uyKe7XMI z;e!8#!tfc$VQzw|%$6-0*$!^%Akm{5(qJGpw%Gg)4JI>GHjwuhYhPQnxl``+xh&4e zg_gtl$kQEK{vXXAmhwI^89u|Mr}8KNm*vTQQd+AU-Rz#tYr1aA+Hxm;|4Pw&X4T}% zoK$+M@8LHTx3v+!sRXU{%ox1AA1kk0+hKmu`qx&ToPamUQ2r^h!2hM$usl8~MCPUi zNs5B!r^=6P-CBb2yOf)@zj$_!4ml4Jp)3QXZ8BxaZa2OSix@ugr1D4mX^$C4@k6fx X$MD0tKHUBAI~0xgZ?QB!P)`0|2(7Y< diff --git a/backend/app/engine/scheduler.py b/backend/app/engine/scheduler.py index 80d6b7ff..13f49925 100644 --- a/backend/app/engine/scheduler.py +++ b/backend/app/engine/scheduler.py @@ -39,22 +39,6 @@ async def _run_scan(session_name: str): await log_error("scheduler", f"定时扫描失败 ({session_name}): {e}", detail=traceback.format_exc()) -async def _generate_daily_review(): - """16:10 自动生成每日复盘报告""" - logger.info("=== 开始生成每日复盘报告 ===") - try: - from app.llm.daily_review import generate_review - result = await generate_review() - if result.get("status") == "ok": - logger.info(f"复盘报告生成成功: {result.get('trade_date')}") - else: - logger.warning(f"复盘报告生成失败: {result.get('message')}") - except Exception as e: - logger.error(f"复盘报告生成异常: {e}") - from app.db.error_logger import log_error - await log_error("scheduler", f"复盘报告生成异常: {e}", detail=traceback.format_exc()) - - async def _run_watchlist_analysis(): """收盘后自动分析所有用户自选股。""" logger.info("=== 开始自选股定时分析 ===") @@ -76,39 +60,26 @@ def setup_scheduler(): args=["pre_market"], id="pre_market", replace_existing=True ) - # 早盘扫描 09:35-09:55 每5分钟 + 10:00 - for m in range(35, 60, 5): + # 盘中扫描:按交易节奏执行,避免高频重复计算 + scan_schedule = [ + ("morning_open_0935", 9, 35, "morning_open"), + ("morning_open_0950", 9, 50, "morning_open"), + ("morning_mid_1020", 10, 20, "morning_mid"), + ("morning_mid_1050", 10, 50, "morning_mid"), + ("morning_mid_1120", 11, 20, "morning_mid"), + ("afternoon_1310", 13, 10, "afternoon"), + ("afternoon_1340", 13, 40, "afternoon"), + ("late_1410", 14, 10, "late_session"), + ("late_1440", 14, 40, "late_session"), + ("close_1500", 15, 0, "late_session"), + ] + for job_id, hour, minute, session_name in scan_schedule: scheduler.add_job( - _run_scan, CronTrigger(hour=9, minute=m, day_of_week="mon-fri"), - args=["morning_open"], id=f"morning_{m}", replace_existing=True - ) - scheduler.add_job( - _run_scan, CronTrigger(hour=10, minute=0, day_of_week="mon-fri"), - args=["morning_open"], id="morning_1000", replace_existing=True - ) - - # 上午盘中 10:10-11:30 - 每10分钟 - for h in [10, 11]: - start_m = 10 if h == 10 else 0 - end_m = 60 if h == 10 else 31 - for m in range(start_m, end_m, 10): - scheduler.add_job( - _run_scan, CronTrigger(hour=h, minute=m, day_of_week="mon-fri"), - args=["morning_mid"], id=f"morning_mid_{h}_{m}", replace_existing=True - ) - - # 午后扫描 13:00-14:00 - 每10分钟 - for m in range(0, 60, 10): - scheduler.add_job( - _run_scan, CronTrigger(hour=13, minute=m, day_of_week="mon-fri"), - args=["afternoon"], id=f"afternoon_{m}", replace_existing=True - ) - - # 尾盘扫描 14:00-14:50 - 每10分钟 - for m in range(0, 51, 10): - scheduler.add_job( - _run_scan, CronTrigger(hour=14, minute=m, day_of_week="mon-fri"), - args=["late_session"], id=f"late_{m}", replace_existing=True + _run_scan, + CronTrigger(hour=hour, minute=minute, day_of_week="mon-fri"), + args=[session_name], + id=job_id, + replace_existing=True, ) # 收盘总结 16:00(Tushare 日线数据通常在 15:30 后更新完成) @@ -117,12 +88,6 @@ def setup_scheduler(): args=["post_market"], id="post_market", replace_existing=True ) - # 每日复盘报告 16:10(扫描完成后生成) - scheduler.add_job( - _generate_daily_review, CronTrigger(hour=16, minute=10, day_of_week="mon-fri"), - id="daily_review", replace_existing=True - ) - scheduler.add_job( _run_watchlist_analysis, CronTrigger(hour=16, minute=20, day_of_week="mon-fri"), id="watchlist_analysis", replace_existing=True diff --git a/backend/app/llm/analysis_agent.py b/backend/app/llm/analysis_agent.py deleted file mode 100644 index aef3b591..00000000 --- a/backend/app/llm/analysis_agent.py +++ /dev/null @@ -1,252 +0,0 @@ -"""AI 深度分析 - -预先获取 K 线、资金流、技术信号等数据,一次性传入 LLM 生成结构化分析报告。 -不依赖 tool calling,避免 DeepSeek DSML 标签问题。 -""" - -import asyncio -import json -import logging -import re -import traceback - -from app.llm.client import chat_completion -from app.llm.prompts import TREND_BREAKOUT_ANALYSIS_PROMPT -from app.config import settings - -logger = logging.getLogger(__name__) - - -async def analyze_recommendations(result: dict) -> None: - """对所有推荐股票执行 AI 深度分析""" - recommendations = result.get("recommendations", []) - if not recommendations or not settings.deepseek_api_key: - return - - try: - await _do_analyze(result, recommendations) - except Exception as e: - logger.error(f"AI 分析任务异常: {e}") - from app.db.error_logger import log_error - await log_error("analysis_agent", f"AI 分析任务异常: {e}", detail=traceback.format_exc()) - for rec in recommendations: - if not rec.llm_analysis: - rec.llm_analysis = "AI 分析暂时不可用" - await _broadcast_llm_ready(recommendations) - - -async def _do_analyze(result: dict, recommendations: list) -> None: - """分析核心逻辑""" - market_temp = result.get("market_temp") - hot_sectors = result.get("hot_sectors", []) - - # 构建板块文本 - sectors_text = "\n".join( - f"- {s.sector_name}: 涨幅{s.pct_change}%, 资金流入{s.capital_inflow}万, " - f"涨停{s.limit_up_count}家, 热度{s.heat_score}分, 阶段={s.stage}" - for s in hot_sectors[:5] - ) if hot_sectors else "暂无板块数据" - - # 温度等级 - temp_val = market_temp.temperature if market_temp else 0 - if temp_val >= 60: - temp_level = "积极" - elif temp_val >= 30: - temp_level = "谨慎" - else: - temp_level = "低迷" - - enhanced_count = 0 - for rec in recommendations: - try: - # 预先获取该股票的详细数据 - stock_data = await _fetch_stock_data(rec.ts_code, rec.sector) - - strategy_label = "趋势突破" - signal_map = {"breakout": "突破型", "pullback": "回踩型", "launch": "启动型", "none": "无信号"} - entry_label = signal_map.get(rec.entry_signal_type, "无信号") - system_prompt = TREND_BREAKOUT_ANALYSIS_PROMPT - - user_msg = _build_user_message( - rec=rec, - strategy_label=strategy_label, - entry_label=entry_label, - market_temp=market_temp, - temp_level=temp_level, - sectors_text=sectors_text, - stock_data=stock_data, - ) - - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_msg}, - ] - - resp = await chat_completion(messages) - if resp and resp.content: - analysis = resp.content.strip() - rec.llm_analysis = analysis - rec.llm_score = _extract_score(analysis) - enhanced_count += 1 - else: - rec.llm_analysis = "AI 分析暂时不可用" - - except asyncio.CancelledError: - logger.warning(f"AI 分析 {rec.ts_code} 被取消") - break - except Exception as e: - logger.error(f"AI 分析 {rec.ts_code} 失败: {e}") - rec.llm_analysis = "AI 分析暂时不可用" - - # 无论成功失败都保存并广播 - await _save_llm_analysis_to_db(recommendations) - await _broadcast_llm_ready(recommendations) - - logger.info(f"AI 深度分析完成: {enhanced_count}/{len(recommendations)} 条") - - -async def _fetch_stock_data(ts_code: str, sector: str) -> str: - """预先获取个股详细数据,拼接为文本供 LLM 分析""" - from app.llm.tool_executor import ( - _get_stock_kline, - _get_stock_capital_flow, - _get_stock_technical_signal, - _get_sector_performance, - ) - - parts = [] - - # K 线(最近 30 天摘要) - try: - kline_text = await _get_stock_kline(ts_code, 60) - kline_data = json.loads(kline_text) - if isinstance(kline_data, list) and kline_data: - # 只取最近 10 条以控制 token - recent = kline_data[-10:] - kline_summary = "\n".join( - f" {d.get('trade_date', '')}: 收{d.get('close', '')} " - f"涨跌{d.get('pct_chg', '')}% 量{d.get('vol', '')} " - f"MA5={d.get('ma5', '')} MA10={d.get('ma10', '')} MA20={d.get('ma20', '')} " - f"DIF={d.get('dif', '')} DEA={d.get('dea', '')} RSI={d.get('rsi14', '')}" - for d in recent - ) - parts.append(f"## K线数据(近10日)\n{kline_summary}") - except Exception as e: - logger.debug(f"获取K线数据失败 {ts_code}: {e}") - - # 资金流向 - try: - flow_text = await _get_stock_capital_flow(ts_code, 5) - flow_data = json.loads(flow_text) - if isinstance(flow_data, list) and flow_data: - flow_summary = "\n".join( - f" {d.get('trade_date', '')}: 主力净流入{d.get('main_net_inflow', 0)}万" - for d in flow_data[-5:] - ) - parts.append(f"## 资金流向(近5日)\n{flow_summary}") - except Exception as e: - logger.debug(f"获取资金流向失败 {ts_code}: {e}") - - # 技术信号 - try: - signal_text = await _get_stock_technical_signal(ts_code) - parts.append(f"## 技术信号\n{signal_text}") - except Exception as e: - logger.debug(f"获取技术信号失败 {ts_code}: {e}") - - # 板块表现 - if sector: - try: - sector_text = await _get_sector_performance(sector) - parts.append(f"## 板块数据\n{sector_text}") - except Exception as e: - logger.debug(f"获取板块数据失败 {sector}: {e}") - - return "\n\n".join(parts) if parts else "暂无额外数据" - - -def _build_user_message( - rec, - strategy_label: str, - entry_label: str, - market_temp, - temp_level: str, - sectors_text: str, - stock_data: str, -) -> str: - """构建完整的用户消息(含预获取的数据)""" - return f"""## 量化系统数据 -- 股票: {rec.name}({rec.ts_code}) -- 所属板块: {rec.sector} -- 策略类型: {strategy_label} -- 入场信号: {entry_label} -- 综合评分: {rec.score}分({rec.level}) -- 各维度: 市场{rec.market_temp_score} | 板块{rec.sector_score} | 资金{rec.capital_score} | 技术{rec.technical_score} | 位置{rec.position_score} | 估值{rec.valuation_score} -- 信号: {rec.signal} -- 参考价: 入场{rec.entry_price or 'N/A'} / 目标{rec.target_price or 'N/A'} / 止损{rec.stop_loss or 'N/A'} -- 量化理由: {";".join(rec.reasons) if rec.reasons else "无"} - -## 市场环境 -- 市场温度: {market_temp.temperature if market_temp else 'N/A'}/100({temp_level}) -- 涨跌比: {market_temp.up_count if market_temp else 0}涨 / {market_temp.down_count if market_temp else 0}跌 -- 涨停: {market_temp.limit_up_count if market_temp else 0}家 - -## 热门板块 -{sectors_text} - -## 个股详细数据 -{stock_data} - -请根据以上所有数据,按照指定格式输出深度分析报告。""" - - -def _extract_score(text: str) -> float | None: - """从 AI 分析报告中提取评分(1-10)""" - match = re.search(r"###\s*AI\s*评分[^\d]*(\d+(?:\.\d+)?)", text) - if match: - score = float(match.group(1)) - return min(max(score, 1), 10) - return None - - -async def _save_llm_analysis_to_db(recommendations: list) -> None: - """将 AI 分析结果更新到数据库""" - try: - from app.db.database import get_db - from sqlalchemy import text - - async with get_db() as db: - for rec in recommendations: - if not rec.llm_analysis: - continue - await db.execute( - text( - "UPDATE recommendations SET llm_analysis = :analysis, " - "llm_score = :score " - "WHERE ts_code = :code AND date(created_at) = date('now', 'localtime') " - "AND scan_session = :session" - ), - { - "analysis": rec.llm_analysis, - "score": rec.llm_score, - "code": rec.ts_code, - "session": rec.scan_session, - }, - ) - await db.commit() - except Exception as e: - logger.error(f"保存 AI 分析到数据库失败: {e}") - from app.db.error_logger import log_error - await log_error("analysis_agent", f"保存 AI 分析到数据库失败: {e}", detail=traceback.format_exc()) - - -async def _broadcast_llm_ready(recommendations: list) -> None: - """通过 WebSocket 广播 AI 分析完成事件""" - try: - from app.api.websocket import broadcast_update - await broadcast_update({ - "type": "llm_analysis_ready", - "count": len([r for r in recommendations if r.llm_analysis]), - }) - except Exception as e: - logger.error(f"广播 AI 分析完成失败: {e}") diff --git a/backend/app/llm/daily_review.py b/backend/app/llm/daily_review.py deleted file mode 100644 index 7172001d..00000000 --- a/backend/app/llm/daily_review.py +++ /dev/null @@ -1,169 +0,0 @@ -"""每日复盘报告生成""" - -import logging - -from app.config import settings, today_trade_date - -logger = logging.getLogger(__name__) - - -async def generate_review() -> dict: - """生成每日复盘报告""" - from app.data.tushare_client import tushare_client - from app.data import tencent_client - from app.engine.recommender import get_latest_recommendations, get_latest_sectors - - latest_trade_date = tushare_client.get_latest_trade_date() - trade_date = today_trade_date() - - # 收集市场数据 - result = await get_latest_recommendations() - mt = result.get("market_temp") - sectors = await get_latest_sectors() - recs = result.get("recommendations", []) - - # 实时指数 - try: - index_data = await tencent_client.get_index_realtime() - except Exception: - index_data = {} - - # 构建数据摘要 - market_summary = "" - if mt: - market_summary = ( - f"市场温度: {mt.temperature}/100, " - f"上涨{mt.up_count}家/下跌{mt.down_count}家, " - f"涨停{mt.limit_up_count}家/跌停{mt.limit_down_count}家, " - f"连板最高{mt.max_streak}板, 炸板率{mt.broken_rate}%" - ) - - index_summary = "" - name_map = {"000001.SH": "上证", "399001.SZ": "深证", "399006.SZ": "创业板"} - for code in ["000001.SH", "399001.SZ", "399006.SZ"]: - d = index_data.get(code, {}) - if d: - index_summary += f"{name_map[code]}: {d.get('price', 0):.2f} ({d.get('pct_chg', 0):+.2f}%), " - - sector_summary = "热门板块: " + "、".join( - f"{s.sector_name}({(getattr(s, 'realtime_pct_change', None) or s.pct_change):+.1f}%)" for s in sectors[:5] - ) - - rec_summary = "" - for r in recs[:5]: - signal_map = {"breakout": "突破", "breakout_confirm": "确认", "pullback": "回踩", - "launch": "启动", "reversal": "反转"} - st = signal_map.get(r.entry_signal_type, r.entry_signal_type) - rec_summary += f"\n- {r.name}({r.ts_code}): {st}型, 评分{r.score}, {r.signal}" - - user_msg = f"""请根据以下数据生成今日A股市场复盘报告(中文): - -日期: {trade_date} -Tushare 最新交易日: {latest_trade_date} -{market_summary} -{index_summary} -{sector_summary} - -今日推荐股票: -{rec_summary} - -请按以下格式输出(Markdown格式,总字数300-500字): -## 市场概况 -(指数走势、量能变化) - -## 板块热点 -(哪些板块领涨、资金流向) - -## 交易机会 -(今日推荐个股简要点评) - -## 明日关注 -(关注方向和操作建议)""" - - if settings.deepseek_api_key: - try: - from app.llm.client import get_client - - client = get_client() - response = await client.chat.completions.create( - model=settings.deepseek_model, - messages=[ - {"role": "system", "content": "你是一位专业的A股市场分析师,擅长市场复盘和策略分析。回复使用Markdown格式,简洁专业。"}, - {"role": "user", "content": user_msg}, - ], - max_tokens=1500, - temperature=0.5, - ) - content = response.choices[0].message.content.strip() - generated_by = "llm" - except Exception as e: - logger.error(f"生成复盘报告失败,使用规则兜底: {e}") - content = _build_fallback_review( - trade_date=trade_date, - market_summary=market_summary, - index_summary=index_summary, - sector_summary=sector_summary, - recommendations=recs, - ) - generated_by = "rules" - else: - content = _build_fallback_review( - trade_date=trade_date, - market_summary=market_summary, - index_summary=index_summary, - sector_summary=sector_summary, - recommendations=recs, - ) - generated_by = "rules" - - try: - # 保存到数据库 - from sqlalchemy import text - from app.db.database import get_db - async with get_db() as db: - await db.execute( - text( - "INSERT OR REPLACE INTO daily_reviews (trade_date, content) " - "VALUES (:td, :content)" - ), - {"td": trade_date, "content": content}, - ) - await db.commit() - - logger.info(f"已生成 {trade_date} 复盘报告 ({generated_by})") - return {"status": "ok", "trade_date": trade_date, "content": content, "generated_by": generated_by} - - except Exception as e: - logger.error(f"保存复盘报告失败: {e}") - return {"status": "error", "message": str(e)} - - -def _build_fallback_review( - trade_date: str, - market_summary: str, - index_summary: str, - sector_summary: str, - recommendations: list, -) -> str: - """LLM 不可用时生成结构化规则复盘,避免页面空白。""" - actionable = [r for r in recommendations if getattr(r, "action_plan", "") == "可操作"] - watch = [r for r in recommendations if getattr(r, "action_plan", "") == "重点关注"] - top_recs = recommendations[:5] - rec_lines = "\n".join( - f"- {r.name}({r.ts_code}):{getattr(r, 'action_plan', '观察')}," - f"{getattr(r, 'entry_signal_type', 'none')} 信号,评分 {getattr(r, 'score', 0)}。" - for r in top_recs - ) or "- 暂无推荐标的。" - - return f"""## 市场概况 -{trade_date} 市场温度处于中性偏谨慎区间。{market_summary or "暂无市场温度数据。"} {index_summary or ""} - -## 板块热点 -{sector_summary or "暂无板块热度数据。"} 当前板块证据主要用于确认推荐方向是否有资金和赚钱效应支撑。 - -## 交易机会 -今日推荐池共 {len(recommendations)} 只,其中可操作 {len(actionable)} 只、重点关注 {len(watch)} 只。当前更适合按触发条件等待确认,不宜把观察标的直接当作买入标的。 -{rec_lines} - -## 明日关注 -优先跟踪重点关注标的能否满足触发条件,同时观察主线板块是否延续。若市场温度回落或板块资金退潮,应降低仓位并把未确认标的转回观察池。""" diff --git a/backend/app/llm/enhancer.py b/backend/app/llm/enhancer.py deleted file mode 100644 index 1d45eade..00000000 --- a/backend/app/llm/enhancer.py +++ /dev/null @@ -1,15 +0,0 @@ -"""推荐结果 LLM 增强 - -现在统一使用 analysis_agent 模块进行深度分析。 -此文件保留为兼容入口,内部直接调用 analysis_agent。 -""" - -import logging - -logger = logging.getLogger(__name__) - - -async def enhance_recommendations(result: dict) -> None: - """对推荐结果进行 LLM 增强分析(兼容入口,委托给 analysis_agent)""" - from app.llm.analysis_agent import analyze_recommendations - await analyze_recommendations(result) \ No newline at end of file diff --git a/frontend/.next/server/app-paths-manifest.json b/frontend/.next/server/app-paths-manifest.json index 21ce9557..a923b582 100644 --- a/frontend/.next/server/app-paths-manifest.json +++ b/frontend/.next/server/app-paths-manifest.json @@ -1,7 +1,7 @@ { - "/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js", "/(auth)/dashboard/page": "app/(auth)/dashboard/page.js", "/(auth)/chat/page": "app/(auth)/chat/page.js", "/(public)/login/page": "app/(public)/login/page.js", + "/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js", "/(public)/page": "app/(public)/page.js" } \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 671ffb1d..af7fe275 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -608,7 +608,6 @@ export interface DataStats { tracking: number; sector_heat: number; market_temperature: number; - daily_reviews: number; low_score_count: number; latest_date: string; earliest_date: string;