Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
4539 lines (4208 loc) · 308 KB
/
Copy pathapp.py
File metadata and controls
4539 lines (4208 loc) · 308 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
Fin Synagent - 基于多智能体协同的智能投顾 Demo (v2)
UI 全面升级 + 星火大模型模拟引擎
还原自《基于多智能体协同的智能投顾设计》/《AIGC》/《创作思路说明》
"""
import os
import re
import json
import time
import datetime
import random
import numpy as np
import pandas as pd
import streamlit as st
from streamlit_option_menu import option_menu
# 技能内置的可运行 Python 脚本目录(随 Demo 一同部署,页面直接读取真实文件内容)
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "scripts")
# 技能内置的 references 知识文档目录(随 Demo 一同部署,页面可展开渲染并下载)
REFERENCES_DIR = os.path.join(os.path.dirname(__file__), "references")
# 知识库加载/检索/诊断已抽离到独立模块 kb.py(避免 LangGraph 依赖注入在云端失效导致静默空结果)
from kb import (
KB, KB_LOAD_ERROR, KB_DATA_PATH,
get_rag_hits, kb_unload_reason, kb_unavailable_message, kb_status_info,
)
import collections
import statistics
# 构建指纹:运行时读取 git 短哈希,部署后可在页脚核对线上版本是否与本地一致
def _app_build():
try:
import subprocess
_h = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=os.path.dirname(os.path.abspath(__file__)),
capture_output=True, text=True, timeout=5,
)
if _h.returncode == 0 and _h.stdout.strip():
return _h.stdout.strip()
except Exception:
pass
return "unknown"
APP_BUILD = _app_build()
def compute_live_rag_metrics(retrieval, retrieval_entities=None):
"""从 kb_data.json 中真实召回样本(bge 模型实跑输出)实时统计可观测指标:
Top-5 来源覆盖数、相似度均值/分布、相似度直方图。路由质量类(Recall/MRR/NDCG)
需全库 qrels,直接沿用 retrieval_eval 中 build_retrieval_eval.py 的实跑真值。"""
hist_bins = [(0.30, 0.35), (0.35, 0.40), (0.40, 0.45), (0.45, 0.50),
(0.50, 0.55), (0.55, 0.60), (0.60, 1.01)]
hist_counts = collections.Counter()
all_cov, all_sim = [], []
for ind, qmap in retrieval.items():
for q, hits in qmap.items():
top5 = hits[:5]
all_cov.append(len({h.get("source", "") for h in top5}))
for h in top5:
s = float(h.get("score", 0) or 0)
all_sim.append(s)
for b0, b1 in hist_bins:
if b0 <= s < b1:
hist_counts[(b0, b1)] += 1
live = {
"source_coverage_top5": round(statistics.mean(all_cov), 3) if all_cov else 0.0,
"sim_mean_top5": round(statistics.mean(all_sim), 4) if all_sim else 0.0,
"histogram": [[f"{b0:.2f}-{b1:.2f}" if b1 <= 1.0 else f"{b0:.2f}+", hist_counts[(b0, b1)]]
for b0, b1 in hist_bins],
}
if all_sim:
ss = sorted(all_sim)
n = len(ss)
live["sim_median_top5"] = round(statistics.median(ss), 4)
live["sim_p10_top5"] = round(ss[max(0, n // 10 - 1)], 4)
live["sim_p90_top5"] = round(ss[min(n - 1, n * 9 // 10)], 4)
return live, {}
st.set_page_config(
page_title="Fin Synagent · 多智能体协同智能投顾",
page_icon="🚩",
layout="wide",
initial_sidebar_state="expanded",
)
# ============================================================== 设计系统
NAVY = "#0A1F44"
NAVY2 = "#16305E"
ROYAL = "#2B4C9B"
BLUE = "#4A6FD4"
GOLD = "#C9A227"
GOLD2 = "#E8C766"
GOLD_L = "#F6ECD0"
RED = "#E54545" # 涨(A股规范)
GREEN = "#1E9E6A" # 跌
INK = "#22304F"
MUTE = "#6B7699"
BG = "#F4F6FB"
st.markdown(f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&family=Noto+Serif+SC:wght@600;900&display=swap');
html, body, [class*="css"] {{ font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif; }}
.stApp {{ background: {BG}; }}
.block-container {{ padding-top: 1.6rem; max-width: 1180px; }}
h1,h2,h3 {{ font-family: "Noto Sans SC", sans-serif; }}
/* ---------- HERO ---------- */
.hero {{
position: relative; overflow: hidden;
background: linear-gradient(125deg, #081733 0%, {NAVY} 32%, {NAVY2} 62%, #2b4c9b 100%);
border-radius: 24px; padding: 58px 52px 50px 52px; color: #fff;
box-shadow: 0 20px 50px rgba(10,31,68,.35); margin-bottom: 30px;
}}
.hero::before {{
content:""; position:absolute; width:520px; height:520px; right:-140px; top:-190px;
background: radial-gradient(circle, rgba(201,162,39,.42) 0%, rgba(201,162,39,0) 65%);
animation: float1 9s ease-in-out infinite;
}}
.hero::after {{
content:""; position:absolute; width:380px; height:380px; left:-120px; bottom:-190px;
background: radial-gradient(circle, rgba(74,111,212,.5) 0%, rgba(74,111,212,0) 65%);
animation: float2 11s ease-in-out infinite;
}}
@keyframes float1 {{ 0%,100%{{transform:translate(0,0)}} 50%{{transform:translate(-26px,20px)}} }}
@keyframes float2 {{ 0%,100%{{transform:translate(0,0)}} 50%{{transform:translate(24px,-16px)}} }}
.hero .kicker {{
font-size:.82rem; letter-spacing:4px; color:{GOLD2}; font-weight:500;
text-transform:uppercase; margin-bottom:14px; position:relative; z-index:1;
}}
.hero h1 {{
font-family:"Noto Serif SC", serif; font-size:3.1rem; font-weight:900;
margin:0 0 10px 0; letter-spacing:2px; position:relative; z-index:1;
background: linear-gradient(90deg,#FFFFFF 30%, {GOLD2} 100%);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
}}
.hero .sub {{ font-size:1.08rem; color:#C6D3F0; max-width:760px; line-height:1.7; position:relative; z-index:1; margin-bottom:22px; }}
.hero .tag {{
display:inline-block; position:relative; z-index:1;
background: rgba(255,255,255,.08); backdrop-filter: blur(6px);
color:{GOLD_L}; border:1px solid rgba(201,162,39,.5);
padding:5px 16px; border-radius:999px; font-size:.82rem; margin:0 8px 6px 0;
}}
.hero-mini {{ padding:34px 40px 30px 40px; margin-bottom:24px; }}
.hero-mini h1 {{ font-size:2rem; }}
/* ---------- 卡片 ---------- */
.card {{
background: rgba(255,255,255,.9); backdrop-filter: blur(8px);
border-radius:18px; padding:24px 24px 20px 24px;
border:1px solid #E4E9F4; box-shadow:0 6px 20px rgba(22,48,94,.06);
height:100%; transition: all .25s ease;
}}
.card:hover {{ transform: translateY(-4px); box-shadow:0 14px 32px rgba(22,48,94,.13); border-color:#C9D5EF; }}
.card .icon {{
width:46px; height:46px; border-radius:13px; display:flex; align-items:center; justify-content:center;
font-size:1.45rem; margin-bottom:12px;
background: linear-gradient(135deg, #EDF1FC, #E2E9FA); border:1px solid #D8E1F5;
}}
.card h4 {{ margin:0 0 8px 0; color:{NAVY}; font-size:1.05rem; font-weight:700; }}
.card p {{ color:{MUTE}; font-size:.9rem; line-height:1.7; margin:0; }}
/* ---------- 小节标题 ---------- */
.sec-title {{
font-family:"Noto Serif SC", serif; font-size:1.5rem; font-weight:900; color:{NAVY};
margin:40px 0 4px 0; display:flex; align-items:center; gap:12px;
}}
.sec-title::before {{
content:""; width:7px; height:26px; border-radius:4px;
background: linear-gradient(180deg, {GOLD}, {GOLD2});
box-shadow:0 2px 8px rgba(201,162,39,.45);
}}
.sec-sub {{ color:{MUTE}; font-size:.9rem; margin:2px 0 20px 19px; }}
/* ---------- 步骤 / 时间线 ---------- */
.step {{
background:#fff; border:1px solid #E4E9F4; border-radius:14px; padding:16px 18px; margin-bottom:12px;
border-left:5px solid {ROYAL}; box-shadow:0 3px 12px rgba(22,48,94,.05);
}}
.step b {{ color:{NAVY}; }}
.tl {{ border-left:3px solid #D9E1F2; padding-left:22px; margin-left:8px; }}
.tl-item {{ margin-bottom:22px; position:relative; }}
.tl-item::before {{
content:""; position:absolute; left:-29px; top:5px; width:14px; height:14px; border-radius:50%;
background: radial-gradient(circle at 35% 35%, {GOLD2}, {GOLD});
border:3px solid #fff; box-shadow:0 0 0 2.5px {GOLD}, 0 3px 8px rgba(201,162,39,.5);
}}
.tl-item b {{ color:{NAVY}; font-size:1rem; }}
.tl-item p {{ color:{MUTE}; font-size:.88rem; margin:3px 0 0 0; line-height:1.6; }}
/* ---------- 智能体 ---------- */
.agent {{
border-radius:16px; padding:16px 12px; border:1px solid #E4E9F4; background:#fff;
text-align:center; transition: all .25s ease; box-shadow:0 3px 10px rgba(22,48,94,.05);
}}
.agent:hover {{ transform: translateY(-3px); }}
.agent .em {{ font-size:1.9rem; }}
.agent .role {{ font-weight:700; font-size:.95rem; color:{NAVY}; margin-top:4px; }}
.agent .desc {{ color:{MUTE}; font-size:.8rem; line-height:1.5; margin-top:2px; }}
.a-leader {{ border-top:4px solid {NAVY2}; }}
.a-expert {{ border-top:4px solid {BLUE}; }}
.a-critic {{ border-top:4px solid {GOLD}; }}
.a-verify {{ border-top:4px solid {GREEN}; }}
.a-sum {{ border-top:4px solid {RED}; }}
/* ---------- 股票卡 ---------- */
.stock-card {{
background: linear-gradient(160deg, #FFFFFF 60%, #F4F7FE 100%);
border-radius:18px; padding:20px 22px; border:1px solid #E4E9F4;
box-shadow:0 6px 18px rgba(22,48,94,.07); margin-bottom:4px;
}}
.stock-name {{ font-size:1.15rem; font-weight:800; color:{NAVY}; }}
.stock-code {{ color:#98A2C0; font-size:.8rem; margin-left:6px; }}
.up {{ color:{RED}; font-weight:800; }}
.down {{ color:{GREEN}; font-weight:800; }}
/* ---------- KPI ---------- */
.kpi {{
position:relative; overflow:hidden;
background: linear-gradient(150deg, #FFFFFF 40%, #EEF2FC 100%);
border:1px solid #E4E9F4; border-radius:18px; padding:22px 14px; text-align:center;
box-shadow:0 5px 16px rgba(22,48,94,.06);
}}
.kpi::after {{ content:""; position:absolute; top:0; left:0; right:0; height:4px;
background: linear-gradient(90deg, {GOLD}, {GOLD2}, {GOLD}); }}
.kpi .v {{ font-family:"Noto Serif SC",serif; font-size:1.9rem; font-weight:900; color:{NAVY}; }}
.kpi .k {{ font-size:.8rem; color:{MUTE}; margin-top:2px; }}
.pill {{
display:inline-block; background:#EDF1FA; color:{NAVY2}; border-radius:8px;
padding:4px 12px; font-size:.78rem; margin:2px 5px 2px 0; border:1px solid #DCE3F2; font-weight:500;
}}
.src {{
background:#F2FAF6; border:1px solid #D5EBDF; border-radius:10px;
padding:9px 13px; font-size:.82rem; color:#2C5A41; margin-bottom:7px;
}}
/* ---------- 星火模型卡 ---------- */
.spark-card {{
border-radius:18px; padding:20px 22px; height:100%; color:#fff;
box-shadow:0 10px 26px rgba(22,48,94,.18); transition: all .25s ease;
border:1px solid rgba(255,255,255,.25);
}}
.spark-card:hover {{ transform: translateY(-4px) scale(1.01); }}
.spark-card h4 {{ color:#fff; margin:8px 0 6px 0; font-size:1.05rem; }}
.spark-card p {{ color:rgba(255,255,255,.85); font-size:.82rem; line-height:1.6; margin:0; }}
.spark-card .badge {{
display:inline-block; background:rgba(255,255,255,.18); border:1px solid rgba(255,255,255,.4);
font-size:.7rem; padding:2px 10px; border-radius:999px;
}}
/* ---------- 流式输出 ---------- */
.gen-box {{
background:#fff; border:1px solid #E4E9F4; border-radius:16px; padding:20px 24px;
box-shadow:0 4px 14px rgba(22,48,94,.06); line-height:1.85; font-size:.95rem; color:{INK};
}}
.cursor {{ display:inline-block; width:9px; height:18px; background:{GOLD}; vertical-align:-3px;
animation: blink .8s steps(1) infinite; border-radius:2px; }}
@keyframes blink {{ 50% {{ opacity:0; }} }}
/* ---------- 侧边栏 ---------- */
section[data-testid="stSidebar"],
div[data-testid="stSidebar"] {{
background: linear-gradient(180deg, #081733 0%, {NAVY} 55%, {NAVY2} 100%) !important;
border-right: 1px solid rgba(201,162,39,.25);
}}
section[data-testid="stSidebar"] > div,
div[data-testid="stSidebar"] > div,
div[data-testid="stSidebarContent"],
div[data-testid="stSidebar"] [data-testid="stSidebarContent"],
div[data-testid="stSidebar"] [data-testid="stSidebarUserContent"] {{
background: transparent !important;
}}
section[data-testid="stSidebar"] *, div[data-testid="stSidebar"] * {{ color:#FFFFFF !important; }}
section[data-testid="stSidebar"] .stCaption, section[data-testid="stSidebar"] small,
div[data-testid="stSidebar"] .stCaption, div[data-testid="stSidebar"] small {{ color:#AFC2E8 !important; }}
section[data-testid="stSidebar"] label, section[data-testid="stSidebar"] label p,
section[data-testid="stSidebar"] [data-testid="stWidgetLabel"],
section[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p {{ color:#FFFFFF !important; }}
/* 白底输入控件保持深色字 */
section[data-testid="stSidebar"] input,
section[data-testid="stSidebar"] [data-baseweb="select"] *,
section[data-testid="stSidebar"] [data-baseweb="select"] input,
section[data-testid="stSidebar"] [data-baseweb="popover"] * {{ color:{INK} !important; }}
section[data-testid="stSidebar"] .stButton > button,
div[data-testid="stSidebar"] .stButton > button {{
background: rgba(255,255,255,.08); border: 1px solid rgba(201,162,39,.45);
color: #FFFFFF !important; border-radius: 11px; transition: all .2s ease; font-weight:500;
white-space: normal; line-height: 1.5; text-align: left; padding: 10px 14px;
}}
section[data-testid="stSidebar"] .stButton > button:hover,
div[data-testid="stSidebar"] .stButton > button:hover {{
background: rgba(201,162,39,.16); border-color: rgba(201,162,39,.7); transform: translateX(2px);
}}
section[data-testid="stSidebar"] .stButton > button[kind="primary"],
div[data-testid="stSidebar"] .stButton > button[kind="primary"] {{
background: linear-gradient(90deg, {GOLD}, {GOLD2}); color: {NAVY} !important;
border: none; font-weight: 700; text-align: center;
}}
.sb-brand {{ text-align:center; padding:10px 0 4px 0; }}
.sb-brand .logo {{ font-family:"Noto Serif SC",serif; font-size:1.55rem; font-weight:900; color:#fff !important; letter-spacing:1px; }}
.sb-brand .logo span {{ color:{GOLD2} !important; }}
.sb-brand .slogan {{ font-size:.72rem; color:#C9D4F2 !important; letter-spacing:2.5px; margin-top:2px; }}
.footer {{
text-align:center; color:#96A0BD; font-size:.8rem; margin-top:56px; padding-top:18px;
border-top:1px solid #E0E6F2;
}}
/* ---------- 对话式聊天:AI 左 · 用户右 ---------- */
[data-testid="stChatMessage"] {{
background: #fff;
border: 1px solid #E3E8F4;
border-radius: 14px;
padding: 12px 16px;
box-shadow: 0 2px 10px rgba(20,40,80,.05);
}}
[data-testid="stChatMessage"]:has([data-testid="chatAvatarIcon-assistant"]) {{
margin-right: 30%; /* AI 回答靠左 */
}}
[data-testid="stChatMessage"]:has([data-testid="chatAvatarIcon-user"]) {{
flex-direction: row-reverse; /* 头像与内容镜像到右侧 */
margin-left: 30%; /* 我的提问靠右 */
background: linear-gradient(135deg, #EEF4FF, #F6FAFF);
border-color: #C9DCF7;
}}
@media (max-width: 720px) {{
[data-testid="stChatMessage"]:has([data-testid="chatAvatarIcon-assistant"]) {{ margin-right: 4%; }}
[data-testid="stChatMessage"]:has([data-testid="chatAvatarIcon-user"]) {{ margin-left: 4%; }}
}}
.chat-welcome {{
background: linear-gradient(135deg, #EEF4FF, #F7FAFF);
border:1px solid #D6E4FB; border-radius:18px;
padding:18px 22px; margin:6px 0 16px 0;
color:{INK}; font-size:.95rem; line-height:1.7;
}}
.chat-welcome b {{ color:{NAVY}; }}
.mode-badge {{ display:inline-block; padding:5px 14px; border-radius:999px; font-size:.82rem; font-weight:700; margin-bottom:12px; letter-spacing:.02em; }}
.mode-real {{ background:rgba(34,197,94,.14); color:#15803d; border:1px solid rgba(34,197,94,.45); }}
.mode-demo {{ background:rgba(245,158,11,.16); color:#b45309; border:1px solid rgba(245,158,11,.45); }}
/* ---------- 专有名词解释 ---------- */
.gloss-grid {{ display:grid; grid-template-columns: repeat(2, 1fr); gap:14px; margin:8px 0 26px 0; }}
.gloss-card {{
background: #fff; border:1px solid #E3E8F4; border-left:4px solid {GOLD};
border-radius:14px; padding:16px 18px; box-shadow: 0 4px 14px rgba(20,40,80,0.05);
}}
.gloss-card .gt {{
font-weight:800; font-size:1.02rem; color:{NAVY}; margin-bottom:6px;
font-family:"Noto Serif SC",serif;
}}
.gloss-card .gd {{ font-size:.92rem; color:#3A4866; line-height:1.65; }}
.gloss-card .gt-star {{ color:{GOLD}; }}
.resume-card {{
background:#fff; border:1px solid #E3E8F4; border-radius:12px; padding:14px 16px;
font-size:.92rem; line-height:1.8; color:#2E3A52; box-shadow:0 2px 10px rgba(20,40,80,.05);
}}
@media (max-width: 720px) {{ .gloss-grid {{ grid-template-columns: 1fr; }} }}
</style>
""", unsafe_allow_html=True)
PLOTLY_BASE = dict(font=dict(family="Noto Sans SC", color=INK), paper_bgcolor="white", plot_bgcolor="white")
# ============================================================== 数据
STOCKS = {
"白酒": [
{"name": "贵州茅台", "code": "600519.SH", "price": 1688.00, "chg": +1.86, "pe": 27.4, "mv": "2.12万亿",
"reason": "高端白酒绝对龙头,品牌壁垒深厚,出厂价与一批价坚挺,直销渠道占比持续提升带来吨价上行;现金流充沛,分红率稳定,弱周期属性在行业调整期具备防御价值。"},
{"name": "五粮液", "code": "000858.SZ", "price": 141.25, "chg": +0.94, "pe": 17.8, "mv": "5483亿",
"reason": "普五批价企稳回升,公司控量挺价策略执行坚决;1618、低度系列放量贡献增量;估值处于近五年低位,安全边际充足,弹性空间大。"},
{"name": "泸州老窖", "code": "000568.SZ", "price": 156.40, "chg": -0.62, "pe": 15.2, "mv": "2302亿",
"reason": "国窖1573稳居高端第三极,腰部产品特曲、窖龄酒复苏明确;费用管控精细化,净利率仍有上行空间;短期受行业去库存扰动,回调即配置机会。"},
{"name": "山西汾酒", "code": "600809.SH", "price": 198.60, "chg": +0.71, "pe": 19.6, "mv": "2431亿",
"reason": "青花汾酒全国化扩张成效显著,长江以南市场增速亮眼;青花20/30放量带动产品结构升级,省外收入占比持续提升;清香型龙头受益消费多元化,成长性居板块前列。"},
{"name": "洋河股份", "code": "002304.SZ", "price": 92.30, "chg": -0.45, "pe": 13.9, "mv": "1390亿",
"reason": "梦之蓝M6+/M3水晶版完成渠道梳理,库存去化接近尾声;海之蓝基本盘稳固,省内深耕+省外恢复;估值处历史低位,静待需求回暖后的业绩弹性释放。"},
],
"红利": [
{"name": "中国神华", "code": "601088.SH", "price": 41.72, "chg": +1.24, "pe": 12.6, "mv": "8288亿",
"reason": "煤电运化一体化产业链,长协煤占比高平滑周期波动;连续多年分红率超 70%,股息率约 5.6%,是红利资产的核心压舱石。"},
{"name": "长江电力", "code": "600900.SH", "price": 27.95, "chg": +0.58, "pe": 21.3, "mv": "6839亿",
"reason": "全球最大水电上市公司,六库联调提升发电效率,来水偏丰叠加电价市场化改革;类债属性突出,分红承诺 70% 以上,确定性极强。"},
{"name": "工商银行", "code": "601398.SH", "price": 6.18, "chg": -0.16, "pe": 6.1, "mv": "2.20万亿",
"reason": "国有大行龙头,资产质量稳健,不良率持续下行;股息率超 5%,险资与被动资金持续增配,受益于中特估与市值管理政策。"},
{"name": "陕西煤业", "code": "601225.SH", "price": 23.10, "chg": +0.52, "pe": 11.4, "mv": "2238亿",
"reason": "陕北优质动力煤田成本低、储量丰,长协占比高保障盈利稳定;高分红承诺(分红率不低于60%)叠加现金流充沛,是红利资产中弹性与防御兼备的品种。"},
{"name": "宁沪高速", "code": "600377.SH", "price": 13.85, "chg": +0.22, "pe": 12.8, "mv": "698亿",
"reason": "长三角核心路产车流量稳健,沪宁高速通行费收入韧性强;持续高分红(股息率超5%),类债属性突出,弱市下防御价值显著,兼具REITs化资产盘活预期。"},
],
"贵金属": [
{"name": "紫金矿业", "code": "601899.SH", "price": 18.64, "chg": +2.31, "pe": 16.9, "mv": "4926亿",
"reason": "铜金双轮驱动,卡莫阿、巨龙铜矿放量进入收获期;美联储降息周期开启利好金价,公司矿产金产量三年复合增速超 20%,量价齐升。"},
{"name": "山东黄金", "code": "600547.SH", "price": 29.87, "chg": +1.05, "pe": 38.2, "mv": "1336亿",
"reason": "纯正黄金标的,金价上行期业绩弹性最大;西岭金矿探矿权注入预期强化资源储备;避险需求与央行购金构成中长期支撑。"},
{"name": "中金黄金", "code": "600489.SH", "price": 14.52, "chg": -0.88, "pe": 22.7, "mv": "703亿",
"reason": "央企背景,纱岭金矿投产在即带来产量跃升;集团资产注入预期明确;短期跟随板块回调,估值修复空间可观。"},
{"name": "赤峰黄金", "code": "600988.SH", "price": 24.63, "chg": +1.42, "pe": 25.4, "mv": "410亿",
"reason": "海外矿山(万象、Sepon)黄金产量进入释放期,黄金资源量持续增长;金价高位运行直接抬升业绩弹性,叠加降本增效,盈利对金价的敏感度在板块内居前。"},
{"name": "银泰黄金", "code": "000975.SZ", "price": 17.05, "chg": -0.63, "pe": 20.8, "mv": "473亿",
"reason": "黑河银泰、玉龙矿业等优质金银矿山贡献高品位资源;虽短期技术面偏弱,但金价中长期上行趋势下,具备资源增储与产量爬坡带来的修复弹性。"},
],
}
INDUSTRY_FEATURE = {
"白酒": {"macro": ["社零餐饮消费回暖", "CPI 温和回升", "流动性合理充裕"],
"industry": ["批价企稳回升", "渠道库存去化至良性", "头部集中度提升"],
"view": "行业处于周期底部右侧,需求弱复苏、供给端主动出清,龙头阿尔法属性凸显。"},
"红利": {"macro": ["十年期国债收益率低位", "市值管理新政催化", "险资配置需求旺盛"],
"industry": ["股息率相对债券利差走阔", "央国企分红意愿提升", "现金流稳定"],
"view": "无风险利率下行环境中,高股息资产配置价值持续凸显,攻守兼备。"},
"贵金属": {"macro": ["美联储降息预期升温", "实际利率下行", "地缘风险抬升避险需求"],
"industry": ["全球央行连续购金", "矿产供给刚性", "金价屡创新高"],
"view": "美元信用体系松动叠加降息周期,黄金中长期牛市格局确立,铜金共振。"},
}
# Screen 候选池(含四维特征与 LLM 综合评分,源自《Fin Synagent (9)》Screen 流程)
CANDIDATES = {
"白酒": [
{"name": "贵州茅台", "code": "600519.SH", "pe": 27.4, "pb": 9.5, "roe": 32.1, "rev": 15.8,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.78, 0.08, 0.14), "score": 91.2},
{"name": "五粮液", "code": "000858.SZ", "pe": 17.8, "pb": 4.1, "roe": 24.5, "rev": 11.2,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.71, 0.11, 0.18), "score": 86.7},
{"name": "泸州老窖", "code": "000568.SZ", "pe": 15.2, "pb": 4.4, "roe": 28.7, "rev": 13.5,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "中波动", "macd": "粘合",
"sent": (0.64, 0.15, 0.21), "score": 84.3},
{"name": "山西汾酒", "code": "600809.SH", "pe": 19.6, "pb": 5.8, "roe": 26.3, "rev": 16.9,
"trend": "横盘整理", "ma": "MA20 < MA60", "vol": "高波动", "macd": "死叉",
"sent": (0.58, 0.19, 0.23), "score": 82.5},
{"name": "洋河股份", "code": "002304.SZ", "pe": 13.9, "pb": 2.9, "roe": 20.4, "rev": 6.7,
"trend": "弱势整理", "ma": "MA20 < MA60", "vol": "中波动", "macd": "死叉",
"sent": (0.52, 0.22, 0.26), "score": 80.1},
{"name": "古井贡酒", "code": "000596.SZ", "pe": 18.3, "pb": 4.0, "roe": 23.1, "rev": 18.6,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.66, 0.13, 0.21), "score": 83.4},
{"name": "今世缘", "code": "603369.SH", "pe": 20.1, "pb": 4.6, "roe": 22.4, "rev": 22.3,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.69, 0.12, 0.19), "score": 84.0},
{"name": "舍得酒业", "code": "600702.SH", "pe": 14.7, "pb": 3.1, "roe": 19.8, "rev": 9.4,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "高波动", "macd": "粘合",
"sent": (0.55, 0.18, 0.27), "score": 79.6},
{"name": "迎驾贡酒", "code": "603198.SH", "pe": 16.2, "pb": 3.6, "roe": 21.0, "rev": 19.2,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.63, 0.16, 0.21), "score": 82.1},
],
"红利": [
{"name": "中国神华", "code": "601088.SH", "pe": 12.6, "pb": 1.9, "roe": 15.2, "rev": 3.1,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.74, 0.09, 0.17), "score": 90.5},
{"name": "长江电力", "code": "600900.SH", "pe": 21.3, "pb": 3.1, "roe": 14.8, "rev": 8.4,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.72, 0.10, 0.18), "score": 88.9},
{"name": "工商银行", "code": "601398.SH", "pe": 6.1, "pb": 0.7, "roe": 10.6, "rev": 1.2,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.66, 0.12, 0.22), "score": 85.4},
{"name": "陕西煤业", "code": "601225.SH", "pe": 11.4, "pb": 2.3, "roe": 20.1, "rev": -2.3,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "中波动", "macd": "粘合",
"sent": (0.59, 0.17, 0.24), "score": 83.8},
{"name": "宁沪高速", "code": "600377.SH", "pe": 12.8, "pb": 1.6, "roe": 12.4, "rev": 5.6,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "低波动", "macd": "粘合",
"sent": (0.55, 0.18, 0.27), "score": 81.2},
{"name": "大秦铁路", "code": "601006.SH", "pe": 8.9, "pb": 0.9, "roe": 9.8, "rev": 2.4,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.62, 0.15, 0.23), "score": 82.7},
{"name": "华能水电", "code": "600025.SH", "pe": 19.8, "pb": 2.7, "roe": 13.6, "rev": 7.1,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.64, 0.14, 0.22), "score": 83.0},
{"name": "中国移动", "code": "600941.SH", "pe": 17.2, "pb": 1.8, "roe": 10.2, "rev": 6.3,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "低波动", "macd": "金叉",
"sent": (0.61, 0.15, 0.24), "score": 84.2},
{"name": "中国海油", "code": "600938.SH", "pe": 9.6, "pb": 1.7, "roe": 18.9, "rev": 4.7,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "低波动", "macd": "粘合",
"sent": (0.58, 0.16, 0.26), "score": 83.5},
],
"贵金属": [
{"name": "紫金矿业", "code": "601899.SH", "pe": 16.9, "pb": 3.8, "roe": 22.6, "rev": 18.3,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.81, 0.07, 0.12), "score": 91.8},
{"name": "山东黄金", "code": "600547.SH", "pe": 38.2, "pb": 5.1, "roe": 13.4, "rev": 21.7,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.75, 0.10, 0.15), "score": 87.3},
{"name": "中金黄金", "code": "600489.SH", "pe": 22.7, "pb": 2.6, "roe": 11.2, "rev": 12.5,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.68, 0.13, 0.19), "score": 84.9},
{"name": "赤峰黄金", "code": "600988.SH", "pe": 25.4, "pb": 3.2, "roe": 12.8, "rev": 15.1,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "高波动", "macd": "粘合",
"sent": (0.61, 0.16, 0.23), "score": 82.6},
{"name": "银泰黄金", "code": "000975.SZ", "pe": 20.8, "pb": 2.4, "roe": 11.6, "rev": 9.8,
"trend": "弱势整理", "ma": "MA20 < MA60", "vol": "中波动", "macd": "死叉",
"sent": (0.54, 0.20, 0.26), "score": 80.4},
{"name": "湖南黄金", "code": "002155.SZ", "pe": 24.1, "pb": 2.9, "roe": 12.1, "rev": 14.2,
"trend": "震荡向上", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.66, 0.14, 0.20), "score": 83.3},
{"name": "中金岭南", "code": "000060.SZ", "pe": 13.8, "pb": 1.4, "roe": 9.7, "rev": 8.9,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "中波动", "macd": "粘合",
"sent": (0.57, 0.17, 0.26), "score": 79.9},
{"name": "铜陵有色", "code": "000630.SZ", "pe": 15.2, "pb": 1.3, "roe": 8.9, "rev": 10.4,
"trend": "横盘整理", "ma": "MA20 ≈ MA60", "vol": "高波动", "macd": "粘合",
"sent": (0.56, 0.18, 0.26), "score": 78.8},
{"name": "洛阳钼业", "code": "603993.SH", "pe": 16.1, "pb": 2.2, "roe": 15.4, "rev": 19.8,
"trend": "上升趋势", "ma": "MA20 > MA60", "vol": "中波动", "macd": "金叉",
"sent": (0.70, 0.12, 0.18), "score": 85.6},
],
}
# RAG 模拟检索片段(源自文档:券商研报 / 协会报告 / 财报)
RAG_CHUNKS = {
"白酒": [
("券商白酒深度研报:库存周期与批价跟踪", 0.923, "渠道库存去化至 1.5-2 个月良性区间,飞天批价站稳 2200 元上方……"),
("中酒协行业报告:二季度终端动销调研", 0.887, "宴席与商务场景修复明显,次高端以下价位带动销环比改善……"),
("头部酒企财报:茅台 / 五粮液 2026Q2", 0.851, "直销占比提升带动吨价上行,分红率维持高位,现金流充沛……"),
],
"红利": [
("券商红利策略专题:股息率与利率利差", 0.917, "中证红利股息率约 5.2%,与 10Y 国债利差处于历史高位……"),
("央国企分红政策汇编:市值管理指引", 0.879, "鼓励提高分红频次与比例,多家央企承诺分红率 70% 以上……"),
("公用事业 / 煤炭年报:现金流与分红覆盖", 0.842, "经营现金流对分红覆盖率 1.5 倍以上,长协机制平滑周期……"),
],
"贵金属": [
("券商贵金属中期策略:金铜共振", 0.928, "美联储降息周期开启,实际利率下行,金价中枢上移……"),
("世界黄金协会:全球央行购金季报", 0.895, "全球央行连续 30 个月净购金,新兴市场央行增持显著……"),
("头部矿企财报:紫金 / 山金产量指引", 0.858, "矿产金产量三年复合增速超 20%,资源储备持续扩张……"),
],
"default": [
("券商 A 股中期策略:哑铃配置", 0.876, "高股息防御 + 景气成长进攻,底部区域分批布局……"),
("货币政策执行报告(季度)", 0.842, "保持流动性合理充裕,引导实体融资成本下行……"),
("资本市场改革文件汇编", 0.815, "市值管理、分红回购新政持续落地,夯实市场底部……"),
],
}
CONSULT_SCRIPT = {
"白酒": {
"subtasks": ["行业景气度与终端动销分析", "竞争格局与头部酒企策略研判", "估值水平与投资风险评估"],
"experts": ["白酒行业专家", "宏观消费分析师"],
"expert_answer": "根据知识库 2026 年最新研报:\n\n**1. 景气度**:二季度以来白酒终端动销环比改善,宴席与商务场景修复明显,渠道反馈库存已去化至 1.5-2 个月的良性区间。高端酒批价率先企稳,飞天茅台一批价维持在 2200 元上方。\n\n**2. 竞争格局**:行业进入存量竞争阶段,市场份额加速向头部集中。CR5 收入占比已超 45%,区域酒企分化加剧,具备品牌力与渠道掌控力的龙头将持续收割份额。\n\n**3. 估值与风险**:板块当前 PE-TTM 约 18 倍,处于近十年 10% 分位以下,配置价值凸显。风险点在于需求复苏斜率不及预期与批价波动。",
"critic": "回答整体扎实,但有两点需要补充:① 需区分高端、次高端与区域酒的不同逻辑,不能一概而论;② 应给出更明确的短期/中长期持有建议,并引用具体批价数据佐证。",
"expert_revise": "已按评论家意见修正:高端酒(茅台、五粮液)确定性最强,适合中长期持有;次高端弹性大但需等待动销验证,建议短期波段操作。补充数据:普五批价已由 920 元回升至 960 元,国窖 1573 批价稳定在 880 元左右。",
"verify": ["《白酒行业 2026 年中期策略:底部蓄势,龙头先行》· 中信证券研究部 · 2026-06",
"《酒类流通协会:二季度渠道库存调研报告》· 2026-07",
"新浪财经 · 飞天茅台批价日报 · 2026-07-27"],
"summary": "综合来看,白酒行业正处于周期底部右侧:库存良性、批价企稳、估值处于历史低位。高端龙头具备确定的中长期配置价值,建议以茅台、五粮液为核心底仓,次高端择机波段参与;中长期持有为主,短期关注中秋备货催化。",
},
"红利": {
"subtasks": ["红利产业链上下游结构拆解", "高股息资产的定价逻辑", "细分板块股息率与持续性比较"],
"experts": ["红利策略专家", "资产配置分析师"],
"expert_answer": "按产业上下游逻辑拆解红利行业投资逻辑:\n\n**上游(资源类)**:煤炭、油气、有色等资源品企业资本开支高峰已过,自由现金流充沛,长协机制平滑周期波动,是红利资产的「现金奶牛」,如中国神华、陕西煤业。\n\n**中游(公用事业与交运)**:水电、核电、高速公路具备特许经营属性,需求刚性、现金流可预测性强,长江电力、宁沪高速为代表,类债属性突出。\n\n**下游(金融与消费)**:国有大行不良率下行、分红比例稳定在 30% 以上;白电、纺服龙头分红率持续提升。\n\n定价核心在于:股息率与无风险利率的利差。当前 10Y 国债收益率约 1.7%,而中证红利指数股息率约 5.2%,利差处于历史高位,配置价值显著。",
"critic": "逻辑框架清晰,但建议补充:① 红利资产的拥挤度与潜在回撤风险;② 不同细分板块分红可持续性的量化依据(分红率、经营现金流覆盖率)。",
"expert_revise": "补充如下:拥挤度方面,红利指数换手率仍低于历史中位数,尚未过热;风险方面,若经济复苏超预期导致利率快速上行,红利资产将阶段性跑输。可持续性上,煤炭/水电板块经营现金流对分红的覆盖率均在 1.5 倍以上。",
"verify": ["《红利策略深度:从哑铃一端到压舱石》· 华泰证券 · 2026-05",
"中证指数公司 · 中证红利指数股息率月报 · 2026-06",
"Wind 资讯 · 上市公司 2025 年度分红实施公告汇总"],
"summary": "红利行业的投资逻辑可沿「上游资源现金流—中游特许经营—下游金融消费」的产业链条展开,核心驱动是股息率与无风险利率的高利差。建议以中国神华、长江电力等现金流确定性强的标的为底仓,长期持有获取股息与估值修复的双重收益,同时关注利率快速上行带来的阶段性波动风险。",
},
"贵金属": {
"subtasks": ["全球宏观与货币政策环境研判", "黄金供需格局与价格驱动分析", "贵金属板块标的选择与风险提示"],
"experts": ["贵金属行业专家", "宏观策略分析师"],
"expert_answer": "基于知识库最新研报与国际市场数据:\n\n**1. 宏观环境**:美联储 6 月议息会议释放明确鸽派信号,市场预期年内降息两次;美国实际利率下行是金价最核心的驱动变量。叠加地缘政治风险反复,避险需求持续。\n\n**2. 供需格局**:全球央行已连续 30 个月净购金,中国、波兰、土耳其央行增持明显;矿产金供给刚性,回收金弹性有限。金价突破 2400 美元/盎司后站稳,沪金主力合约创出新高。\n\n**3. 投资结论**:贵金属板块在当前市场中具备明确的配置价值,黄金股相对金价存在杠杆弹性,建议关注资源储量扩张确定性强的龙头。风险在于降息节奏不及预期引发的阶段性回调。",
"critic": "回答数据翔实,但需补充:① 白银等工业属性贵金属与黄金的差异;② 短期持有与长期持有的具体建议;③ 金价与黄金股估值的相对位置。",
"expert_revise": "补充说明:白银兼具工业属性,光伏需求支撑其弹性大于黄金,但波动也更大;黄金股当前估值隐含金价约 2200 美元,低于现价,存在修复空间。建议:长期持有黄金龙头,短期回调即加仓窗口。",
"verify": ["《贵金属行业 2026 年中期策略:金铜共振》· 国泰君安有色团队 · 2026-06",
"世界黄金协会 · 全球央行购金季度报告 · 2026Q2",
"COMEX 黄金期货与沪金主力合约行情数据 · 2026-07-27"],
"summary": "贵金属行业当前值得投资:美联储降息周期、央行持续购金与避险需求构成三重驱动,黄金中长期牛市格局确立。建议以紫金矿业、山东黄金等资源扩张确定性强的龙头为主,长期持有为主、短期回调加仓;关注降息节奏变化带来的波动风险。",
},
"default": {
"subtasks": ["宏观经济环境与政策面分析", "相关行业景气度与趋势研判", "投资策略与风险管理建议"],
"experts": ["宏观策略分析师", "金融知识库专家"],
"expert_answer": "基于金融行业知识库与联网检索信息:\n\n**1. 宏观层面**:当前国内货币政策保持合理充裕,财政发力稳增长,资本市场改革深化(市值管理、分红回购新政),为权益市场提供底部支撑。\n\n**2. 行业层面**:建议沿「高股息防御 + 景气成长进攻」的哑铃思路配置:一端是红利、公用事业等现金流资产;另一端是 AI、高端制造等政策支持方向。\n\n**3. 策略层面**:个人投资者应结合自身风险偏好与资金期限,避免追涨杀跌,采用定投或分批建仓方式平滑波动。",
"critic": "回答框架完整,但个性化不足:建议结合用户的风险承受能力与投资期限给出更具体的配置比例建议。",
"expert_revise": "已修正:稳健型投资者可按「60% 红利固收 + 30% 宽基指数 + 10% 行业主题」配置;积极型可提高行业主题至 30%,并设置 8% 止损纪律。",
"verify": ["《2026 年中期 A 股策略展望》· 中金公司研究部 · 2026-06",
"中国人民银行 · 2026 年第二季度货币政策执行报告",
"沪深交易所 · 上市公司市值管理指引解读"],
"summary": "当前市场环境下,建议采取哑铃型配置策略:以红利资产打底获取确定性收益,以政策支持的成长方向博取弹性。请结合您的风险偏好与资金期限执行,并坚持纪律化投资。如需针对白酒、红利、贵金属等具体行业的深度分析,欢迎继续追问。",
},
}
GUIDE_PROMPTS = [
"根据最近白酒行业的行情,你能得出什么结果?",
"请你按照产业上下游的逻辑说明红利行业的投资逻辑?",
"贵金属行业在目前的股票市场中值得投资吗?",
"【追问】现在市场看好这个行业吗?建议短期持有还是长期持有?",
]
# ============================================================== Screen 四维分析(带实例)
DIM_TITLE = {"fundamental": "基本面", "technical": "技术面", "sentiment": "情绪面", "industry": "行业面"}
SCREEN_ANALYSIS = {
"白酒": {
"fundamental": {
"summary": "白酒板块基本面高度分化:高端龙头盈利质量最优,区域酒增长承压。核心看 ROE 与营收增速的匹配度——低 PE 未必便宜、高 PE 也未必贵,必须结合成长性判断。",
"examples": [
{"title": "贵州茅台", "tag": "高 ROE 支撑高估值", "point": "PE 27.4、PB 9.5、ROE 32.1%、营收 +15.8%。实例:ROE 超 30% 领跑全板块,自由现金流充沛、分红率稳定,高估值由高确定性盈利质量支撑,仍是溢价最高的标的。"},
{"title": "五粮液", "tag": "估值处历史低位", "point": "PE 17.8、ROE 24.5%、营收 +11.2%。实例:估值处于近五年低位,批价由 920 元回升至 960 元带动盈利预期修复,性价比突出。"},
{"title": "洋河股份", "tag": "低估值≠便宜", "point": "PE 13.9 看似最低,但 ROE 20.4%、营收仅 +6.7%。实例:低估值背后是增长乏力,印证「便宜有便宜的道理」,基本面相对最弱。"},
],
},
"technical": {
"summary": "均线系统与 MACD 是判断白酒短期动能的关键:多头排列(MA20>MA60 + 金叉)趋势最健康,死叉/空头排列则短期承压,粘合形态预示变盘。",
"examples": [
{"title": "贵州茅台", "tag": "多头排列最健康", "point": "上升趋势、MA20>MA60、MACD 金叉、低波动。实例:均线多头叠加金叉,技术形态最健康,适合顺势持有。"},
{"title": "山西汾酒", "tag": "空头排列短期承压", "point": "横盘整理、MA20<MA60、MACD 死叉、高波动。实例:均线空头且死叉,短期动能最弱,需等待放量突破信号。"},
{"title": "泸州老窖", "tag": "方向选择期", "point": "横盘、MA20≈MA60、MACD 粘合。实例:粘合形态预示变盘,宜观望或分批,不宜追涨。"},
],
},
"sentiment": {
"summary": "FinBERT 对新闻/研报的情感三分类显示:龙头正面情绪占比最高、负面最低;增长存疑的标的负面占比明显抬升。",
"examples": [
{"title": "贵州茅台", "tag": "正面 0.78 居首", "point": "正面 0.78 / 负面 0.08 / 中性 0.14。实例:近八成舆情正面、负面不足一成,市场对高端确定性共识最强。"},
{"title": "五粮液", "tag": "正面 0.71", "point": "批价回升提振情绪,正面占比居前,负面仅 0.11。"},
{"title": "洋河股份", "tag": "负面 0.22 最高", "point": "正面 0.52 / 负面 0.22 / 中性 0.26。实例:负面占比超两成、居白酒首位,反映市场对增长放缓的担忧,情绪面最弱。"},
],
},
"industry": {
"summary": "行业处于周期底部右侧:库存去化至良性、批价企稳、集中度提升,龙头阿尔法属性凸显。",
"examples": [
{"title": "宏观驱动", "tag": "消费场景修复", "point": "社零餐饮回暖、CPI 温和回升、流动性合理充裕,共同支撑白酒消费场景修复。"},
{"title": "行业景气", "tag": "库存去化+批价企稳", "point": "渠道库存去化至 1.5-2 个月良性区间,飞天批价站稳 2200 元上方,头部 CR5 超 45%。"},
{"title": "配置观点", "tag": "龙头底仓", "point": "实例:以茅台/五粮液为核心底仓,次高端(汾酒/老窖)择机波段参与。"},
],
},
},
"红利": {
"fundamental": {
"summary": "红利资产的核心定价锚是「股息率 − 无风险利率」的利差与分红可持续性(分红率、经营现金流覆盖率)。低 PE/PB + 高 ROE + 稳定现金流是优选标准。",
"examples": [
{"title": "中国神华", "tag": "现金奶牛", "point": "PE 12.6、PB 1.9、ROE 15.2%、营收 +3.1%。实例:煤电运化一体化、长协煤占比高平滑周期,分红率超 70%、股息率约 5.6%,现金流对分红覆盖 1.5 倍以上。"},
{"title": "工商银行", "tag": "破净高股息", "point": "PE 6.1、PB 0.7、ROE 10.6%。实例:市净率破净、股息率超 5%,受益中特估与市值管理,险资持续增配。"},
{"title": "陕西煤业", "tag": "周期波动需警惕", "point": "营收 −2.3% 下滑。实例:盈利随煤价波动,但分红仍稳,体现「现金奶牛」属性,需关注周期下行风险。"},
],
},
"technical": {
"summary": "红利多为低波动防御品种,技术信号以趋势延续为主;MA20>MA60 且金叉的标的处于上升通道,窄幅震荡者弹性有限。",
"examples": [
{"title": "中国神华", "tag": "慢牛红利形态", "point": "上升趋势、MA20>MA60、金叉、低波动。实例:稳步抬升,典型慢牛红利形态。"},
{"title": "长江电力", "tag": "技术面最稳健", "point": "上升趋势、金叉、低波动。实例:类债属性、回撤极小,技术面最稳健。"},
{"title": "宁沪高速", "tag": "防御强弹性弱", "point": "横盘、MA20≈MA60、粘合、低波动。实例:窄幅震荡,防御属性强但弹性有限。"},
],
},
"sentiment": {
"summary": "红利标的情绪整体平稳偏暖,负面占比普遍低于 0.2;利率下行预期持续强化其配置逻辑。",
"examples": [
{"title": "中国神华", "tag": "正面 0.74", "point": "高股息共识强,负面仅 0.09(多为煤价波动担忧),为红利板块最低。"},
{"title": "工商银行", "tag": "正面 0.66", "point": "中特估与分红新政提振情绪,负面仅 0.12。"},
{"title": "陕西煤业", "tag": "情绪略弱", "point": "正面 0.59 较低。实例:煤价下行预期使情绪略弱于水电/银行。"},
],
},
"industry": {
"summary": "无风险利率下行 + 市值管理新政 + 险资配置需求,使高股息资产配置价值持续凸显,攻守兼备。",
"examples": [
{"title": "宏观驱动", "tag": "利率低位", "point": "十年期国债收益率约 1.7% 低位,险资配置需求旺盛,红利相对债券利差走阔。"},
{"title": "行业景气", "tag": "利差历史高位", "point": "中证红利股息率约 5.2%,与 10Y 国债利差处历史高位,央国企分红意愿提升。"},
{"title": "配置观点", "tag": "底仓长期持有", "point": "实例:以神华/长电为底仓长期持有,获取股息 + 估值修复双重收益。"},
],
},
},
"贵金属": {
"fundamental": {
"summary": "贵金属股弹性来自「量 × 价」双击:金价/铜价上行叠加矿产产量扩张。需关注 PE 与产量增速的匹配,纯金标的在金价上行期业绩弹性最大。",
"examples": [
{"title": "紫金矿业", "tag": "量价齐升", "point": "PE 16.9、PB 3.8、ROE 22.6%、营收 +18.3%。实例:铜金双轮,卡莫阿/巨龙铜矿放量,矿产金三年复合增速超 20%,量价齐升。"},
{"title": "山东黄金", "tag": "金价弹性最大", "point": "PE 38.2 偏高、ROE 13.4%、营收 +21.7%。实例:纯正黄金标的,金价上行期业绩弹性最大,西岭金矿探矿权注入预期强化资源储备。"},
{"title": "中金黄金", "tag": "产量跃升在望", "point": "PE 22.7、营收 +12.5%。实例:央企背景、纱岭金矿投产在即,产量跃升在望。"},
],
},
"technical": {
"summary": "贵金属受商品价格驱动,趋势性强、波动中高;MA20>MA60 且金叉的标的处于主升段,空头排列者短期宜等企稳。",
"examples": [
{"title": "紫金矿业", "tag": "主升浪最清晰", "point": "上升趋势、MA20>MA60、金叉、中波动。实例:均线多头叠加金叉,主升浪形态最清晰。"},
{"title": "山东黄金", "tag": "随金价走强", "point": "上升趋势、金叉。实例:随金价突破同步走强,趋势与商品共振。"},
{"title": "银泰黄金", "tag": "技术面最弱", "point": "弱势整理、MA20<MA60、死叉。实例:短期空头排列,宜等企稳再介入。"},
],
},
"sentiment": {
"summary": "降息预期与央行购金叙事使贵金属正面情绪高涨,紫金/山金正面占比居前。",
"examples": [
{"title": "紫金矿业", "tag": "正面 0.81 最高", "point": "量价齐升逻辑共识强,负面仅 0.07,为贵金属板块最低。"},
{"title": "山东黄金", "tag": "正面 0.75", "point": "避险 + 降息双驱动,情绪乐观。"},
{"title": "银泰黄金", "tag": "正面 0.54 最低", "point": "技术走弱拖累情绪,负面占比抬升。"},
],
},
"industry": {
"summary": "美联储降息周期 + 全球央行连续购金 + 地缘避险,黄金中长期牛市格局确立,铜金共振。",
"examples": [
{"title": "宏观驱动", "tag": "实际利率下行", "point": "美联储降息预期升温、实际利率下行、地缘风险抬升避险需求。"},
{"title": "行业景气", "tag": "央行连续购金", "point": "全球央行连续 30 个月净购金、矿产供给刚性、金价屡创新高,沪金主力创历史新高。"},
{"title": "配置观点", "tag": "龙头长期持有", "point": "实例:以紫金/山金为龙头长期持有,短期回调即加仓窗口。"},
],
},
},
}
# 情绪面评论实例(FinBERT 标注 · 演示用示例,用于说明情绪面特征来源)
SCREEN_SENTIMENT_SAMPLES = {
"白酒": {
"贵州茅台": [("飞天批价站稳 2200 元上方,渠道库存去化至良性区间", "正面", 0.94),
("直销渠道占比提升带动吨价上行,分红承诺稳定", "正面", 0.90)],
"五粮液": [("普五批价由 920 元回升至 960 元,盈利预期修复", "正面", 0.88),
("次高端需求仍有待动销验证", "中性", 0.55)],
"泸州老窖": [("国窖 1573 稳居高端第三极,腰部产品复苏明确", "正面", 0.76),
("行业去库存短期扰动业绩", "负面", 0.61)],
"山西汾酒": [("青花系列全国化扩张势头强劲", "正面", 0.74),
("青花 20 批价波动引发渠道担忧", "负面", 0.58)],
"洋河股份": [("梦之蓝 M6+ 省内动销平稳", "中性", 0.60),
("增长放缓,市场对其改革成效存疑", "负面", 0.62)],
},
"红利": {
"中国神华": [("分红率超 70%、股息率约 5.6%,现金流覆盖 1.5 倍", "正面", 0.91),
("长协煤占比高平滑周期波动", "正面", 0.85)],
"长江电力": [("六库联调提升发电效率,来水偏丰", "正面", 0.86),
("分红承诺 70% 以上,确定性极强", "正面", 0.88)],
"工商银行": [("股息率超 5%,受益中特估与市值管理", "正面", 0.80),
("净息差承压、信贷需求偏弱", "负面", 0.57)],
"陕西煤业": [("高分红延续,现金奶牛属性突出", "正面", 0.78),
("煤价下行预期压制盈利", "负面", 0.59)],
"宁沪高速": [("车流稳健、类债属性强", "正面", 0.72),
("弹性有限,缺乏成长故事", "中性", 0.52)],
},
"贵金属": {
"紫金矿业": [("卡莫阿、巨龙铜矿放量,矿产金三年复合增速超 20%", "正面", 0.93),
("美联储降息周期利好金价,量价齐升", "正面", 0.90)],
"山东黄金": [("西岭金矿探矿权注入预期强化资源储备", "正面", 0.84),
("金价上行期业绩弹性最大", "正面", 0.82)],
"中金黄金": [("纱岭金矿投产在即,产量跃升", "正面", 0.80),
("短期跟随板块回调", "负面", 0.60)],
"赤峰黄金": [("海外矿山产能释放,量增明确", "正面", 0.76),
("海外运营与汇率风险偏高", "负面", 0.58)],
"银泰黄金": [("玉龙矿业储量优质,成长空间大", "正面", 0.73),
("技术面走弱拖累短期情绪", "负面", 0.61)],
},
}
def render_analysis_block(industry: str, dim: str):
"""在 Screen 各维度 tab 中渲染带实例的叙事分析。"""
block = SCREEN_ANALYSIS.get(industry, {}).get(dim)
if not block:
return
st.markdown(f"**📝 {DIM_TITLE[dim]}分析要点**")
st.write(block["summary"])
exs = block["examples"]
cols = st.columns(len(exs))
for col, ex in zip(cols, exs):
with col:
st.markdown(
f'<div class="card" style="height:100%"><div class="icon">📌</div>'
f'<h4>{ex["title"]}</h4>'
f'<p><b style="color:{GOLD}">{ex.get("tag","实例")}</b><br>{ex["point"]}</p></div>',
unsafe_allow_html=True)
SPARK_MODELS = {
"Spark4.0 Ultra": {"domain": "4.0Ultra", "desc": "星火最强旗舰模型,分析与推理能力卓越,Fin 1.5 起作为专家模型,内生联网搜索。", "grad": "linear-gradient(135deg,#7A4FD0,#4A2C9B)", "badge": "专家模型 · 本项目采用"},
"Spark Max": {"domain": "generalv3.5", "desc": "高性能通用模型,兼顾速度与质量,适合复杂任务拆解与总结。", "grad": "linear-gradient(135deg,#2B4C9B,#16305E)", "badge": "旗舰"},
"Spark Pro": {"domain": "generalv3", "desc": "均衡型模型,支持微调定制,本项目以其为基座完成金融领域微调(lr=8e-5, 5 epochs)。", "grad": "linear-gradient(135deg,#2E7DB2,#1A4E7A)", "badge": "支持微调"},
"Spark Lite": {"domain": "general", "desc": "轻量级免费模型,响应极快,适合高并发轻量问答场景。", "grad": "linear-gradient(135deg,#3E9E8F,#206655)", "badge": "极速免费"},
}
# ============================================================== 星火大模型模拟引擎
def match_script(query: str) -> dict:
for key in ["白酒", "红利", "贵金属"]:
if key in query:
return CONSULT_SCRIPT[key]
return CONSULT_SCRIPT["default"]
def spark_generate(prompt: str, model: str, system: str = "") -> str:
"""模拟星火大模型生成:金融关键词命中知识库,否则模板化组织回答。"""
script = match_script(prompt)
if script is not CONSULT_SCRIPT["default"]:
body = script["expert_answer"] + "\n\n**综合建议**:" + script["summary"]
else:
topic = prompt.strip().rstrip("??。")[:30] or "该问题"
body = (
f"关于「{topic}」,基于星火大模型的金融语料训练与联网检索信息,为您梳理如下:\n\n"
f"**一、背景与现状**:该议题涉及宏观经济环境、行业景气度与市场情绪的多重博弈。"
f"当前国内货币政策保持合理充裕,资本市场改革持续深化,为相关领域提供了稳定的政策底部。\n\n"
f"**二、核心逻辑**:从投资视角看,建议从供需格局、估值分位与资金结构三个维度评估。"
f"历史经验表明,在市场分歧较大、估值处于历史中低分位时进行分批布局,长期胜率更高。\n\n"
f"**三、风险提示**:需警惕外部环境变化、政策节奏不及预期以及流动性边际收紧带来的波动风险。"
f"请结合自身风险承受能力与资金期限审慎决策。\n\n"
f"以上为模拟星火大模型生成的分析框架,如需针对白酒、红利、贵金属行业的深度投顾报告,"
f"可前往 **Consult 智能咨询** 页面体验多智能体协同推理。"
)
if system:
body = f"(已遵循系统设定:{system[:40]})\n\n" + body
if "Lite" in model:
# 轻量模型:精简输出
body = body.split("**二、")[0] if "**二、" in body else body[:400]
return body
def spark_stream(text: str, placeholder, speed: float = 0.012):
"""打字机流式渲染"""
out = ""
step = 3
for i in range(0, len(text), step):
out += text[i:i + step]
placeholder.markdown(f'<div class="gen-box">{out}<span class="cursor"></span></div>', unsafe_allow_html=True)
time.sleep(speed)
placeholder.markdown(f'<div class="gen-box">{out}</div>', unsafe_allow_html=True)
return out
def mock_spark_response(model: str, prompt: str, answer: str, temperature: float, top_k: int, max_tokens: int):
"""模拟讯飞星火 Web API v4.0 的响应帧结构"""
return {
"header": {"code": 0, "message": "Success", "sid": f"cht{random.randint(10**10, 10**11-1)}mock", "status": 2},
"payload": {
"choices": {"status": 2, "seq": 0, "text": [{"role": "assistant", "content": answer[:120] + "……"}]},
"usage": {"text": {"question_tokens": len(prompt) // 2, "prompt_tokens": len(prompt) // 2 + 12,
"completion_tokens": len(answer) // 2, "total_tokens": len(prompt) // 2 + len(answer) // 2 + 12}},
"model": {"domain": SPARK_MODELS[model]["domain"], "temperature": temperature, "top_k": top_k, "max_tokens": max_tokens},
},
}
# ============================================================== DeepSeek 真实大模型接入
# 通过 OpenAI 兼容协议调用 DeepSeek:https://api.deepseek.com ,默认模型 deepseek-chat (V3)
DS_BASE_URL = "https://api.deepseek.com"
DS_MODEL = "deepseek-chat"
DS_FALLBACK_KEY = "" # 不再硬编码真实 Key(公开仓库会泄露)。真实模式仅凭 st.secrets['DEEPSEEK_API_KEY'] 或界面粘贴的 ds_api_key 启用。
def _ds_client():
"""构造 OpenAI 客户端:优先用界面输入的 key(session_state['ds_api_key']),其次 st.secrets['DEEPSEEK_API_KEY'];缺失或异常时返回 None(降级演示模式)。"""
try:
ss_key = (st.session_state.get("ds_api_key", "") or "").strip()
sec_key = (st.secrets.get("DEEPSEEK_API_KEY", "") or "").strip()
except Exception:
ss_key = (st.session_state.get("ds_api_key", "") or "").strip()
sec_key = ""
key = ss_key or sec_key or DS_FALLBACK_KEY
if not key:
return None
try:
from openai import OpenAI
return OpenAI(api_key=key, base_url=DS_BASE_URL)
except Exception:
return None
def render_access_gate():
"""姓名闸门:回答『使用者是谁?』= 狄霄月 才解锁真实模式与 API Key 区域。
返回 _unlocked(bool)。这是软性门禁(非安全机制),仅用于避免公网访客误用主人的 API Key。
"""
if "unlocked" not in st.session_state:
st.session_state["unlocked"] = False
if st.session_state["unlocked"]:
return True
st.info("🔐 演示模式:请输入正确姓名解锁真实模式与 API Key。")
_name = st.text_input("使用者是谁?", key="gate_name", placeholder="输入姓名")
if st.button("解锁", key="gate_btn"):
if _name.strip() == "狄霄月":
st.session_state["unlocked"] = True
st.rerun()
else:
st.error("姓名不正确,仅演示模式可用。")
return False
def _ds_text(system: str, user: str, fallback: str, allow_real: bool = True,
temperature: float = 0.7) -> str:
"""非流式调用 DeepSeek;缺密钥/异常/allow_real=False 时返回 fallback 文本。"""
if not allow_real:
return fallback
client = _ds_client()
if client is None:
return fallback
try:
resp = client.chat.completions.create(
model=DS_MODEL,
messages=[{"role": "system", "content": system},
{"role": "user", "content": user}],
temperature=temperature,
stream=False,
)
return (resp.choices[0].message.content or "").strip() or fallback
except Exception:
return fallback
def _ds_stream_into(ph, system: str, user: str, fallback: str, allow_real: bool = True) -> str:
"""流式调用 DeepSeek,逐块渲染到 ph(打字机效果);缺密钥/异常/allow_real=False 时回退 fallback。"""
if not allow_real:
ph.markdown(f'<div class="gen-box">{fallback}</div>', unsafe_allow_html=True)
return fallback
client = _ds_client()
if client is None:
ph.markdown(f'<div class="gen-box">{fallback}</div>', unsafe_allow_html=True)
return fallback
out, cnt = "", 0
try:
stream = client.chat.completions.create(
model=DS_MODEL,
messages=[{"role": "system", "content": system},
{"role": "user", "content": user}],
temperature=0.7,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
if not delta:
continue
out += delta
cnt += len(delta)
if cnt >= 24: # 每累积约 24 字刷新一次,避免过于频繁重绘
ph.markdown(f'<div class="gen-box">{out}<span class="cursor"></span></div>', unsafe_allow_html=True)
cnt = 0
ph.markdown(f'<div class="gen-box">{out}</div>', unsafe_allow_html=True)
return out or fallback
except Exception:
if out:
ph.markdown(f'<div class="gen-box">{out}</div>', unsafe_allow_html=True)
return out
ph.markdown(f'<div class="gen-box">{fallback}</div>', unsafe_allow_html=True)
return fallback
# ============================================================== 图表
def make_price_series(base: float, days: int = 120, drift: float = 0.0006, vol: float = 0.016):
rng = np.random.default_rng(int(base * 100) % 2**32)
rets = rng.normal(drift, vol, days)
price = base * np.exp(np.cumsum(rets))
price = price / price[-1] * base
idx = pd.date_range(end=pd.Timestamp.today(), periods=days, freq="B")
return pd.DataFrame({"date": idx, "close": np.round(price, 2)})
def price_chart(df: pd.DataFrame, name: str):
import plotly.graph_objects as go
up = df["close"].iloc[-1] >= df["close"].iloc[0]
color = RED if up else GREEN
fig = go.Figure()
fig.add_trace(go.Scatter(x=df["date"], y=df["close"], mode="lines",
line=dict(color=color, width=2.2, shape="spline", smoothing=0.6), name=name,
fill="tozeroy", fillcolor=f"rgba({int(color[1:3],16)},{int(color[3:5],16)},{int(color[5:7],16)},0.09)"))
fig.update_layout(**PLOTLY_BASE, height=270, margin=dict(l=8, r=8, t=34, b=8),
title=dict(text=f"{name} · 近 120 个交易日走势", font=dict(size=13, color=NAVY)),
xaxis=dict(showgrid=False), yaxis=dict(gridcolor="#EEF1F8"), showlegend=False)
return fig
# ============================================================== 实时行情接入
# 数据源:东方财富 push2 实时报价 + push2his 日K。纯标准库 urllib,无需额外依赖;
# 网络受限时所有函数安全回退(返回空 / None),由调用方落到内置演示数据。
import urllib.request as _ur
import urllib.parse as _up
_EM_HEADERS = {"User-Agent": "Mozilla/5.0", "Referer": "https://quote.eastmoney.com/"}
def _em_secid(code: str) -> str:
"""600519.SH -> 1.600519 ; 000858.SZ -> 0.000858"""
mkt = "1" if code.upper().endswith("SH") else "0"
return f"{mkt}.{code.split('.')[0]}"
def _to_float(v):
try:
return float(v)
except Exception:
return None
def _fmt_mv(yuan):
"""东方财富总市值单位为「元」,转换为 亿 / 万亿。"""
if yuan is None:
return "—"
try:
yi = float(yuan) / 1e8
except Exception:
return "—"
if yi >= 10000:
return f"{yi / 10000:.2f}万亿"
return f"{yi:.0f}亿"
def _yh_symbol(code: str) -> str:
"""600519.SH -> 600519.SS ; 000858.SZ -> 000858.SZ(Yahoo 命名)"""
base, mkt = code.split(".")
return base + (".SS" if mkt.upper() == "SH" else ".SZ")
_YH_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
def _http_get_json(url, headers, retries: int = 2, timeout: int = 6):
"""带退避重试的 JSON GET;全部失败则抛出最后一次异常。"""
last = None
for i in range(retries):
try:
req = _ur.Request(url, headers=headers)
with _ur.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
except Exception as e:
last = e