-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathgui_app.py
More file actions
2448 lines (2186 loc) · 111 KB
/
Copy pathgui_app.py
File metadata and controls
2448 lines (2186 loc) · 111 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 _*_
"""
DeltaLab - 期权对冲回测 GUI 应用
基于 tkinter 构建,支持选择不同期权类型、回测方式(模拟/历史数据),
并以图表和表格形式展示回测结果。
"""
import sys
import os
import copy
import platform
import threading
import datetime
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
def _resource_path(*parts: str) -> str:
# PyInstaller 解压到 sys._MEIPASS; 开发态以源文件所在目录为根.
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, *parts)
# ---- 跨平台中文字体设置 ----
_SYSTEM = platform.system()
if _SYSTEM == "Darwin":
_CJK_CANDIDATES = ["PingFang SC", "Heiti SC", "STHeiti", "Arial Unicode MS",
"Hiragino Sans GB", "Songti SC"]
elif _SYSTEM == "Windows":
_CJK_CANDIDATES = ["Microsoft YaHei", "SimHei", "SimSun"]
else:
_CJK_CANDIDATES = ["Noto Sans CJK SC", "WenQuanYi Zen Hei",
"WenQuanYi Micro Hei", "Source Han Sans SC"]
_AVAILABLE_FONTS = {f.name for f in font_manager.fontManager.ttflist}
_CJK_FALLBACK = [f for f in _CJK_CANDIDATES if f in _AVAILABLE_FONTS] + ["DejaVu Sans"]
plt.rcParams['font.sans-serif'] = _CJK_FALLBACK
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False
# 显式绑定一个 CJK 字体文件, 供 matplotlib text() 等接口通过 fontproperties 强制使用,
# 避免某些调用路径回退到不含 CJK 字形的默认字体导致乱码.
_MPL_CJK_FP = None
for _f in font_manager.fontManager.ttflist:
if _f.name in _CJK_CANDIDATES:
_MPL_CJK_FP = font_manager.FontProperties(fname=_f.fname)
break
# Tk/ttk 使用的中文 UI 字体族(取第一个可用的 CJK 字体)
_UI_FONT_FAMILY = _CJK_FALLBACK[0] if _CJK_FALLBACK[0] != "DejaVu Sans" else "TkDefaultFont"
# 等宽字体 (用于摘要/结构文本)
if _SYSTEM == "Windows":
_MONO_CANDIDATES = ["Cascadia Mono", "Consolas", "Courier New"]
elif _SYSTEM == "Darwin":
_MONO_CANDIDATES = ["Menlo", "Monaco", "Courier New"]
else:
_MONO_CANDIDATES = ["DejaVu Sans Mono", "Liberation Mono", "Courier New"]
_MONO_FONT_FAMILY = next((f for f in _MONO_CANDIDATES if f in _AVAILABLE_FONTS), "Courier")
# ---- 统一视觉调色板 (现代扁平风格, 蓝灰系) ----
PALETTE = {
"bg": "#F3F5F9", # 窗口底色
"surface": "#FFFFFF", # 卡片/面板
"surface_alt": "#F8FAFC", # 次级表面 (Text 背景, 斑马行)
"border": "#D8DEE8", # 边框
"border_soft": "#E5E9F0", # 轻分割线
"text": "#1F2937", # 主文字
"text_muted": "#6B7280", # 次要文字
"text_light": "#9CA3AF", # 更浅文字 (占位符)
"primary": "#2563EB", # 主色 (运行按钮)
"primary_hov": "#1D4ED8",
"primary_act": "#1E40AF",
"primary_light":"#EFF6FF", # 主色浅底
"accent": "#0EA5E9", # 次级按钮
"accent_hov": "#0284C7",
"success": "#16A34A",
"success_light":"#F0FDF4", # 成功浅底
"warning": "#D97706",
"warning_light":"#FFFBEB", # 警告浅底
"danger": "#DC2626",
"danger_light": "#FEF2F2", # 危险浅底
"selected": "#DBEAFE", # 选中高亮
"gold": "#B8860B", # 金色 (装饰线)
"tab_inactive": "#E2E8F0", # 未选中 tab 底色
}
# matplotlib 整体风格配置 (与 Tk 主题协调)
plt.rcParams['axes.facecolor'] = PALETTE["surface"]
plt.rcParams['figure.facecolor'] = PALETTE["surface"]
plt.rcParams['axes.edgecolor'] = PALETTE["border"]
plt.rcParams['axes.labelcolor'] = PALETTE["text"]
plt.rcParams['xtick.color'] = PALETTE["text_muted"]
plt.rcParams['ytick.color'] = PALETTE["text_muted"]
plt.rcParams['axes.titlecolor'] = PALETTE["text"]
plt.rcParams['axes.titleweight'] = 'bold'
plt.rcParams['grid.color'] = PALETTE["border_soft"]
plt.rcParams['grid.linestyle'] = '--'
plt.rcParams['grid.linewidth'] = 0.6
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
# 确保 pricing 包可导入
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from pricing import (
Option_AB, Option_AS, Option_DE, Option_SNB, Option_Vanilla, HedgeBacktest,
FixedFreqStrategy, SigmaBandStrategy,
)
from pricing.constants import ANNUAL_DAYS
# ============================================================
# 期权类型注册表
# ============================================================
def _snowball_ko_observ(T, first_obs, period):
"""按"锁定期 + 固定间隔 + 末次=到期"生成敲出观察交易日序号(1-based)。
全用交易日:首个观察在第 first_obs 日,其后每 period 个交易日一次,并
强制最后一次落在到期日 T(与到期不齐时末段为短桩)。返回升序去重列表。
"""
T = int(T)
first_obs = max(1, int(first_obs))
period = max(1, int(period))
days = list(range(first_obs, T + 1, period))
if not days or days[-1] != T:
days.append(T) # 末次观察 = 到期
return sorted({d for d in days if 1 <= d <= T})
def _build_snowball(st, p):
"""构造雪球(act=1 交易日计息)。观察日按锁定期+固定间隔生成(全交易日口径);
ko_step>0 时为降敲,KO 自期初值每观察日递减 ko_step 个点,得逐观察日 KO 向量。"""
T = int(p["T"])
ko_observ = _snowball_ko_observ(T, p["first_obs_d"], p["obs_period_d"])
step = float(p.get("ko_step", 0.0) or 0.0)
KO = [p["KO"] - step * i for i in range(len(ko_observ))] if step else p["KO"]
return Option_SNB(
st, p["s00"], p["s0"], p["K"], p["KI"], KO, T,
p["sigma"], p["coupon"], p["coupon_ko"], p["margin"], 1, p["cp"],
r=p["r"], q=p["q"], sr=[], ko_observ=ko_observ, nPath=p["nPath"],
margin_call=bool(p["margin_call"]),
)
OPTION_CLASSES = {
"香草期权 (Vanilla)": {
"class": Option_Vanilla,
"subtypes": ["Eu"],
"params": [
("s0", "初始价格 S0", float, 100.0),
("K", "行权价", float, 100.0),
("T_days", "期限(交易日)", int, 22),
("sigma", "波动率", float, 0.18),
("cp", "方向", int, 1, {"看涨 (Call)": 1, "看跌 (Put)": -1}),
("r", "无风险利率", float, 0.03),
("q", "分红率", float, 0.03),
],
"build": lambda st, p: Option_Vanilla(
st, p["s0"], [], p["K"], p["T_days"],
p["sigma"], p["cp"],
r=p["r"], q=p["q"], exe_mode=st,
),
},
"累计期权 (Decumulator)": {
"class": Option_DE,
"subtypes": [
"Opt_Decumulator", "Opt_Decumulator_Back",
"Opt_Decumulator_Fix", "Opt_Decumulator_Fix_E",
"Opt_EnDecumulator", "Opt_EnDecumulator_Fix",
"Opt_ASGQ_call_put", "Opt_ASGQ_EP", "Opt_ASGQ_EF", "Opt_ASGQ_EFF",
"Opt_ASGQ_DP", "Opt_ASGQ_DF", "Opt_ASGQ_DFF",
],
"params": [
("s0", "初始价格 S0", float, 100.0),
("K", "行权价", float, 90.0),
("T_days", "剩余期限(交易日)", int, 20),
("T_over", "已过天数", int, 0),
("sigma", "波动率", float, 0.18),
("H", "障碍价格", float, 110.0),
("N", "杠杆倍数", int, 2),
("cp", "方向", int, 1, {"看涨 (Call)": 1, "看跌 (Put)": -1}),
("fix", "固定赔付(可选)", float, 0.0),
("P", "保障价格(可选)", float, 0.0),
("amount", "固定金额(可选)", float, 0.0),
("r", "无风险利率", float, 0.03),
("q", "分红率", float, 0.03),
("nPath", "定价路径数 (MC)", int, 100000),
],
"build": lambda st, p: Option_DE(
st, p["s0"], [], p["K"], p["T_over"], p["T_days"],
list(range(1, p["T_days"] + p["T_over"] + 1)),
p["sigma"], p["H"], p["N"], p["cp"],
r=p["r"], q=p["q"], nPath=p["nPath"],
fix=p["fix"] if p["fix"] else None,
P=p["P"] if p["P"] else None,
amount=p["amount"] if p["amount"] else None,
),
},
"亚式期权 (Asian)": {
"class": Option_AS,
"subtypes": ["Asian", "EnhanceAsian"],
"params": [
("s0", "初始价格 S0", float, 100.0),
("K", "行权价", float, 100.0),
("E", "增强价(Enhanced)", float, 100.0),
("T", "期限(交易日)", int, 22),
("N", "观察日数", int, 22),
("sigma", "波动率", float, 0.15),
("cp", "方向", int, 1, {"看涨 (Call)": 1, "看跌 (Put)": -1}),
("minPay", "最低赔付", float, 0.0),
("maxPay", "最高赔付", float, 999999.0),
("r", "无风险利率", float, 0.03),
("q", "分红率", float, 0.03),
("nPath", "定价路径数 (MC)", int, 100000),
],
"build": lambda st, p: Option_AS(
st, p["s0"], [], p["K"], p["E"], p["T"], p["N"],
p["sigma"], p["cp"], p["minPay"], p["maxPay"],
r=p["r"], q=p["q"], nPath=p["nPath"]
),
},
"气囊期权 (Airbag)": {
"class": Option_AB,
"subtypes": ["Opt_Airbag"],
"params": [
("s0", "初始价格 S0", float, 100.0),
("K", "行权价", float, 100.0),
("KI", "敲入价", float, 90.0),
("T_days","期限(交易日)", int, 20),
("sigma", "波动率", float, 0.18),
("pr", "参与率", float, 0.8),
("pr_ki", "敲入参与率", float, 1.0),
("cp", "方向", int, 1, {"看涨 (Call)": 1, "看跌 (Put)": -1}),
("r", "无风险利率", float, 0.03),
("q", "分红率", float, 0.03),
("nPath", "定价路径数 (MC)", int, 100000),
],
"build": lambda st, p: Option_AB(
st, p["s0"], [], p["K"], p["KI"], p["T_days"],
list(range(1, p["T_days"] + 1)),
p["sigma"], p["pr"], p["pr_ki"], p["cp"],
r=p["r"], q=p["q"], nPath=p["nPath"]
),
},
"雪球期权 (Snowball)": {
"class": Option_SNB,
"subtypes": ["Opt_Snowball"],
"params": [
("s00", "入场价 S00", float, 100.0),
("s0", "最新价 S0", float, 100.0),
("K", "行权价", float, 100.0),
("KI", "敲入价", float, 80.0),
("KO", "期初敲出价", float, 103.0),
("T", "剩余期限(交易日)", int, 243),
# 锁定期/观察间隔:值用交易日(与引擎一致),下拉给月度预设辅助输入,
# 可编辑——既能选预设也能手填自定义交易日数(21 交易日 ≈ 1 个月)。
("first_obs_d","首次敲出观察", int, 63,
{"锁1月 (21)": 21, "锁2月 (42)": 42, "锁3月 (63)": 63, "锁6月 (126)": 126},
{"editable": True}),
("obs_period_d","观察间隔", int, 21,
{"月度 (21)": 21, "双月 (42)": 42, "季度 (63)": 63, "半年 (126)": 126},
{"editable": True}),
("ko_step", "每期降敲(点,0=平敲)", float, 0.0),
("sigma", "波动率", float, 0.15),
("coupon", "未敲出票息率(年化)", float, 0.15),
("coupon_ko", "敲出票息率(年化)", float, 0.15),
("margin_call", "保证金模式", int, 1, {"追保(亏损不封顶)": 1, "不追保(有限亏损)": 0}),
("margin", "保证金比例(不追保封顶)", float, 0.2),
("cp", "方向", int, -1, {"雪球 (卖看跌)": -1, "反雪球 (卖看涨)": 1}),
("r", "无风险利率", float, 0.03),
("q", "分红率", float, 0.03),
("nPath", "定价路径数 (MC)", int, 20000),
],
# act 固定为 1(交易日计息,无需交易日历);观察日=锁定期+固定间隔+末次到期,
# ko_step>0 时为降敲(逐观察日 KO 递减),见 _build_snowball。
"build": _build_snowball,
},
}
# ============================================================
# GUI 显示名 ↔ 后端内部键 映射
# 说明:后端 (hedge_backtest / Option_* 类) 使用英文/方法名做字符串匹配,
# 这里仅影响界面显示;读取 Combobox 值后需通过 *_FROM_DISPLAY 反向映射
# 还原为内部键再传给后端。
# ============================================================
SUBTYPE_DISPLAY = {
"Eu": "欧式 (Eu)",
"Opt_Decumulator": "普通累计 (Opt_Decumulator)",
"Opt_Decumulator_Back": "回归累计 (Opt_Decumulator_Back)",
"Opt_Decumulator_Fix": "固定赔付回归累计 (Opt_Decumulator_Fix)",
"Opt_Decumulator_Fix_E": "固赔到期结算累计 (Opt_Decumulator_Fix_E)",
"Opt_EnDecumulator": "增强回归累计 (Opt_EnDecumulator)",
"Opt_EnDecumulator_Fix": "固定赔付增强累计 (Opt_EnDecumulator_Fix)",
"Opt_ASGQ_call_put": "到期熔断保障累计 (Opt_ASGQ_call_put)",
"Opt_ASGQ_EP": "熔断每日保障累计 (Opt_ASGQ_EP)",
"Opt_ASGQ_EF": "熔断每日固赔累计 (Opt_ASGQ_EF)",
"Opt_ASGQ_EFF": "熔断每日双固赔累计 (Opt_ASGQ_EFF)",
"Opt_ASGQ_DP": "每日熔断保障累计 (Opt_ASGQ_DP)",
"Opt_ASGQ_DF": "每日熔断固赔累计 (Opt_ASGQ_DF)",
"Opt_ASGQ_DFF": "每日熔断双固赔累计 (Opt_ASGQ_DFF)",
"Asian": "标准亚式 (Asian)",
"EnhanceAsian": "增强亚式 (EnhanceAsian)",
"Opt_Airbag": "气囊 (Opt_Airbag)",
"Opt_Snowball": "雪球 (Opt_Snowball)",
}
SUBTYPE_FROM_DISPLAY = {v: k for k, v in SUBTYPE_DISPLAY.items()}
STRATEGY_DISPLAY = {
"fixed_freq": "固定频率调仓",
"sigma_band": "σ 带宽调仓",
}
STRATEGY_FROM_DISPLAY = {v: k for k, v in STRATEGY_DISPLAY.items()}
SIGMA_SOURCE_DISPLAY = {
"implied": "隐含波动率",
"realized": "已实现波动率",
}
SIGMA_SOURCE_FROM_DISPLAY = {v: k for k, v in SIGMA_SOURCE_DISPLAY.items()}
# ============================================================
# 期权结构说明文档
# ============================================================
STRUCTURE_DOCS = {
("香草期权 (Vanilla)", "Eu"): (
"【欧式香草期权】\n"
"• Payoff: Call max(S_T−K,0) / Put max(K−S_T,0)\n"
"• 定价: Black-Scholes 封闭解\n\n"
"风险特征:\n"
" Delta 单调 0→1 (call) 或 −1→0 (put)\n"
" Gamma 集中于 ATM (S≈K), 随 T 缩小放大\n"
" Vega 对 ATM 最敏感, 随 √T 增长\n"
" Theta 为买方持续付出的时间价值"
),
("累计期权 (Decumulator)", "Opt_Decumulator"): (
"【普通累计 Opt_Decumulator】\n"
"每日观察 + 每日结算, 敲出即终止存续:\n"
" • 首次 S ≥ H (敲出): 当日及之后均停止累计\n"
" • K < S < H : 1 倍 (S − K) 结算\n"
" • S ≤ K : N 倍杠杆 (S − K) 结算\n\n"
"与 Back 的差异: Back 敲出仅当日计 0、后续仍继续观察;\n"
"本结构敲出即彻底了结, 路径依赖更强."
),
("累计期权 (Decumulator)", "Opt_Decumulator_Back"): (
"【回归累计 Opt_Decumulator_Back】\n"
"每日观察 + 每日结算, 三段式 cashflow:\n"
" • S ≥ H (敲出障碍): 当日 0 赔付\n"
" • K < S < H : 1 倍 (S − K) 结算\n"
" • S ≤ K : N 倍杠杆 (S − K) 结算\n\n"
"总损益 = 所有观察日折现加总.\n"
"卖方希望标的震荡于 (K, H) 区间, 触 K 承 N 倍下行."
),
("累计期权 (Decumulator)", "Opt_Decumulator_Fix"): (
"【固定赔付回归累计 Opt_Decumulator_Fix】\n"
"结构同 Back, 差异:\n"
" • K < S < H 区间按固定金额 `fix` 结算, 而非 (S−K)\n"
" • 敲出段/杠杆段逻辑不变\n\n"
"锁定中间段现金流, 便于账务管理."
),
("累计期权 (Decumulator)", "Opt_Decumulator_Fix_E"): (
"【固赔到期结算累计 Opt_Decumulator_Fix_E】\n"
"结构同 Fix, 差异在杠杆段结算方式:\n"
" • K ≤ S < H 区间: 每日固定金额 `fix`\n"
" • 杠杆段 (S ≤ K): 不每日结算, 仅按到期日收盘价\n"
" 一次性结算 (S_T − K) × 累计天数 × N\n\n"
"适合到期一次性交割杠杆腿的固赔回归累计."
),
("累计期权 (Decumulator)", "Opt_EnDecumulator"): (
"【增强回归累计 Opt_EnDecumulator】\n"
"三段式每日结算:\n"
" • S ≥ H : (S − H) 1 倍 (敲出后仍给买方正向收益)\n"
" • K < S < H: (S − K) 1 倍\n"
" • S ≤ K : (S − K) N 倍\n\n"
"相比 Back, 保留敲出后上行收益, 故称'增强'."
),
("累计期权 (Decumulator)", "Opt_EnDecumulator_Fix"): (
"【固定赔付增强累计 Opt_EnDecumulator_Fix】\n"
" • S ≥ H : (S − H) 1 倍\n"
" • K < S < H: 固定金额 `fix`\n"
" • S ≤ K : (S − K) N 倍"
),
("累计期权 (Decumulator)", "Opt_ASGQ_call_put"): (
"【到期观察熔断保障累计 ASGQ_call_put】\n"
"路径依赖 + 到期一次性结算:\n"
" • 若路径曾 S ≥ H (熔断): 熔断日后统一按 (S_T − P)\n"
" • 从未熔断: 按 (S_T − K), 若 S_T ≤ K 额外 N 倍\n\n"
"保障价 P 提供下行软保护, ASGQ = 熔断保障累计."
),
("累计期权 (Decumulator)", "Opt_ASGQ_EP"): (
"【熔断保障累计(每日结算) ASGQ_EP】\n"
" • 未熔断部分: 按日 (S − K) 累加\n"
" • 熔断日起 : 每日 (S − P) 结算"
),
("累计期权 (Decumulator)", "Opt_ASGQ_EF"): (
"【熔断固定赔付累计 ASGQ_EF】\n"
" • 未熔断部分: 按日 (S − K) 累加\n"
" • 熔断日起 : 每日固定金额 `amount`"
),
("累计期权 (Decumulator)", "Opt_ASGQ_EFF"): (
"【熔断每日双固赔累计 ASGQ_EFF】\n"
"到期观察 + 每日结算, 双固定赔付:\n"
" • K < S < H (区间): 每日固定金额 `fix`\n"
" • S ≤ K : 每日 (S − K) 1 倍\n"
" • 熔断日起 : 每日固定金额 `amount`\n"
" • 到期 S_T ≤ K 且未熔断: 额外结算\n"
" (S_T − K) × 累计天数 × (N − 1) 杠杆腿"
),
("累计期权 (Decumulator)", "Opt_ASGQ_DP"): (
"【每日观察熔断保障累计 ASGQ_DP】\n"
"每日观察 + 每日结算:\n"
" • 未熔断: (S − K), S ≤ K 时乘 N 倍\n"
" • 熔断日起: 每日 (S − P)\n\n"
"比到期版对路径更敏感, Delta/Gamma 跳跃更剧烈."
),
("累计期权 (Decumulator)", "Opt_ASGQ_DF"): (
"【每日观察熔断固定赔付累计 ASGQ_DF】\n"
" • 未熔断: (S − K), S ≤ K 时 N 倍\n"
" • 熔断日起: 每日固定金额 `amount`"
),
("累计期权 (Decumulator)", "Opt_ASGQ_DFF"): (
"【每日熔断双固赔累计 ASGQ_DFF】\n"
"每日观察 + 每日结算, 双固定赔付:\n"
" • K < S < H (区间): 每日固定金额 `fix`\n"
" • S ≤ K : (S − K) N 倍\n"
" • 熔断日起 : 每日固定金额 `amount`"
),
("亚式期权 (Asian)", "Asian"): (
"【亚式期权 Asian】\n"
"Payoff = clip( mean(S[-N:]) − K, minPay, maxPay ) × cp\n\n"
" • 取最后 N 个交易日均价与 K 的差额\n"
" • minPay / maxPay 限定赔付区间\n"
" • 平均化显著降低末日价格风险\n"
" • Gamma / Vega 远低于同期限 Vanilla"
),
("亚式期权 (Asian)", "EnhanceAsian"): (
"【增强亚式 EnhanceAsian】\n"
"每日先做价格增强:\n"
" • Call: 观察价 = max(S, E)\n"
" • Put : 观察价 = min(S, E)\n"
"再求均值与 K 比较, 并 clip 到 [minPay, maxPay].\n\n"
"E 提供'每日保底'效果, 提升买方期望."
),
("气囊期权 (Airbag)", "Opt_Airbag"): (
"【气囊期权 Opt_Airbag】\n"
"到期结算, 路径判断是否敲入 KI:\n"
" • 未敲入 (Call: min(S) > KI): pr × max(S_T − K, 0)\n"
" • 已敲入 : pr_ki × (S_T − K)\n\n"
"小幅下行时买方有软垫保护 (payoff=0 而非负);\n"
"一旦跌破 KI, 转为线性承担下行, 即'气囊爆掉'."
),
("雪球期权 (Snowball)", "Opt_Snowball"): (
"【雪球期权 Opt_Snowball】 (cp=-1 雪球 / cp=1 反雪球)\n"
"MC 定价, 路径依赖, 四种到期情形:\n"
" • 未敲入未敲出: 全期票息 s00 × coupon × 期限\n"
" • 敲出 (观察日触 KO): 敲出票息 × 持有期, 提前了结\n"
" • 敲入且敲出: 同敲出 (敲出优先)\n"
" • 敲入未敲出: 追保=承担完整亏损; 不追保=亏损按 margin × s00 封顶\n\n"
"敲入逐日监测; 敲出按固定间隔观察 (首次=锁定期后, 末次=到期日).\n"
"每期降敲(ko_step>0)时 KO 自期初值逐期递减, 越往后越易敲出.\n"
"卖方 (持有者) 短 vega/gamma、正 theta; 现价 ↗ 近 KO 时\n"
"Delta 与 Gamma 易出现剧烈跳变 (敲出悬崖)."
),
}
# ============================================================
# 主窗口
# ============================================================
class BacktestApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("DeltaLab - 期权对冲回测系统")
self.geometry("1600x1000")
# 左侧面板已启用垂直滚动, 这里可以给一个更宽容的最小尺寸,
# 即便在高 DPI / 小分辨率屏幕下也不会裁掉底部按钮.
self.minsize(1200, 720)
self.configure(bg=PALETTE["bg"])
self._apply_window_icon()
self._setup_styles()
self._build_ui()
self._param_entries = {}
self._on_option_class_change(None)
# ---- 窗口图标 ----
def _apply_window_icon(self):
ico_path = _resource_path("assets", "deltalab.ico")
# 带透明边距的图标 (符合 macOS 网格), 直接运行脚本时 Dock 图标不会过大
padded_path = _resource_path("assets", "deltalab_padded.png")
png_path = padded_path if os.path.exists(padded_path) else _resource_path("assets", "deltalab.png")
# Windows: .ico 在任务栏/标题栏表现最佳
if _SYSTEM == "Windows" and os.path.exists(ico_path):
try:
self.iconbitmap(default=ico_path)
return
except tk.TclError:
pass
# 其它平台 (macOS / Linux) 或 Windows 回退: iconphoto + PNG
if os.path.exists(png_path):
try:
self._icon_photo = tk.PhotoImage(file=png_path)
self.iconphoto(True, self._icon_photo)
except tk.TclError:
pass
# ---- 样式 ----
def _setup_styles(self):
style = ttk.Style(self)
style.theme_use("clam")
base_font = (_UI_FONT_FAMILY, 10)
small_font = (_UI_FONT_FAMILY, 9)
title_font = (_UI_FONT_FAMILY, 18, "bold")
subtitle_font = (_UI_FONT_FAMILY, 10)
header_font = (_UI_FONT_FAMILY, 10, "bold")
group_font = (_UI_FONT_FAMILY, 10, "bold")
tab_font = (_UI_FONT_FAMILY, 12, "bold")
btn_font = (_UI_FONT_FAMILY, 10)
run_font = (_UI_FONT_FAMILY, 11, "bold")
# 默认选项 (供 tk.* 原生控件继承)
self.option_add("*Font", base_font)
self.option_add("*TCombobox*Listbox*Font", base_font)
# ---- 通用 Frame / Label ----
style.configure("TFrame", background=PALETTE["bg"])
style.configure("Surface.TFrame", background=PALETTE["surface"])
style.configure("Card.TFrame",
background=PALETTE["surface"],
relief="flat", borderwidth=1)
style.configure("TLabel",
background=PALETTE["bg"],
foreground=PALETTE["text"],
font=base_font)
style.configure("Surface.TLabel",
background=PALETTE["surface"],
foreground=PALETTE["text"])
style.configure("Muted.TLabel",
background=PALETTE["bg"],
foreground=PALETTE["text_muted"],
font=small_font)
style.configure("SurfaceMuted.TLabel",
background=PALETTE["surface"],
foreground=PALETTE["text_muted"],
font=small_font)
style.configure("Title.TLabel",
background=PALETTE["bg"],
foreground=PALETTE["text"],
font=title_font)
style.configure("Subtitle.TLabel",
background=PALETTE["bg"],
foreground=PALETTE["text_muted"],
font=subtitle_font)
style.configure("Header.TLabel",
background=PALETTE["bg"],
foreground=PALETTE["text"],
font=header_font)
style.configure("Status.TLabel",
background=PALETTE["surface"],
foreground=PALETTE["text_muted"],
font=small_font,
padding=(8, 4))
# ---- LabelFrame (分组容器) ----
style.configure("TLabelframe",
background=PALETTE["surface"],
bordercolor=PALETTE["border"],
relief="solid", borderwidth=1)
style.configure("TLabelframe.Label",
background=PALETTE["surface"],
foreground=PALETTE["primary"],
font=group_font,
padding=(4, 0))
# ---- 输入控件 ----
style.configure("TEntry",
fieldbackground=PALETTE["surface"],
foreground=PALETTE["text"],
bordercolor=PALETTE["border"],
lightcolor=PALETTE["border"],
darkcolor=PALETTE["border"],
padding=4)
style.map("TEntry",
bordercolor=[("focus", PALETTE["primary"])],
lightcolor=[("focus", PALETTE["primary"])])
style.configure("TCombobox",
fieldbackground=PALETTE["surface"],
background=PALETTE["surface"],
foreground=PALETTE["text"],
bordercolor=PALETTE["border"],
arrowcolor=PALETTE["text_muted"],
padding=3)
style.map("TCombobox",
fieldbackground=[("readonly", PALETTE["surface"])],
bordercolor=[("focus", PALETTE["primary"])],
arrowcolor=[("active", PALETTE["primary"])])
# ---- Radio/Check (背景分两套: Frame 背景 vs Surface 背景) ----
style.configure("TRadiobutton",
background=PALETTE["surface"],
foreground=PALETTE["text"],
font=base_font,
focuscolor=PALETTE["surface"])
style.map("TRadiobutton",
background=[("active", PALETTE["surface"])],
foreground=[("active", PALETTE["primary"])])
style.configure("TCheckbutton",
background=PALETTE["surface"],
foreground=PALETTE["text"],
font=base_font,
focuscolor=PALETTE["surface"])
# ---- 按钮 ----
style.configure("TButton",
font=btn_font,
background=PALETTE["surface"],
foreground=PALETTE["text"],
bordercolor=PALETTE["border"],
focusthickness=0,
padding=(10, 6),
relief="flat")
style.map("TButton",
background=[("active", PALETTE["border_soft"]),
("pressed", PALETTE["border"])],
bordercolor=[("active", PALETTE["primary"])])
# 主色按钮 (运行)
style.configure("Run.TButton",
font=run_font,
foreground="white",
background=PALETTE["primary"],
bordercolor=PALETTE["primary"],
padding=(14, 8),
relief="flat")
style.map("Run.TButton",
background=[("active", PALETTE["primary_hov"]),
("pressed", PALETTE["primary_act"]),
("disabled", "#9CA3AF")],
foreground=[("disabled", "#E5E7EB")])
# 次级按钮 (结构图)
style.configure("Accent.TButton",
font=btn_font,
foreground="white",
background=PALETTE["accent"],
bordercolor=PALETTE["accent"],
padding=(10, 6),
relief="flat")
style.map("Accent.TButton",
background=[("active", PALETTE["accent_hov"]),
("pressed", PALETTE["accent_hov"]),
("disabled", "#9CA3AF")],
foreground=[("disabled", "#E5E7EB")])
# ---- Notebook (现代卡片式 Tab) ----
style.configure("TNotebook",
background=PALETTE["bg"],
bordercolor=PALETTE["border_soft"],
tabmargins=(4, 8, 4, 0))
style.configure("TNotebook.Tab",
font=tab_font,
background=PALETTE["tab_inactive"],
foreground=PALETTE["text_muted"],
bordercolor=PALETTE["border_soft"],
padding=(20, 10, 20, 10),
focuscolor=PALETTE["bg"])
style.map("TNotebook.Tab",
background=[("selected", PALETTE["surface"]),
("active", PALETTE["border_soft"])],
foreground=[("selected", PALETTE["primary"]),
("active", PALETTE["text"])],
bordercolor=[("selected", PALETTE["primary"])],
expand=[("selected", (0, 2, 0, 0))])
# ---- Treeview (更宽松的行距, 更美观的表头) ----
style.configure("Treeview",
background=PALETTE["surface"],
fieldbackground=PALETTE["surface"],
foreground=PALETTE["text"],
bordercolor=PALETTE["border_soft"],
rowheight=28,
font=(_MONO_FONT_FAMILY, 9))
style.configure("Treeview.Heading",
background=PALETTE["primary_light"],
foreground=PALETTE["primary"],
font=header_font,
relief="flat",
padding=(8, 8))
style.map("Treeview.Heading",
background=[("active", PALETTE["selected"])])
style.map("Treeview",
background=[("selected", PALETTE["selected"])],
foreground=[("selected", PALETTE["primary"])])
# ---- Scrollbar ----
style.configure("Vertical.TScrollbar",
background=PALETTE["bg"],
troughcolor=PALETTE["bg"],
bordercolor=PALETTE["bg"],
arrowcolor=PALETTE["text_muted"],
gripcount=0)
style.map("Vertical.TScrollbar",
background=[("active", PALETTE["border"])])
style.configure("Horizontal.TScrollbar",
background=PALETTE["bg"],
troughcolor=PALETTE["bg"],
bordercolor=PALETTE["bg"],
arrowcolor=PALETTE["text_muted"],
gripcount=0)
style.map("Horizontal.TScrollbar",
background=[("active", PALETTE["border"])])
# ---- Progressbar ----
style.configure("TProgressbar",
background=PALETTE["primary"],
troughcolor=PALETTE["border_soft"],
bordercolor=PALETTE["border_soft"],
lightcolor=PALETTE["primary"],
darkcolor=PALETTE["primary"])
# ---- PanedWindow ----
style.configure("TPanedwindow", background=PALETTE["bg"])
style.configure("TPanedwindow.Sash",
background=PALETTE["border"],
sashthickness=6)
# ---- Separator ----
style.configure("TSeparator", background=PALETTE["border"])
# ---- 界面构建 ----
def _build_ui(self):
# 顶部标题条 (带底部分隔线)
header = ttk.Frame(self)
header.pack(fill="x", padx=0, pady=0)
title_row = ttk.Frame(header)
title_row.pack(fill="x", padx=18, pady=(14, 4))
ttk.Label(title_row, text="DeltaLab",
style="Title.TLabel").pack(side="left")
ttk.Label(title_row,
text="期权动态对冲回测系统",
style="Subtitle.TLabel").pack(side="left", padx=(12, 0), pady=(8, 0))
ttk.Separator(header, orient="horizontal").pack(fill="x", padx=0, pady=(6, 0))
# 主体:左侧参数 + 右侧结果
body = ttk.PanedWindow(self, orient="horizontal")
body.pack(fill="both", expand=True, padx=12, pady=(8, 4))
# ─── 左侧面板 (整体包一层 Canvas + Scrollbar, 解决低分辨率/高 DPI 下底部按钮被裁) ───
left_outer = ttk.Frame(body)
body.add(left_outer, weight=1)
# 外层 Canvas 横向充满, Scrollbar 靠右
# width 仅作为 PanedWindow 初始 sash 位置的参考, 不阻止后续缩放
self._left_canvas = tk.Canvas(
left_outer, highlightthickness=0, bd=0,
bg=PALETTE["surface"], width=440,
)
self._left_scrollbar = ttk.Scrollbar(
left_outer, orient="vertical", command=self._left_canvas.yview
)
self._left_canvas.configure(yscrollcommand=self._left_scrollbar.set)
self._left_scrollbar.pack(side="right", fill="y")
self._left_canvas.pack(side="left", fill="both", expand=True)
# inner frame: 真正承载左侧所有 LabelFrame/按钮; 父控件必须是 Canvas 本身.
self._left_inner = ttk.Frame(self._left_canvas, style="Surface.TFrame")
self._left_inner_id = self._left_canvas.create_window(
(0, 0), window=self._left_inner, anchor="nw"
)
# inner 尺寸变化 → 更新 scrollregion
def _on_inner_configure(event):
self._left_canvas.configure(scrollregion=self._left_canvas.bbox("all"))
self._left_inner.bind("<Configure>", _on_inner_configure)
# canvas 宽度变化 → inner 跟随横向铺满 (否则 inner 默认是内容宽度, 右侧会有空白)
def _on_canvas_configure(event):
self._left_canvas.itemconfigure(self._left_inner_id, width=event.width)
self._left_canvas.bind("<Configure>", _on_canvas_configure)
# 鼠标滚轮处理: 只在鼠标进入左侧面板时启用, 离开则取消, 避免劫持右侧图表/表格的滚轮.
def _on_left_mousewheel(event):
# Windows: event.delta 为 ±120 的倍数; 正值向上滚, 负值向下滚.
self._left_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break"
def _on_left_mousewheel_linux(event):
units = -3 if event.num == 4 else 3
self._left_canvas.yview_scroll(units, "units")
return "break"
def _bind_left_wheel(_event=None):
if _SYSTEM == "Linux":
self._left_canvas.bind_all("<Button-4>", _on_left_mousewheel_linux)
self._left_canvas.bind_all("<Button-5>", _on_left_mousewheel_linux)
else:
self._left_canvas.bind_all("<MouseWheel>", _on_left_mousewheel)
def _unbind_left_wheel(_event=None):
if _SYSTEM == "Linux":
self._left_canvas.unbind_all("<Button-4>")
self._left_canvas.unbind_all("<Button-5>")
else:
self._left_canvas.unbind_all("<MouseWheel>")
# 保留引用, 其它地方如需手动启停滚轮可复用
self._left_wheel_bind = _bind_left_wheel
self._left_wheel_unbind = _unbind_left_wheel
self._left_canvas.bind("<Enter>", _bind_left_wheel)
self._left_canvas.bind("<Leave>", _unbind_left_wheel)
self._left_inner.bind("<Enter>", _bind_left_wheel)
self._left_inner.bind("<Leave>", _unbind_left_wheel)
# 之后所有原本放在 left 里的控件, 父容器改为 left_inner
left = self._left_inner
# 1) 期权大类
sec1 = ttk.LabelFrame(left, text=" 期权类型 ", padding=12)
sec1.pack(fill="x", pady=(0, 8))
ttk.Label(sec1, text="大类:", style="Surface.TLabel").grid(
row=0, column=0, sticky="w", pady=4)
self._class_var = tk.StringVar()
class_cb = ttk.Combobox(sec1, textvariable=self._class_var, width=25,
values=list(OPTION_CLASSES.keys()), state="readonly")
class_cb.grid(row=0, column=1, padx=(8, 0), pady=4, sticky="ew")
class_cb.current(0)
class_cb.bind("<<ComboboxSelected>>", self._on_option_class_change)
ttk.Label(sec1, text="子类型:", style="Surface.TLabel").grid(
row=1, column=0, sticky="w", pady=4)
self._subtype_var = tk.StringVar()
self._subtype_cb = ttk.Combobox(sec1, textvariable=self._subtype_var,
width=25, state="readonly")
self._subtype_cb.grid(row=1, column=1, padx=(8, 0), pady=4, sticky="ew")
sec1.columnconfigure(1, weight=1)
# 2) 期权参数
# 说明: 外层左侧已经有整体 Canvas+Scrollbar, 这里不再嵌套独立滚动容器,
# 让参数区按内容自然撑开高度, 整体滚动由外层统一处理.
sec2 = ttk.LabelFrame(left, text=" 期权参数 ", padding=12)
sec2.pack(fill="x", pady=(0, 8))
self._param_frame = ttk.Frame(sec2, style="Surface.TFrame")
self._param_frame.pack(fill="x", expand=True)
# 3) 回测设置
sec3 = ttk.LabelFrame(left, text=" 回测设置 ", padding=12)
sec3.pack(fill="x", pady=(0, 8))
ttk.Label(sec3, text="数据来源:", style="Surface.TLabel").grid(
row=0, column=0, sticky="w", pady=4)
self._source_var = tk.StringVar(value="simulate")
src_frame = ttk.Frame(sec3, style="Surface.TFrame")
src_frame.grid(row=0, column=1, sticky="w", padx=(8, 0))
ttk.Radiobutton(src_frame, text="模拟", variable=self._source_var,
value="simulate", command=self._toggle_source).pack(side="left", padx=(0, 6))
ttk.Radiobutton(src_frame, text="CSV", variable=self._source_var,
value="csv", command=self._toggle_source).pack(side="left", padx=6)
ttk.Radiobutton(src_frame, text="Wind", variable=self._source_var,
value="wind", command=self._toggle_source).pack(side="left", padx=6)
# 模拟参数
self._sim_frame = ttk.Frame(sec3, style="Surface.TFrame")
self._sim_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(4, 2))
ttk.Label(self._sim_frame, text="种子:", style="Surface.TLabel").grid(
row=0, column=0, sticky="w", pady=2)
self._seed_var = tk.StringVar(value="42")
ttk.Entry(self._sim_frame, textvariable=self._seed_var, width=10).grid(
row=0, column=1, padx=(6, 0), pady=2, sticky="w")
ttk.Label(self._sim_frame, text="已实现波动率:", style="Surface.TLabel").grid(
row=1, column=0, sticky="w", pady=2)
self._real_vol_var = tk.StringVar(value="")
rv_frame = ttk.Frame(self._sim_frame, style="Surface.TFrame")
rv_frame.grid(row=1, column=1, columnspan=3, sticky="w", padx=(6, 0), pady=2)
ttk.Entry(rv_frame, textvariable=self._real_vol_var, width=10).pack(side="left")
ttk.Label(rv_frame, text=" 空=同隐含", style="SurfaceMuted.TLabel").pack(side="left")
ttk.Label(self._sim_frame, text="回测路径数 (MC):", style="Surface.TLabel").grid(
row=2, column=0, sticky="w", pady=2)
self._npaths_var = tk.StringVar(value="10")
ttk.Entry(self._sim_frame, textvariable=self._npaths_var, width=10).grid(
row=2, column=1, padx=(6, 0), pady=2, sticky="w")
# CSV 参数
self._csv_frame = ttk.Frame(sec3, style="Surface.TFrame")
ttk.Label(self._csv_frame, text="文件:", style="Surface.TLabel").grid(
row=0, column=0, sticky="w", pady=2)
self._csv_path_var = tk.StringVar()
ttk.Entry(self._csv_frame, textvariable=self._csv_path_var, width=22).grid(
row=0, column=1, padx=(6, 4), pady=2)
ttk.Button(self._csv_frame, text="浏览…", width=6,
command=self._browse_csv).grid(row=0, column=2, pady=2)
ttk.Label(self._csv_frame, text="价格列:", style="Surface.TLabel").grid(
row=1, column=0, sticky="w", pady=2)
self._csv_col_var = tk.StringVar(value="close")
ttk.Entry(self._csv_frame, textvariable=self._csv_col_var, width=12).grid(
row=1, column=1, padx=(6, 0), pady=2, sticky="w")
# Wind 参数
self._wind_frame = ttk.Frame(sec3, style="Surface.TFrame")
ttk.Label(self._wind_frame, text="代码:", style="Surface.TLabel").grid(
row=0, column=0, sticky="w", pady=2)
self._wind_code_var = tk.StringVar(value="510050.SH")
ttk.Entry(self._wind_frame, textvariable=self._wind_code_var, width=15).grid(
row=0, column=1, padx=(6, 0), pady=2)
ttk.Label(self._wind_frame, text="起始日:", style="Surface.TLabel").grid(
row=1, column=0, sticky="w", pady=2)
_today = datetime.date.today()
_wind_start_default = (_today - datetime.timedelta(days=90)).strftime("%Y-%m-%d")
_wind_end_default = _today.strftime("%Y-%m-%d")
self._wind_start_var = tk.StringVar(value=_wind_start_default)
ttk.Entry(self._wind_frame, textvariable=self._wind_start_var, width=15).grid(
row=1, column=1, padx=(6, 8), pady=2)
ttk.Label(self._wind_frame, text="结束日:", style="Surface.TLabel").grid(
row=1, column=2, sticky="w", pady=2)
self._wind_end_var = tk.StringVar(value=_wind_end_default)
ttk.Entry(self._wind_frame, textvariable=self._wind_end_var, width=15).grid(
row=1, column=3, padx=(6, 0), pady=2)
# Wind intraday bar_size 选择
ttk.Label(self._wind_frame, text="频率:", style="Surface.TLabel").grid(
row=2, column=0, sticky="w", pady=2)
self._wind_bar_size_var = tk.StringVar(value="日频")
self._wind_bar_size_combo = ttk.Combobox(
self._wind_frame, textvariable=self._wind_bar_size_var, width=10,
values=("日频", "60min", "30min", "15min", "5min", "1min"),
state="readonly",
)
self._wind_bar_size_combo.grid(row=2, column=1, padx=(6, 8),
pady=2, sticky="w")
self._wind_bar_size_combo.bind(
"<<ComboboxSelected>>", lambda e: self._toggle_source())
# 轻分割线
ttk.Separator(sec3, orient="horizontal").grid(
row=2, column=0, columnspan=2, sticky="ew", pady=(8, 6))
# 对冲参数
row_h = 3
ttk.Label(sec3, text="调仓频率(天):", style="Surface.TLabel").grid(
row=row_h, column=0, sticky="w", pady=4)
self._freq_var = tk.StringVar(value="1")
ttk.Entry(sec3, textvariable=self._freq_var, width=10).grid(
row=row_h, column=1, sticky="w", padx=(8, 0), pady=4)
row_h += 1
ttk.Label(sec3, text="交易成本率(%):", style="Surface.TLabel").grid(
row=row_h, column=0, sticky="w", pady=4)
self._tc_var = tk.StringVar(value="0.01")
ttk.Entry(sec3, textvariable=self._tc_var, width=10).grid(
row=row_h, column=1, sticky="w", padx=(8, 0), pady=4)
row_h += 1
ttk.Label(sec3, text="头寸方向:", style="Surface.TLabel").grid(
row=row_h, column=0, sticky="w", pady=4)
self._pos_var = tk.StringVar(value="1")
pos_frame = ttk.Frame(sec3, style="Surface.TFrame")
pos_frame.grid(row=row_h, column=1, sticky="w", padx=(8, 0), pady=4)
ttk.Radiobutton(pos_frame, text="卖出 (short)", variable=self._pos_var,
value="1").pack(side="left", padx=(0, 8))
ttk.Radiobutton(pos_frame, text="买入 (long)", variable=self._pos_var,
value="-1").pack(side="left")
row_h += 1
ttk.Label(sec3, text="交易数量:", style="Surface.TLabel").grid(
row=row_h, column=0, sticky="w", pady=4)
self._qty_var = tk.StringVar(value="100")
ttk.Entry(sec3, textvariable=self._qty_var, width=12).grid(
row=row_h, column=1, sticky="w", padx=(8, 0), pady=4)
row_h += 1
ttk.Label(sec3, text="合约乘数:", style="Surface.TLabel").grid(
row=row_h, column=0, sticky="w", pady=4)
self._mult_var = tk.StringVar(value="5")
mult_frame = ttk.Frame(sec3, style="Surface.TFrame")
mult_frame.grid(row=row_h, column=1, sticky="w", padx=(8, 0), pady=4)
ttk.Entry(mult_frame, textvariable=self._mult_var, width=10).pack(side="left")
ttk.Label(mult_frame, text=" 0=不取整", style="SurfaceMuted.TLabel").pack(side="left")
# 轻分割线:高级对冲参数(策略 / intraday / 滑点)
row_h += 1
ttk.Separator(sec3, orient="horizontal").grid(