From d9e77a7bec573af470a9169cb305293e572b98c0 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 23 Apr 2026 17:59:43 +0800 Subject: [PATCH] 1 --- .../app/__pycache__/config.cpython-313.pyc | Bin 6125 -> 6341 bytes .../__pycache__/intraday.cpython-313.pyc | Bin 17020 -> 22142 bytes backend/app/analysis/intraday.py | 95 +++++++++++++++++- backend/app/analysis/sector_realtime.py | 48 ++++++++- .../api/__pycache__/market.cpython-313.pyc | Bin 9765 -> 10314 bytes .../recommendations.cpython-313.pyc | Bin 9117 -> 9124 bytes .../api/__pycache__/sectors.cpython-313.pyc | Bin 6911 -> 7522 bytes backend/app/api/market.py | 11 +- backend/app/api/recommendations.py | 2 +- backend/app/api/sectors.py | 15 ++- backend/app/config.py | 21 ++-- backend/app/data/eastmoney_client.py | 85 ++++++++++++++++ .../__pycache__/screener.cpython-313.pyc | Bin 71016 -> 71891 bytes backend/app/engine/screener.py | 35 +++++-- backend/app/llm/strategy_board.py | 8 +- backend/astock.db | Bin 2797568 -> 2797568 bytes frontend/.next/server/app-paths-manifest.json | 2 +- frontend/src/app/(auth)/dashboard/page.tsx | 4 +- frontend/src/app/(auth)/sectors/page.tsx | 7 +- frontend/src/lib/api.ts | 2 +- 20 files changed, 302 insertions(+), 33 deletions(-) diff --git a/backend/app/__pycache__/config.cpython-313.pyc b/backend/app/__pycache__/config.cpython-313.pyc index bc830f608085603dd5e7b47e42d598a4e6a8612d..bc43f2b8b488fb5c68753f5e432bbb0cbcd88f35 100644 GIT binary patch delta 682 zcmaE>f7FogGcPX}0}!~rd70@fypbHy?Ct_8f?A>vw&i8x{->?I&${_w>sIf==snyf{+AVIJeu<8_$G;2wIN@8U(C~g%L6ed@Rsnr(&*$qHk zTnJ`-6<}ahdcq~tuhXS7v0?%LWpRVcT!szKH`uw_s~fAEYiBTCW|zFdFY5EvSu5KcQ{>^ zwLhSGS=Jq71UG-tQNIesf%M8SymjH>W4AG203Jgr4v<8$;5ny15 z2B`!C3|XMQbjD~_boo5SPzIO@iEIoE5sX&l3=D}N)l49}%utQ9WrQhC0XkFxi-jr- zn#`qUlj}vj*`DlL@@&TB$rnUr>Yui?zTeaFbi(SVy&X^XOnltX_-x9y=d))#ozVAw zPv_HRP0!}dQ3xq5&PXguRd_b1;n}ne5Z%wFFMY9p#*-br&t|NDvb*c)l9liG^gQjD z`DD+Gr>z}N*Ufu2eH)jtp2E}K%}@62$7MwQ)9ss{@9sx2qkq+lWsOgE_dZ?H@?=+^ zf}yFEv4O(V&fe#f%!F1wdW{5Eom48D9h#cx110%U$G_TT-*U zZeiUGevv0UGM9Pe8eH$OOI(oEyDV#dgJ1Xwx6Ea3x$E2pOLP|NU0^Yoyk9&?9%NRL wJBSDd5nzirY;yBcN^?@}ib^LtNMuX#Ga7Di-l4M5>jMLjc|zqIhytqz0O>^4w*UYD diff --git a/backend/app/analysis/__pycache__/intraday.cpython-313.pyc b/backend/app/analysis/__pycache__/intraday.cpython-313.pyc index 5459eee6db425ff5464553766a2ce8b8addf693a..80b07a209dbc439c4e690a10de56be09d4138482 100644 GIT binary patch delta 8784 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 MarketTem limit_up_count = prev_temp.limit_up_count limit_down_count = prev_temp.limit_down_count + eastmoney_quotes = await get_a_share_realtime_ranking(page_size=6000) + if eastmoney_quotes: + up_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) > 0) + down_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) < 0) + limit_up_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) >= _limit_threshold(q.get("ts_code", ""))) + limit_down_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) <= -_limit_threshold(q.get("ts_code", ""))) + logger.info( + "东方财富实时市场温度: 上涨=%s 下跌=%s 涨停=%s 跌停=%s (共%s只)", + up_count, + down_count, + limit_up_count, + limit_down_count, + len(eastmoney_quotes), + ) + else: + logger.warning("东方财富全市场实时行情为空,尝试腾讯批量行情补充涨跌家数") + try: - stock_basic = tushare_client.get_stock_basic() - if not stock_basic.empty: + if not eastmoney_quotes: + stock_basic = tushare_client.get_stock_basic() + if stock_basic.empty: + raise ValueError("股票基础列表为空") all_codes = stock_basic[~stock_basic["name"].str.contains("ST", na=False)]["ts_code"].tolist() if all_codes: @@ -61,6 +85,8 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem # ── 用东方财富 clist API 统计涨停跌停(比腾讯涨停价字段更可靠) ── try: + if eastmoney_quotes: + raise RuntimeError("已使用东方财富全市场实时涨跌停统计") realtime_limit_up_count = 0 realtime_limit_down_count = 0 @@ -108,7 +134,8 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem limit_down_count = realtime_limit_down_count logger.info(f"东方财富盘中涨跌停: 涨停={limit_up_count} 跌停={limit_down_count}") except Exception as e: - logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}") + if not eastmoney_quotes: + logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}") # ── 温度分数:基于实时涨跌比重新计算 ── ratio = up_count / max(down_count, 1) @@ -135,6 +162,13 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem ) +def _limit_threshold(ts_code: str) -> float: + code = ts_code.split(".")[0] if ts_code else "" + if code.startswith(("300", "301", "688")): + return 19.8 + return 9.8 + + async def intraday_filter_stocks( hot_sectors: list[SectorInfo], ) -> list[dict]: @@ -245,6 +279,59 @@ async def intraday_filter_stocks( return top +async def intraday_active_market_recall(limit: int = 80) -> list[dict]: + """实时全市场活跃股召回,不依赖 Tushare 板块成分映射。""" + quotes = await get_a_share_realtime_ranking(sort_by="f8", descending=True, page_size=800) + if not quotes: + return [] + + results = [] + for item in quotes: + ts_code = item.get("ts_code", "") + name = item.get("name", "") + if not ts_code or not name or "ST" in name: + continue + pct_chg = float(item.get("pct_chg", 0) or 0) + turnover_rate = float(item.get("turnover_rate", 0) or 0) + circ_mv_raw = item.get("circ_mv") + circ_mv = float(circ_mv_raw or 0) / 100000000 if circ_mv_raw else None + if pct_chg <= 0 or pct_chg >= _limit_threshold(ts_code): + continue + if turnover_rate < max(settings.min_turnover_rate * 0.5, 1.0): + continue + if circ_mv is not None and circ_mv > 0: + if circ_mv < settings.min_circ_mv or circ_mv > settings.max_circ_mv * 1.5: + continue + + recall_score = 35 + recall_score += min(max(pct_chg, 0), 8) * 4 + recall_score += min(turnover_rate, 12) * 2 + if item.get("main_net_inflow", 0) > 0: + recall_score += 8 + + results.append({ + "ts_code": ts_code, + "name": name, + "sector": "实时活跃", + "sector_stage": "intraday", + "price": item.get("price"), + "pct_chg": pct_chg, + "turnover_rate": turnover_rate, + "circ_mv": circ_mv, + "pe": item.get("pe"), + "pb": item.get("pb"), + "volume_ratio": None, + "main_net_inflow": float(item.get("main_net_inflow", 0) or 0) / 10000, + "inflow_ratio": 0, + "recall_score": round(recall_score, 1), + "recall_tags": ["realtime_active"], + "stock_role_hint": "今日全市场活跃股", + }) + + results.sort(key=lambda x: (x["recall_score"], x.get("turnover_rate", 0)), reverse=True) + return results[:limit] + + def _score_intraday(quote: StockQuote) -> float: """盘中评分逻辑(替代资金流向评分) diff --git a/backend/app/analysis/sector_realtime.py b/backend/app/analysis/sector_realtime.py index 90e07da2..c216f5b4 100644 --- a/backend/app/analysis/sector_realtime.py +++ b/backend/app/analysis/sector_realtime.py @@ -6,7 +6,7 @@ import logging -from app.config import should_prefer_realtime_today +from app.config import should_prefer_realtime_today, today_trade_date from app.data.eastmoney_client import get_sector_realtime_ranking from app.data.models import SectorInfo from app.data.tushare_client import tushare_client @@ -37,6 +37,52 @@ def _apply_empty_overlay(sector: SectorInfo) -> SectorInfo: return sector +def _sector_from_eastmoney(item: dict) -> SectorInfo: + """把东方财富板块榜转换成今日展示用 SectorInfo。""" + sector = SectorInfo( + sector_code=item.get("sector_code", ""), + sector_name=item.get("sector_name", ""), + trade_date=today_trade_date(), + pct_change=float(item.get("pct_change", 0) or 0), + capital_inflow=0, + limit_up_count=0, + days_continuous=0, + heat_score=round(max(float(item.get("pct_change", 0) or 0), 0) * 12, 1), + stage="intraday", + turnover_avg=float(item.get("turnover_rate", 0) or 0), + realtime_pct_change=float(item.get("pct_change", 0) or 0), + realtime_limit_up_count=None, + realtime_amount=round(float(item.get("amount", 0) or 0) / 10000, 2), + realtime_turnover_rate=float(item.get("turnover_rate", 0) or 0), + realtime_up_count=int(item.get("up_count", 0) or 0), + realtime_down_count=int(item.get("down_count", 0) or 0), + is_realtime=True, + data_mode="realtime_today", + ) + if item.get("leading_stock_name"): + sector.leading_stocks_realtime = [{ + "ts_code": item.get("leading_stock_code", ""), + "name": item.get("leading_stock_name", ""), + "pct_chg": float(item.get("leading_stock_pct", 0) or 0), + "amount": 0, + }] + sector.leading_stocks = sector.leading_stocks_realtime + return sector + + +async def get_today_realtime_sector_board(limit: int = 20) -> list[SectorInfo]: + """用东方财富今日板块榜生成展示列表,作为 Tushare/定时扫描滞后的兜底。""" + try: + em_sectors = await get_sector_realtime_ranking(page_size=max(limit, 20)) + except Exception: + logger.warning("东方财富今日板块榜获取失败") + return [] + + sectors = [_sector_from_eastmoney(item) for item in em_sectors[:limit]] + sectors.sort(key=lambda s: s.realtime_pct_change if s.realtime_pct_change is not None else s.pct_change, reverse=True) + return sectors + + async def enrich_sectors_with_realtime(sectors: list[SectorInfo]) -> list[SectorInfo]: """按需为板块快照追加实时字段并重排。""" if not sectors: diff --git a/backend/app/api/__pycache__/market.cpython-313.pyc b/backend/app/api/__pycache__/market.cpython-313.pyc index b8d34fb1b2c27e1aee31c564264e8e39e68e91ed..2eff700977e704f44802e70aa14c90bd2691a642 100644 GIT binary patch delta 2363 zcma)7X>1$E72eq;SG=^8NQo8?(G*FEVXPyHq&l)=DXn7*5L?;Nnn(h%AvWYnrc83> z*_B--KqWcNQKy$Nanuw_;s8mTpnw~PXzJ#G;P_8rHvkO-aj@~P)RdqfMTG`P@1}2- zUgszZbU=Q5*Y{?2_PxhfzIo}O@JVB%gMsD!<7VNG$SI)>HJwUKb&b2RJHm)aw56KH zn{ji5$#79dw2O{DBszy#?8$g06XdAiJ@p%(wdIyl^I2O>g?N$A>NB;3X9&@a7)FeU zEtzeDtk@c0W-YU5sBUI(n;A6lRbz;eIuSajvl)SW%)v%P8vw2CAEB|1h zW09Afw#LZ2)+mdEkh$zp{;?+q=PocL1pL zCL-8~^w3BZl56tJLiS-r!g<_Zi8$I2KS<+y$%r%N-w9%p#fF8-DoZm!3i60Egm#k^ z=R@r8t(Ctx&#;42t>813d9!8L)Hc(WcI-j7-Bcuqj4)}(q@UjNZb5Vn(Y&zIu3aFR z_gHTH0^j~$e0!b0?|<`!G&>cnXNKH%Gw-c)z+R&J01}&~+R2(Qikiu1!Z6^lo1*F- zUTn4k-Q+=}E8S?a^9_(wFg1zh@*Hra?O(EZfz$FiN8%x>HSG`>(Ko;y@P8q?89dw4 zcJQ>No#wNK872>jBaG-5m;nnU3rvBnz9u7NITT_iV`SMCct8yHI>eCJE{4VJy&M#K z$4x0J#%@Y+v9m7qS;VehYn-t$W9-C=fsYz^pMeQLH1RJ3KPE3W^)>!=54W9}MmxD_ zluT4UYI=|jwZkEjNavu2;KzDQD$dJhfsupWZ*{gn0H#s81IjFS#He8%_2>eYrJ`0S z$yrSyzx9R}RZS|&x^)`M>S5g~m*%vEc|Q=>pI@szzfxQK<;LT`xbgN6Ys;$}r=P2> zz8aYy0PmmP`9ba6>5Z4)s+~Jgd;N(U@BFm(_)|CDUE6r()f;c0Twi&mcJ_t!l{f22 z)_=bGhouwqUYd1vY5m#PYv)hYR^NiC_0`JXynjbMA$n2)`b^3i2Pw;{sy9eEt)P_E zq(^tCM~hN1cUUeh;1Lq>wJwfQhEJNC>y^thg|gg><(yI~$>qF^@jcYpAb&^B&1BWR zCEa+0`bKGG;(N#$-%fNd`Ox?EDcx#J3|&y=oTlJxQJR5c&FDf&!bhOnZ|bTWh>;_ZZ!b;!UqM?uC z{GZ3jvVX^7Dvr*e@tyoj97rz*Q+93*jqlf6=pl?l9YIw*5LIS}!x|56s-x&TH7!TpQ04MM; zV3MoPWg~?#oN2khdo^Ar&VpjV&&che1iHI28R}r$z6Vy_l2h`~Z@HqP%J@kd_Y`?L zoJ3*r>u|?FgofD467(ljDL!d1XkVzb;*7ef@6ZJ*rf{ZW-@d`#zfAQL6dnVB<9(OP zixidsHYpuOg$4fAR7|JC=4Y@r>7UZ(zd^!JQz;LCp8(LgV@h#9{yrIt#?S;g5)CDO zM14P|KI?Q*fnKob4TW;Pkdsv$p+5R}_#`~#f=c9^H_lf#+XiB9HWq=&`*6_p{(KERHdDwTjms1 zgU2i6wE7z~UV(8-^c=4-nC@S1HQU`KB|l%3NANkYkq_cg%j{KVmi#mRQu989_)j>7 R4Q{bi+&W0kcB&T2{SO48S#baW delta 1778 zcmZ8hU2Icj7(U)Q2aYuBz_9h9FkVBna9FjCNsa9A8}aXGYSqtLD8J79>R9~pMEPT?ThXpaW1CEI%Awd6x%V+Q7`qzxN)JAQyLZfHljHC zcirEW&Y0OlcW!-Nh^Z5O-EQ zOL{lV91YZC*#aZ=Jo1C~e~*V27S!habm3B;MSA@jO_inU7+2y&uvGDuLnHz82}v*8>4X+wFk-e+HOEOht93+q$AO`8^jDxEepyzkKq{q zt|LPBv73&U`Jcs_%Xy9O9^uFQ;I&r$uDUmM6?lq!m?)kxKfB@?AYS%`rw@>L5%+uh z3SJ3f>$eAy&XLCdv3+1`dcx*|jLjpBZcb?#arBdVY!NC!NBG@bP6S5lSnK8%xD!F< z^F>LR{oxFaDuJwB2`X($NC{^J1ul3~YF9corI-@mkhY0RXI4sbHtq;N6t_?UBtzd@ zc+0{o7M9tPZ=35bnqk@tR0DT$LN!cjl4|-HQ_`lZ#(eFV z-^;@h#{p~@9$+5^BB=rNHE0*L!YtOD z)QrNJ%8W)2qTL2XtJTuX6nhj1yPQ@cdH_6hfW?D*$RTzrcyNdA1WW9RMXly@Tg_M1 zCv-zKW_5ZVqK?8(AFVwM?jlX=GS`;+A(6YE=v__pF1~c9pIr@|mvW0!cLFRG-YZ8v zVf%U~D~5MEG9jnD&V?lT&x33!+_R9+l4~SSp~Z9kwr;BsB4iYX|6cu;JW diff --git a/backend/app/api/__pycache__/recommendations.cpython-313.pyc b/backend/app/api/__pycache__/recommendations.cpython-313.pyc index d545b149504c7802cf49b4a59b75165c09197e50..af1ceb67d34b916f74fcf977c15020a865fc35e7 100644 GIT binary patch delta 58 zcmbR1zQmpPGcPX}0}u$meVO@iBd?Ym4_{GgVophBZfbl<{$yLZr;GuUmF2bAH7zcR Ns_w|$94zm}0su786AAzT delta 51 zcmZ4DKG&W1GcPX}0}vd%_9FB4MqVvBHjd1^lA^@P&T>x~{U>Y6Yq2RCO^n*XyE#hU Giv<9_tq`05 diff --git a/backend/app/api/__pycache__/sectors.cpython-313.pyc b/backend/app/api/__pycache__/sectors.cpython-313.pyc index 8f0c39f9c2159caebfed138648eb93d0aa197afb..2bb537afdfe0d23761f527ff77d31c041b2e9c7c 100644 GIT binary patch delta 2223 zcmZ`)UrbY17(eI!X-i8>=`Agm0tNY3z#;;QMV!nrYEdxURn1_j9leDTxFzRaHrtS zJLfy!ch32~bN=hO8+AQ!I_(HP`@ieiKHrk765m>Cu3e0aL<}Jr%fhfFZY9>Zjo6qj z4wuC3#Llo~*by%!r3_n#opBd&h0p|7k7S!%(t&0BZjO{ql#SI<2Ty*oqX*|1*&~-u zly`HoGk|7<8QeqZ{qPgdGfZ8ZrjlI7Ox=%}QcG#7JnUV1*n&Bu8rO`OwNr|o)N|?7 z$s|!yvwC(;NovZZo+HUub19Mzb7oMR&MnNQlk-H$D5N-yS()yazG6f@t%bEzTkBwu`DG7kY4C% zVPh%)wKC)b)DFnI2q03}s$vW5_M%om!0ssEVhe0{Q7a%|cNTE51-7@S0a|35?2lLm z@nLQVAA-zxpb%0msyNyaL#G1i<#(8V|L03AJojsY<)YLpwk4|W~rdQL+ z!lb@Hl;mc)DB&7HZy8ccg@p>5^@Og+qdIcOvb zEcKzyK*g-1Q|O@uRWz@9Th?4Hr-tryML%l3Sa+dyEzofz(D6ODBKU7vx2`noTC?_E zxAv~=8NDe_uFA^l9_3cuzN^}`o|Uneet7$O-I2AbrXwr1$~%s#UwlnxhyD!qUh=O7 z2iAN8pj38$b#%3|f6dc>#=7h%`3y^2yS}rY*>$e-Y;?_0d&5z?GMF&Fwl^*&F)9n+ zx1iureBXxr&*QtE{zvyML9gYJ_ABxyj+K4jl$*nAjNxetVOu| zoCNra2gllkE7DeqcfQaRiwHjk2dto7Ys6HxFxkdrL|J>VpU~(V1rZyfw;SxD-mV>_oOb4&n<9jxXe>)I{ zNGlaK;}h?fP%tAE{%RVr=_;0mmP}_%k-aS>Zk(?87;iH|zJXenj1o=8jaQ1~^pu{> zspL(VVgK?eUkC4e5nvdv(;5nUjc>L@cjoKKo|>PJOy<-~cB)uvrk}EO_agd&Hl2d% zoSe-nst)m$2fVBpOdlPis8d;0i4bKnH#euKX@#)TH7z8!po2meW%60Dq{YDkVtY;w zQ|FbQ|^jVSE~>_I2XQ Zi_${`%>y2B-g_LNd(+0vK(j#S{vTHa)7St2 delta 1679 zcmZuwTTC2P7(QoacJ{_z*qyzw+_u~okUK%B_M*VH5Wp~5LjrL#*&Sf(G7D!0Ea?Mi zpN-;q>5E2NA2cRL%e#pWwLW6{&^Rd%ZETb$(e{N0qyITuXls~<|9t2B|MTDH(a67p z{wF@42f=sl!XJf3Wyc@KKku~Gk0gOArqNn$E0(@MO+-qlkN!*@g&iL(128@ z>gvX-dsrae1@CMX{XP(G^$cR>6d|=z^)C1Z1hpc8u1Ht#V3|h5e`4!9u??wyZX5WY zZD1*w3R4$u!WKj7T5)6jv6jj*K|#qJN1IhYZNWkrPk_L!t`|zYt>%H!wk9Vxl-3 z;u-hCwRVcbY}U5mJnCfuFx1c|N{t-^|)Ep6zesBPN*yjCb~ zW=%s|HJ3{kX@VfOVp?Q9XRQ-GYY{E4XY-n+rxL^oO2XENR5RgcBEUqDiAws3yLYsP z!CEF@e-No>BFO}F29X9P8kyjMM9tVtISG`Ch>HpCH$~s@v`?{M$?`|o(ComHZe=0I ztf}X$5;4gDu+R@={sTYK0oq1SWfW<>r?lPow{52%C_UfB?kU6f{lnYo-AeB*>^jr? zi??=fC{6EHG>^?-6iC_r;iJFh8bv-b`@qu&{_%GFF&?j%c0zz|;qeCP%SJ#yI!9Z^ zo25Ii^uy(+2Fzr$W4wJ=_if17M<0Y+!q3~ms~7=)bUu0woR(YGv+wGgw#;FkRMT%_)p(TNjeUw!RF7Y*=j#w=>rh^WEGb!?pI* zjirL2cM&~TT3OMJyiWM;+cGJwTX11n+hPG;vK{16dC$otyYlp0udS5w>qY$>c^^2| zBy$bKp@=d56-6JT+Q(>YANB2{ bool: """是否应该优先使用“今天”的实时数据。 规则: - 1. 非交易日直接 False - 2. 交易日内(含午休、收盘后)如果 Tushare 最新交易日还不是今天,则优先实时 - 3. 即便 Tushare 最新交易日已经是今天,只要仍处于 15:30 前的延迟窗口,也优先实时 + 1. 非工作日直接 False + 2. 交易日 09:15 后都优先实时源,包括收盘后;腾讯/东方财富会保留当日收盘快照 + 3. 盘前不使用今日实时源,避免拿到昨日收盘价却标成今日 """ - if not is_market_session() and not is_pre_close(): + from zoneinfo import ZoneInfo + + now = datetime.now(ZoneInfo("Asia/Shanghai")) + if now.weekday() >= 5: + return False + + t = now.hour * 100 + now.minute + if t < 915: return False today = today_trade_date() - if latest_trade_date and latest_trade_date != today: - return True + if latest_trade_date and latest_trade_date.replace("-", "") > today: + return False - return is_market_session() or is_pre_close() + return True diff --git a/backend/app/data/eastmoney_client.py b/backend/app/data/eastmoney_client.py index 7b8dd70c..19191b22 100644 --- a/backend/app/data/eastmoney_client.py +++ b/backend/app/data/eastmoney_client.py @@ -140,6 +140,91 @@ async def get_sector_realtime_ranking( return [] +async def get_a_share_realtime_ranking( + sort_by: str = "f3", + descending: bool = True, + page_size: int = 6000, +) -> list[dict]: + """获取 A 股实时行情列表,用于市场温度和实时候选召回。""" + cache_key = f"ashare_rt:{sort_by}:{descending}:{page_size}" + cached = cache.get(cache_key) + if cached is not None: + return cached + + params = { + "pn": "1", + "pz": str(page_size), + "po": "0" if descending else "1", + "np": "1", + "ut": "b1f8f8f8", + "fltt": "2", + "invt": "2", + "fid": sort_by, + "fs": "m:0+t:6,m:0+t:80,m:0+t:81+s:2048,m:1+t:2,m:1+t:23", + "fields": "f2,f3,f6,f8,f9,f12,f14,f20,f21,f23,f62", + } + + try: + async with httpx.AsyncClient() as client: + resp = await client.get( + SECTOR_LIST_URL, + params=params, + headers=SECTOR_HEADERS, + timeout=12, + follow_redirects=True, + ) + data = _parse_eastmoney_json(resp, "A股实时行情") + + items = data.get("data", {}).get("diff", []) + result = [] + for item in items: + pct = item.get("f3") + price = item.get("f2") + if pct == "-" or price == "-" or pct is None or price is None: + continue + result.append({ + "ts_code": _eastmoney_code_to_ts(str(item.get("f12", ""))), + "name": item.get("f14", ""), + "price": float(price or 0), + "pct_chg": float(pct or 0), + "amount": float(item.get("f6", 0) or 0), + "turnover_rate": float(item.get("f8", 0) or 0), + "pe": _safe_float(item.get("f9")), + "pb": _safe_float(item.get("f23")), + "total_mv": _safe_float(item.get("f20")), + "circ_mv": _safe_float(item.get("f21")), + "main_net_inflow": _safe_float(item.get("f62")) or 0, + }) + + ttl = 60 if _is_trading_hours() else 300 + cache.set(cache_key, result, ttl) + logger.info("东方财富A股实时行情: 获取 %s 只", len(result)) + return result + except Exception as e: + logger.error(f"东方财富A股实时行情获取失败: {e}") + await log_error( + "eastmoney", + f"东方财富A股实时行情获取失败: {e}", + detail=f"sort_by={sort_by}, page_size={page_size}", + ) + return [] + + +def _eastmoney_code_to_ts(code: str) -> str: + if code.startswith("6"): + return f"{code}.SH" + return f"{code}.SZ" + + +def _safe_float(value) -> float | None: + if value in (None, "", "-"): + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + async def get_min_kline( ts_code: str, period: str = "5", diff --git a/backend/app/engine/__pycache__/screener.cpython-313.pyc b/backend/app/engine/__pycache__/screener.cpython-313.pyc index 19e80bcf3bceb69f07391401cef88b50e3e72d22..1f093c4572e73e637c5ebbb30043d601a814551a 100644 GIT binary patch delta 10504 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 diff --git a/backend/app/engine/screener.py b/backend/app/engine/screener.py index 9dd64a36..7dc80362 100644 --- a/backend/app/engine/screener.py +++ b/backend/app/engine/screener.py @@ -25,11 +25,17 @@ import pandas as pd from app.analysis.market_temp import calculate_market_temperature from app.analysis.sector_scanner import scan_hot_sectors +from app.analysis.sector_realtime import get_today_realtime_sector_board from app.analysis.trend_scanner import scan_trend_breakout from app.analysis.signals import generate_signals -from app.analysis.intraday import intraday_market_temperature, intraday_filter_stocks, intraday_sector_scan +from app.analysis.intraday import ( + intraday_active_market_recall, + intraday_market_temperature, + intraday_filter_stocks, + intraday_sector_scan, +) from app.data.models import MarketTemperature, SectorInfo, TechnicalSignal, Recommendation -from app.config import settings, is_trading_hours, is_market_session, should_prefer_realtime_today +from app.config import settings, should_prefer_realtime_today from app.data.tushare_client import tushare_client from app.llm.strategy_selector import select_strategy_profile @@ -48,8 +54,8 @@ async def run_screening(trade_date: str = None) -> dict: """ latest_trade_date = tushare_client.get_latest_trade_date() intraday = should_prefer_realtime_today(latest_trade_date) - scan_mode = "intraday" if intraday else "post_market" - logger.info(f"=== 筛选模式: {'盘中实时' if intraday else '盘后'} ===") + scan_mode = "realtime_today" if intraday else "post_market" + logger.info(f"=== 筛选模式: {'今日实时' if intraday else '历史收盘'} ===") # ── 市场温度 ── logger.info("=== 市场温度计 ===") @@ -65,12 +71,14 @@ async def run_screening(trade_date: str = None) -> dict: # ── Step 1: 板块定位 ── logger.info("=== Step 1: 板块定位 ===") - all_sectors = scan_hot_sectors(trade_date) + all_sectors = await get_today_realtime_sector_board(limit=30) if intraday else [] + if not all_sectors: + all_sectors = scan_hot_sectors(trade_date) # 前置过滤:只保留有资金流入 + 非末期的板块 hot_sectors = [ s for s in all_sectors - if s.capital_inflow > 0 and s.stage not in ("end",) + if (s.capital_inflow > 0 or s.is_realtime) and s.stage not in ("end",) ][:settings.top_sector_count] if not hot_sectors: @@ -81,8 +89,8 @@ async def run_screening(trade_date: str = None) -> dict: logger.info(f" 目标板块: {s.sector_name} 涨幅{s.pct_change}% 资金{s.capital_inflow:.0f}万 " f"涨停{s.limit_up_count} 阶段={s.stage}") - # 盘中用实时行情更新板块涨幅和涨停数 - if intraday: + # 如果板块来自 Tushare 快照,盘中/盘后用实时行情更新板块涨幅和广度 + if intraday and hot_sectors and not hot_sectors[0].is_realtime: hot_sectors = await intraday_sector_scan(hot_sectors) strategy_profile = await select_strategy_profile(market_temp, hot_sectors, intraday) @@ -361,6 +369,15 @@ async def _build_candidate_pool( intraday_candidates = [] _merge_candidate_batch(merged, intraday_candidates, route="intraday_active") + try: + realtime_candidates = await intraday_active_market_recall(limit=settings.candidate_pool_limit) + except Exception as e: + logger.warning(f"实时全市场召回失败: {e}") + realtime_candidates = [] + _merge_candidate_batch(merged, realtime_candidates, route="realtime_market") + else: + realtime_candidates = [] + candidates = list(merged.values()) candidates.sort(key=lambda item: ( item.get("recall_score", 0), @@ -372,7 +389,7 @@ async def _build_candidate_pool( logger.info( f"Step 2 多路召回完成: sector={len(sector_candidates)} " f"trend={len(trend_candidates)} " - f"{'intraday=' + str(len(intraday_candidates)) if intraday else ''} " + f"{'intraday=' + str(len(intraday_candidates)) + ' realtime=' + str(len(realtime_candidates)) if intraday else ''} " f"→ merged={len(top)}" ) return top diff --git a/backend/app/llm/strategy_board.py b/backend/app/llm/strategy_board.py index 532cf6e2..2349d9c6 100644 --- a/backend/app/llm/strategy_board.py +++ b/backend/app/llm/strategy_board.py @@ -7,6 +7,7 @@ import logging from app.analysis.sector_realtime import enrich_sectors_with_realtime +from app.analysis.sector_realtime import get_today_realtime_sector_board from app.config import settings, should_prefer_realtime_today, today_trade_date from app.data.models import ( MarketTemperature, @@ -61,7 +62,12 @@ async def build_strategy_board(include_llm: bool = False) -> dict: market_temp = latest.get("market_temp") recommendations = latest.get("recommendations", []) sectors = await get_latest_sectors() - sectors = await enrich_sectors_with_realtime(sectors) + snapshot_trade_date = sectors[0].trade_date if sectors else "" + if should_prefer_realtime_today(snapshot_trade_date) or snapshot_trade_date != today_trade_date(): + realtime_sectors = await get_today_realtime_sector_board(limit=20) + sectors = realtime_sectors or await enrich_sectors_with_realtime(sectors) + else: + sectors = await enrich_sectors_with_realtime(sectors) performance = await get_performance_stats() from app.llm.strategy_iteration import build_strategy_iteration_report iteration_report = await build_strategy_iteration_report(limit=50, include_llm=include_llm) diff --git a/backend/astock.db b/backend/astock.db index e72a35bc5760ffb1ad985a7993f7639314ebed93..7dcbaa3fcb183dfb77d83fccf01f6760e41c0f95 100644 GIT binary patch delta 340 zcmW-VNiRcD0ELw*YIx?EUh`B^t$IAVu+Y7gg@{yoiAdjT?rSQwaaf#{DHiPp3&Fy| z5Mo&QD;8eUpP`YHFZsTMp9dF;JiC(~mliG2qOacMqIPhoyDCe3ZHFtXOEPAs5|$}5 zQYlOQeqQFYUxl|D=jk|q_3d2coX6{Y?md5c;hdioPCuNptaJNPc)K&i+OkrnCFa(J zX~m@(OUR-qVM~?%kEF8FJ8>EJi{fiqDY0u>d*+rMNh}!ifr*q6^b6Zksg2D{WGmzs z$=J5suy^)lBp4`eb}}$C84QVO-I$p-g5fD$59^`u5eOGiN|X_9qMYy$6+|UbMRIjXfCmM)GqKRlGT8LJnjc6x2h)$x5=q7rIUcyHRqL1h&28cmoh!`eDh*4tf H9^-*OtX+fZ delta 192 zcmWN_NlpS$07cPXP|^!Uj{?#Z{fGrHb~%P6+%UL_E&zAnz!3>ES7YKv{4aTTaQOXB zh`*)hQpiHwy>>peuCIsBJcPpgBIY`c{4)w6`RK;{HKV;%qczeCl1M>{lFEaWq%0Mw zN=@q0kVk1sOEPIoN4nCJz6@k2BYBc%8Oubb@**>t%R-it%SzU=k*(}x|Ha|>4~p14 AssI20 diff --git a/frontend/.next/server/app-paths-manifest.json b/frontend/.next/server/app-paths-manifest.json index a923b582..8bd5c2dc 100644 --- a/frontend/.next/server/app-paths-manifest.json +++ b/frontend/.next/server/app-paths-manifest.json @@ -1,7 +1,7 @@ { "/(auth)/dashboard/page": "app/(auth)/dashboard/page.js", + "/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/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/app/(auth)/dashboard/page.tsx b/frontend/src/app/(auth)/dashboard/page.tsx index 548bc231..a81b9d40 100644 --- a/frontend/src/app/(auth)/dashboard/page.tsx +++ b/frontend/src/app/(auth)/dashboard/page.tsx @@ -70,7 +70,9 @@ export default function DashboardPage() { useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => { clearScanTimeout(); if (msg.type === "scan_update") { - const modeLabel = msg.scan_mode === "intraday" ? "盘中实时" : "盘后"; + const modeLabel = msg.scan_mode === "realtime_today" || msg.scan_mode === "intraday" + ? "今日实时" + : "历史收盘"; setRefreshResult(`${modeLabel}扫描完成,发现 ${msg.count ?? 0} 只股票`); setRefreshing(false); loadData(); diff --git a/frontend/src/app/(auth)/sectors/page.tsx b/frontend/src/app/(auth)/sectors/page.tsx index 15cdc91c..b5442780 100644 --- a/frontend/src/app/(auth)/sectors/page.tsx +++ b/frontend/src/app/(auth)/sectors/page.tsx @@ -423,8 +423,13 @@ export default function SectorsPage() {

板块主线

判断当前主线、板块阶段、资金持续性和领涨股强度 - {hasRealtime && · 盘中实时优先} + {hasRealtime && · 今日实时优先}

+ {hasRealtime && dataMode === "realtime_today" && ( +

+ 当前使用东方财富今日板块榜,涨幅、成交额、上涨/下跌家数与领涨股均为今日实时/收盘快照。 +

+ )} {hasRealtime && dataMode === "realtime_overlay" && (

盘中模式下,涨幅、成交额、上涨/下跌家数与领涨股为实时覆盖;阶段、资金连续性等结构字段仍基于 diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index af7fe275..27fd29d2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -197,7 +197,7 @@ export interface SectorData { realtime_up_count?: number | null; realtime_down_count?: number | null; is_realtime?: boolean; - data_mode?: "realtime_overlay" | "daily_snapshot"; + data_mode?: "realtime_today" | "realtime_overlay" | "daily_snapshot"; structure_trade_date?: string; }