Repository navigation
Expand file tree
/
Copy pathconfig.py
More file actions
1057 lines (899 loc) · 51.5 KB
/
Copy pathconfig.py
File metadata and controls
1057 lines (899 loc) · 51.5 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
"""
配置参数管理模块,集中管理所有可配置参数
优化版本:增强止盈止损配置的清晰度
"""
import os
import json
from datetime import datetime, timedelta
# ======================= .env fallback 加载 =======================
def _load_dotenv_fallback(path=None):
"""把项目根 .env 的 KEY=value 载入进程环境,作为环境变量的 fallback。
优先级:Windows 用户级/系统环境变量 > .env。即已存在的环境变量**不覆盖**,
.env 仅补充尚未设置的键。这样 setx 设置的变量始终生效,未 setx 的用 .env 兜底。
解析规则与 scripts/_launcher.py 的 .env 读写保持一致(KEY=value,# 注释行跳过),
额外剥离值两侧成对引号(PowerShell / 手写 .env 常带引号)。
测试隔离:无参(默认路径)调用时,若设了 MINIQMT_DISABLE_DOTENV 则跳过,避免
测试基线随开发者本地 .env 漂移。显式传 path(测试直接调用)不受此开关影响。
"""
explicit = path is not None
if not explicit and os.environ.get("MINIQMT_DISABLE_DOTENV"):
return
if path is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
try:
if not os.path.exists(path):
return
with open(path, "r", encoding="utf-8-sig") as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#") or "=" not
8000
in line:
continue
k, _, v = line.partition("=")
k = k.strip()
v = v.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
v = v[1:-1]
if k and k not in os.environ:
os.environ[k] = v
except Exception:
# .env 解析失败不应阻断启动;缺失的键退回各自的默认值
pass
_load_dotenv_fallback()
# ======================= 系统配置 =======================
# 调试开关
DEBUG = False
DEBUG_SIMU_STOCK_DATA= False
LOG_LEVEL = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_FILE = "qmt_trading.log"
LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB
LOG_BACKUP_COUNT = 5 # 保留5个备份文件
# 持续性状态的重复日志节流间隔(秒):同一股票同一事件在窗口内只输出一次并累计抑制次数。
# 见 logger.log_throttled;针对 T+1 冻结等每轮轮询都命中的状态,避免刷屏喂大终端缓冲。
LOG_THROTTLE_INTERVAL = 300
# 控制台旋转符号刷新间隔(秒)。这是本进程写 stdout 最频繁的来源——0.25 秒时
# 138 小时累计约 199 万次写入,是同期日志行数的 58 倍。Windows Terminal 在高频
# 写入下会无限泄漏内存(microsoft/terminal#8283),2026-09-09 实测其占用 27.8GB
# 打爆系统提交上限,反噬本进程 can't start new thread。
SPINNER_INTERVAL = 1.0
# 控制台输出限速(仅限控制台,文件日志始终完整):令牌桶,稳态速率 + 突发容量。
# 实测稳态日志仅 0.01~0.08 行/秒,启动瞬间峰值约 140 行/秒;因此 burst 需容纳
# 启动峰值,rate 只用于截断异常刷屏(如意外死循环打日志)。
CONSOLE_LOG_RATE = 20.0 # 稳态每秒放行条数
CONSOLE_LOG_BURST = 300 # 突发容量(条),须大于启动期单秒峰值
XQM_LOG_FILE = os.path.join("logs", "xqm_manager.log")
XQM_LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB
XQM_LOG_BACKUP_COUNT = 5 # 保留5个备份文件
# Web访问日志配置
ENABLE_WEB_ACCESS_LOG = True # 是否启用web访问日志
WEB_ACCESS_LOG_LEVEL = "WARNING" # web访问日志级别(DEBUG/INFO/WARNING)
WEB_ACCESS_LOG_EXCLUDE_PATHS = ["/api/sse", "/api/positions/stream", "/<path:filename>"] # 排除的路径(静态文件、SSE长连接等)
WEB_ACCESS_LOG_INCLUDE_TIMING = True # 是否包含请求耗时统计
# 循环日志优化配置
VERBOSE_LOOP_LOGGING = False # 是否输出详细的循环日志(开启后会在DEBUG模式下输出每次循环的开始/结束)
ENABLE_HEARTBEAT_LOG = True # 是否启用系统心跳日志(定期输出运行状态摘要)
HEARTBEAT_INTERVAL = 1800 # 心跳日志间隔(秒,默认1800=30分钟)
# ======================= 功能开关 =======================
def _env_bool(name: str, default: bool) -> bool:
"""从环境变量读 bool 开关;未设置或值非法时返回 default。
用于让 miniqmt.bat / scripts/_launcher.py 在启动时显式控制模式,
避免菜单 [7](实盘)启动后进程默认仍是模拟,需要用户在 web UI
手动切换的尴尬。
"""
v = os.environ.get(name)
if v is None or v == "":
return default
return str(v).strip().lower() in ("1", "true", "yes", "on", "y", "t")
def _env_int(name: str, default: int, min_value=None, max_value=None) -> int:
"""从环境变量读整数;缺失、非法或越界时返回 default。"""
v = os.environ.get(name)
if v is None or v == "":
return default
raw = str(v).strip()
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ("'", '"'):
raw = raw[1:-1]
try:
value = int(raw)
except (TypeError, ValueError):
return default
if min_value is not None and value < min_value:
return default
if max_value is not None and value > max_value:
return default
return value
# 模拟交易模式开关:环境变量 ENABLE_SIMULATION_MODE 优先(启动器控制),
# 否则用源码默认 True(避免开发者本地误连实盘)。
ENABLE_SIMULATION_MODE = _env_bool("ENABLE_SIMULATION_MODE", True)
ENABLE_AUTO_OPERATION = False # 全局自动操作总开关:关闭时自动策略不产生新交易动作
ENABLE_AUTO_TRADING = False # 非网格自动策略执行开关(不影响网格交易)
ENABLE_ALLOW_BUY = True # 是否允许买入操作
ENABLE_ALLOW_SELL = True # 是否允许卖出操作
ENABLE_MACD_SELL = _env_bool("ENABLE_MACD_SELL", False) # MACD技术指标卖出开关:False=满足条件仅记录信号不实盘卖出,True=执行真实卖出
def is_global_monitor_enabled():
"""全局自动操作总开关:关闭时所有自动策略不再产生新交易动作。"""
return bool(ENABLE_AUTO_OPERATION)
# 重要说明:
# - ENABLE_AUTO_OPERATION:全局自动操作总开关,关闭时动态止盈止损和网格交易都不产生新单
# - ENABLE_AUTO_TRADING:控制止盈止损信号的自动执行
# - ENABLE_GRID_TRADING:控制网格交易的检测和执行(独立开关,互不影响)
# - ENABLE_DYNAMIC_STOP_PROFIT:控制止盈止损信号的检测
# baostock 降级配置(防止网络阻塞)
ENABLE_BAOSTOCK_STOCK_NAME_LOOKUP = _env_bool("ENABLE_BAOSTOCK_STOCK_NAME_LOOKUP", False) # 是否允许用baostock兜底查询股票名称
ENABLE_BAOSTOCK_HISTORY_DATA = _env_bool("ENABLE_BAOSTOCK_HISTORY_DATA", False) # 是否允许旧Methods历史行情路径使用baostock
BAOSTOCK_LOGIN_TIMEOUT = 5 # baostock login 超时秒数(防止无超时阻塞)
BAOSTOCK_RETRY_COOLDOWN = 300 # 连续失败后冷却时间(秒,默认5分钟)
BAOSTOCK_MAX_CONSECUTIVE_FAILURES = 3 # 连续失败N次后进入冷却期
# 新版 baostock(0.9.x) 收紧访问后支持的 API Key,登录前通过 set_API_key 传入;
# 为空则匿名访问(免费数据仍可用)。⚠️ 必须用环境变量,切勿硬编码。
BAOSTOCK_API_KEY = os.environ.get("BAOSTOCK_API_KEY", "")
# ============ Tushare Pro 数据源配置 ============
# Tushare 数据源总开关。True = 在标准模式下优先于 Mootdx 使用 Tushare 获取历史数据,
# 并在 get_stock_name 链路中 xtdata 之后、baostock 之前插入 Tushare 查询。
ENABLE_TUSHARE_DATA_SOURCE = _env_bool("ENABLE_TUSHARE_DATA_SOURCE", True)
# Tushare API Token。⚠️ 必须从环境变量读取,切勿硬编码。
# 建议在 .env 文件中设置 TU
8000
SHARE_TOKEN,通过 miniqmt.bat 加载。
TUSHARE_TOKEN = os.environ.get("TUSHARE_TOKEN", "")
# Tushare API 调用超时与冷却配置(与 baostock 机制对称)
TUSHARE_API_TIMEOUT = 10 # Tushare 历史数据 API 超时(秒)
TUSHARE_STOCK_NAME_TIMEOUT = 5 # Tushare 股票名称 API 超时(秒)
TUSHARE_RETRY_COOLDOWN = 300 # 连续失败后冷却时间(秒,默认5分钟)
TUSHARE_MAX_CONSECUTIVE_FAILURES = 3 # 连续失败N次后进入冷却期
# 其他功能开关
ENABLE_DATA_SYNC = True # 是否启用数据同步
ENABLE_POSITION_MONITOR = True # 是否启用持仓监控
ENABLE_LOG_CLEANUP = True # 是否启用日志清理
ENABLE_DB_MAINTENANCE = True # 是否启用数据库维护任务
# QMT API配置
USE_SYNC_ORDER_API = False # 使用同步下单接口(True)还是异步接口(False)
# False: 使用order_stock_async()返回seq号,需要回调映射
# True: 使用order_stock()直接返回order_id
ASYNC_ORDER_ID_WAIT_TIMEOUT_SECONDS = 2.0 # 异步下单后等待 order_id 回推的最长时间
ASYNC_ORDER_ID_WAIT_INTERVAL_SECONDS = 0.1 # 检查 seq->order_id 映射的轮询间隔
ASYNC_ORDER_QUERY_FALLBACK_TIMEOUT_SECONDS = 30.0 # 回推未到时,主动查询委托列表反查 order_id 的最长时间
ASYNC_ORDER_QUERY_FALLBACK_INTERVAL_SECONDS = 0.5 # 主动反查委托列表的轮询间隔
ASYNC_ORDER_QUERY_MATCH_PRE_WINDOW_SECONDS = 5.0 # 允许匹配下单前若干秒内的委托,兼容本机/QMT时间微小偏差
ASYNC_ORDER_QUERY_MATCH_POST_WINDOW_SECONDS = 30.0 # 允许匹配下单后若干秒内的委托,避免误配历史委托
ASYNC_ORDER_UNKNOWN_COOLDOWN_SECONDS = 300 # 正 seq 未确认时,同股同方向暂停再次提交的时间
QMT_ORDER_ID_MAP_TTL_SECONDS = 86400 # seq->order_id 映射保留时长,防止长时间运行后旧seq污染匹配
QMT_ORDER_ID_MAP_MAX_ENTRIES = 4096 # seq->order_id 映射最大键数量(int/str双键计入),防止内存无限增长
QMT_CONNECT_TIMEOUT = 30 # QMT交易接口连接超时(秒),避免connect卡死
QMT_STOP_TIMEOUT = 5.0 # QMT交易接口停止超时(秒),避免旧连接清理卡死
# 注释说明:
# - 策略线程始终运行,进行信号检测和监控
# - ENABLE_AUTO_TRADING 控制是否执行检测到的交易信号
# - ENABLE_DYNAMIC_STOP_PROFIT 控制止盈止损模块
# - ENABLE_GRID_TRADING 控制网格交易模块
# - ENABLE_SIMULATION_MODE 控制交易执行方式(模拟/实盘)
# ======================= 数据配置 =======================
# 历史数据存储路径
DATA_DIR = "data"
# 数据库配置(如果使用SQLite)
DB_PATH = os.path.join(DATA_DIR, "trading.db")
# 行情数据周期
PERIODS = ["1d", "1h", "30m", "15m", "5m", "1m"]
# 默认使用的周期
DEFAULT_PERIOD = "1d"
# 历史数据初始获取天数
INITIAL_DAYS = 365
# 定时更新间隔(秒)
UPDATE_INTERVAL = 60
# 单只股票历史数据更新节流时间(秒),避免策略线程每轮重复拉日线
HISTORY_UPDATE_THROTTLE_SECONDS = 300
# 同一股票同一数据源非法历史日期告警降噪间隔(秒)
HISTORY_INVALID_DATE_LOG_INTERVAL = 600
# 盘中今日日线通常未稳定落库,默认收盘后一段时间才把今天作为历史日线截止日
HISTORY_TODAY_DAILY_AVAILABLE_AFTER = "15:30:00"
# 历史数据为空/指标数据为空时的告警降噪间隔(秒)
HISTORY_NO_DATA_LOG_INTERVAL_SECONDS = 300
INDICATOR_EMPTY_DATA_LOG_INTERVAL_SECONDS = 300
# 备选池股票文件路径
STOCK2BUY_FILE = os.path.join(DATA_DIR, "stock2buy.json")
# 实时数据源配置
REALTIME_DATA_CONFIG = {
'enable_multi_source': True,
'health_check_interval': 30,
'source_timeout': 5,
'max_error_count': 3,
'preferred_sources': [
'XtQuant',
'Mootdx'
]
}
# ======================= 交易配置 =======================
# 交易账号信息(从外部文件读取,避免敏感信息硬编码)
ACCOUNT_CONFIG_FILE = "account_config.json"
# QMT路径配置优先级:
# 1. 环境变量 QMT_PATH
# 2. account_config.json 中的 qmt_path 字段
# 3. 默认值(自动检测常见路径)
DEFAULT_QMT_PATHS = [
r'C:/QMT/userdata_mini',
r'C:/QMT1/userdata_mini',
r'C:/QMT2/userdata_mini',
r'C:/光大证券金阳光QMT实盘/userdata_mini',
r'D:/QMT/userdata_mini',
r'D:/QMT1/userdata_mini',
r'D:/QMT2/userdata_mini',
r'D:/光大证券金阳光QMT实盘/userdata_mini',
]
def _read_raw_account_config():
"""读取原始 account_config.json 文件内容(无任何账号选择逻辑)。"""
try:
with open(ACCOUNT_CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
return {"account_id": "", "account_type": "STOCK"}
except Exception as e:
print(f"读取账户配置文件失败: {str(e)}")
return {"account_id": "", "account_type": "STOCK"}
def get_account_config():
"""
返回"当前进程"实际使用的账号配置。
选择规则(向后兼容):
- 设置了环境变量 QMT_ACCOUNT_ID 且能在 accounts 列表中找到匹配项 → 返回该子配置;
- 否则 → 返回 account_config.json 顶层字段(与改动前完全一致)。
返回结构始终保留顶层的 accounts 字段,便于下游访问全量列表。
"""
raw = _read_raw_account_config()
target_id = os.environ.get('QMT_ACCOUNT_ID', '').strip()
if target_id:
for acc in (raw.get
8BD0
("accounts") or []):
if acc.get("account_id") == target_id:
# 合并:以子账号字段为主,缺失字段用顶层兜底
merged = {k: v for k, v in raw.items() if k != "accounts"}
merged.update({k: v for k, v in acc.items() if v is not None})
merged["accounts"] = raw.get("accounts", [])
return merged
return raw
def get_all_accounts_config():
"""
返回所有账号配置列表,支持单账号和多账号两种格式。
格式 1(单账号,向后兼容):
{"account_id": "xxx", "account_type": "STOCK", "qmt_path": "..."}
→ 返回 [{"account_id": "xxx", ...}]
格式 2(多账号):
{"account_id": "xxx", ..., "accounts": [{"account_id": "aaa", ...}, ...]}
→ 返回 accounts 列表
Returns:
list[dict]: 账号配置列表,至少包含一个元素
"""
raw = _read_raw_account_config()
accounts = raw.get("accounts")
if accounts and isinstance(accounts, list):
valid = [a for a in accounts if a.get("account_id")]
if valid:
return valid
# 兜底:单账号模式(剥离 accounts 字段,避免污染)
fallback = {k: v for k, v in raw.items() if k != "accounts"}
return [fallback]
def get_qmt_path():
"""
获取QMT路径 - 灵活配置方案
优先级:
1. 环境变量 QMT_PATH
2. account_config.json 中的 qmt_path 字段
3. 自动检测常见路径(存在性检查)
4. 使用第一个默认路径(兜底)
Returns:
str: QMT userdata_mini 路径
"""
import os
# 优先级1: 环境变量
env_path = os.environ.get('QMT_PATH')
if env_path and os.path.exists(env_path):
print(f"[QMT_PATH] Using environment variable: {env_path}")
return env_path
# 优先级2: 配置文件中的qmt_path字段
account_config = get_account_config()
config_path = account_config.get('qmt_path')
if config_path and os.path.exists(config_path):
print(f"[QMT_PATH] Using path from account_config.json: {config_path}")
return config_path
# 优先级3: 自动检测常见路径
for path in DEFAULT_QMT_PATHS:
if os.path.exists(path):
print(f"[QMT_PATH] Auto-detected path: {path}")
return path
# 优先级4: 兜底,返回第一个默认路径(即使不存在,后续会有错误提示)
fallback_path = DEFAULT_QMT_PATHS[0]
print(f"[QMT_PATH] WARNING: QMT path not found, using default: {fallback_path}")
print(f"[QMT_PATH] TIP: If QMT is installed elsewhere, set 'qmt_path' in account_config.json")
return fallback_path
# 动态获取QMT路径
QMT_PATH = get_qmt_path()
# 账号信息
ACCOUNT_CONFIG = get_account_config()
# ======================= XtQuantManager 集成开关 =======================
# False(默认): 直接使用本地 xtquant 接口(现有行为不变)
# True: 所有 xtquant 调用通过 XtQuantManager HTTP 服务路由
ENABLE_XTQUANT_MANAGER = False
# XtQuantManager 服务地址(ENABLE_XTQUANT_MANAGER=True 时生效)
XTQUANT_MANAGER_PORT = _env_int("XQM_PORT", 8888, 1, 65535)
XTQUANT_MANAGER_URL = os.environ.get("XTQUANT_MANAGER_URL") or f"http://127.0.0.1:{XTQUANT_MANAGER_PORT}"
# XtQuantManager API Token(空字符串表示不验证)
XTQUANT_MANAGER_TOKEN = ""
# XtQuantManager HTTP 服务速率限制(次/分钟,0=不限速)
# 默认 600:持仓监控每3秒×3只股票=60次/分钟,HealthMonitor+资产查询约30次/分钟,
# 合计约90次/分钟;设600留足6倍余量,避免 HTTP 429 错误。
XTQUANT_MANAGER_RATE_LIMIT = 600
# ======================= 大QMT文件IPC Fallback 集成开关 =======================
# xttrader 降级替代方案:当券商收紧 miniQMT 权限或 xttrader 失效时,
# 通过文件系统 IPC 把订单交给大QMT内置Python执行(大QMT自带xttrader授权)。
# 详见 qmt-trader/大QMT文件IPC方案.md
# False(默认): 使用 xttrader 直连(现有行为不变)
# True: 所有交易操作通过文件 IPC 路由到大QMT执行
# ⚠️ 与 ENABLE_XTQUANT_MANAGER 互斥,ENABLE_XTQUANT_MANAGER=True 时此开关不生效
ENABLE_QMT_IPC_FALLBACK = _env_bool("ENABLE_QMT_IPC_FALLBACK", False)
# IPC 文件目录(策略端与QMT端必须指向同一路径;跨机器时用网络共享路径)
QMT_IPC_ROOT = os.environ.get("QMT_IPC_ROOT", r"C:\QuantIPC")
# IPC 可选共享密钥(为空时保持本地信任模型;非空时 executor 校验 pending 订单)
QMT_IPC_SECRET = os.environ.get("QMT_IPC_SECRET", "")
# 下单后等待成交回执的最大秒数
QMT_IPC_ORDER_TIMEOUT = 30
# 心跳文件最大允许间隔(秒),超过此值判定大QMT离线
QMT_IPC_HEARTBEAT_MAX_AGE = 10
# 成交回报轮询线程扫描 done/ 目录的间隔(秒)
QMT_IPC_DEAL_POLL_INTERVAL = 1.0
# done/ 目录回执聚合查询的回溯窗口(秒),默认24小时
QMT_IPC_DONE_LOOKBACK_SECONDS = 86400
# ======================= 大QMT RPC Fallback(xtquant_big_convert)=======================
# xttrader 降级替代方案:通过 Redis/ZMQ/MySQL RPC 远程驱动大QMT内置Python执行交易。
# 与文件IPC解决同一问题,但通道从文件轮询升级为 RPC(毫秒级、可跨机、含行情能力)。
# 详见 qmt-trader/大QMT-RPC方案.md
# False(默认): 不启用(现有行为不变)
# True: 所有交易操作通过 RPC 路由到大QMT执行
# ⚠️ 与 ENABLE_XTQUANT_MANAGER、ENABLE_QMT_IPC_FALLBACK 三者互斥
ENABLE_QMT_RPC_FALLBACK = _env_bool("ENABLE_QMT_RPC_FALLBACK", False)
# 传输方式:redis(默认,生产推荐)/ zmq(同机低延迟)/ mysql(兜底)
QMT_RPC_TRANSPORT = os.environ.get("QMT_RPC_TRANSPORT", "redis")
# Redis 连接配置(密码走环境变量,切勿硬编码)
QMT_RPC_REDIS = {
"host": os.environ.get("QMT_RPC_REDIS_HOST", "127.0.0.1"),
"port": int(os.environ.get("QMT_RPC_REDIS_PORT", "6379")),
"db": int(os.environ.get("QMT_RPC_REDIS_DB", "5")),
"password": os.environ.get("
10000
;QMT_RPC_REDIS_PASSWORD", ""),
}
# RPC 请求超时(秒)
QMT_RPC_TIMEOUT_SECONDS = 6.0
# 下单后等待成交/委托回执的最大秒数
QMT_RPC_ORDER_TIMEOUT = 30
# 成交/委托回报轮询兜底间隔(秒),推送通道之外的补偿轮询
QMT_RPC_DEAL_POLL_INTERVAL = 1.0
# 下单二次确认开关:False 时即使 ENABLE_QMT_RPC_FALLBACK=True 也拒绝真实下单(只读安全)
QMT_RPC_ALLOW_ORDER = _env_bool("QMT_RPC_ALLOW_ORDER", False)
# ======================= 策略配置 =======================
# 仓位管理
POSITION_UNIT = 35000 # 每次买入金额
MAX_POSITION_VALUE = 70000 # 单只股票最大持仓金额
MAX_TOTAL_POSITION_RATIO = 0.95 # 最大总持仓比例(占总资金)
SIMULATION_BALANCE = 1000000 # 模拟持仓
# ======================= 补仓策略配置(止盈止损策略专用) =======================
# 说明:此配置仅用于动态止盈止损策略的补仓功能,与网格交易策略无关
BUY_GRID_LEVELS = [1.0, 0.93, 0.88] # 建仓价格网格(第一个是初次建仓价格比例,后面是补仓价格比例)
# 说明:
# - BUY_GRID_LEVELS[0] = 1.0:初次建仓价格比例(100%)
# - BUY_GRID_LEVELS[1] = 0.93:首次补仓阈值,当前价格跌至成本价的93%时触发补仓(下跌7%)
# - BUY_GRID_LEVELS[2] = 0.88:第二次补仓阈值,当前价格跌至成本价的88%时触发补仓(下跌12%)
#
# 补仓金额说明:
# - 补仓策略固定使用 POSITION_UNIT(35000元)作为补仓金额
# - 补仓金额会受 MAX_POSITION_VALUE 限制,剩余空间不足时补仓金额自动调整
# - 不再使用 BUY_AMOUNT_RATIO 比例,避免配置冗余
# ⚠️ 已废弃:BUY_AMOUNT_RATIO(保留仅为向后兼容)
# 此参数仅被网格交易策略使用,补仓策略不使用此参数
# 建议使用新的网格交易配置参数:GRID_PRICE_LEVELS 和 GRID_AMOUNT_RATIOS
BUY_AMOUNT_RATIO = [0.4, 0.3, 0.3] # 每次买入金额占单元的比例(已废弃,保留向后兼容)
# ======================= 网格交易策略配置(独立配置) =======================
# ===== 旧的网格交易配置已废弃 =====
# GRID_PRICE_LEVELS, GRID_AMOUNT_RATIOS, get_grid_config()
# 已被config.py末尾的新网格交易配置替代(第504-547行)
# ======================= 止盈止损策略配置 =======================
# 补仓功能开关
ENABLE_STOP_LOSS_BUY = True # 是否启用止损补仓功能
# 统一的止损比例
STOP_LOSS_RATIO = -0.075 # 固定止损比例:成本价下跌7.5%触发止损
# 动态止盈配置
ENABLE_DYNAMIC_STOP_PROFIT = True # 启用动态止盈功能
INITIAL_TAKE_PROFIT_RATIO = 0.06 # 首次止盈触发阈值:盈利6%时触发
INITIAL_TAKE_PROFIT_PULLBACK_RATIO = 0.005 # 回撤比例:0.5%(可配置)
INITIAL_TAKE_PROFIT_RATIO_PERCENTAGE = 0.6 # 首次止盈卖出比例
# 动态止盈止损信号保活(防止瞬时信号在被策略线程消费前丢失)
# 背景:监控线程每 MONITOR_LOOP_INTERVAL(3秒) 检测一次,而策略线程单只股票的实际
# 消费周期约 10 + 持仓数 + 股票池数 秒。首次止盈是"跨过即触发"的瞬时信号,
# 若价格随后回踩,原实现会在下一轮检测直接把已入队信号删除,导致该卖的单子整个消失。
# 开启后:已入队但未被消费的动态信号在保活窗口内不被"无信号"覆盖删除。
# 注意窗口不宜过长——执行时使用的是信号生成时的价格快照,过旧的信号会以偏离的价格下单,
# 故由 DYNAMIC_SIGNAL_MAX_AGE_SECONDS 在执行前做最终时效兜底。
ENABLE_DYNAMIC_SIGNAL_KEEPALIVE = True # 是否启用动态信号保活
DYNAMIC_SIGNAL_KEEPALIVE_SECONDS = 90 # 保活窗口(秒):期间不因"当前无信号"而删除
DYNAMIC_SIGNAL_MAX_AGE_SECONDS = 120 # 执行前信号最大年龄(秒),超过则判定过期不执行
# 🔑 委托单超时管理配置
ENABLE_PENDING_ORDER_AUTO_CANCEL = True # 是否启用委托单超时自动撤单
PENDING_ORDER_TIMEOUT_MINUTES = 5 # 委托单超时时间(分钟),默认5分钟
STOP_LOSS_PENDING_ORDER_TIMEOUT_MINUTES = 0.5 # 止损委托单超时时间(分钟),30秒后触发撤单流程
TAKE_PROFIT_PENDING_ORDER_TIMEOUT_MINUTES = 0.5 # 止盈委托单超时时间(分钟),与止损对齐为30秒
PENDING_ORDER_AUTO_REORDER = True # 撤单后是否自动重新挂单
PENDING_ORDER_REORDER_PRICE_MODE = "best" # 重新挂单价格模式: "market"=市价, "limit"=限价, "best"=对手价
MAX_CANCEL_RETRIES = 3 # 撤单最大重试次数
CANCEL_RETRY_INTERVAL_SECONDS = 1 # 撤单重试间隔(秒)
# ========== 行情异常兜底(风险保护) ==========
ENABLE_MARKET_DATA_CIRCUIT_BREAKER = True # 是否启用行情熔断
MARKET_DATA_FAILURE_THRESHOLD = 3 # 连续失败次数阈值
MARKET_DATA_FAILURE_WINDOW_SECONDS = 60 # 统计窗口(秒)
MARKET_DATA_CIRCUIT_BREAK_SECONDS = 300 # 熔断时长(秒)
# ========== 行情源健康评分(第一阶段:内存观察,不落库) ==========
MARKET_HEALTH_ENABLED = True
MARKET_HEALTH_OBSERVE_ONLY = False
MARKET_HEALTH_WINDOW_SECONDS = 300
MARKET_HEALTH_MAX_EVENTS = 100
MARKET_HEALTH_MIN_EVENTS = 3
MARKET_HEALTH_HEALTHY_SCORE = 80
MARKET_HEALTH_DEGRADED_SCORE = 60
MARKET_HEALTH_UNSTABLE_SCORE = 40
MARKET_HEALTH_TRADING_MIN_SCORE = 70
MARKET_HEALTH_ALLOW_MOOTDX_FOR_TRADING = False
# ========== 全仓止盈与委托冲突控制 ==========
ALLOW_TAKE_PROFIT_FULL_WITH_PENDING = False # 全仓止盈是否允许跳过活跃委托检查
ENABLE_PAUSE_GRID_AFTER_TAKE_PROFIT_FULL = _env_bool("ENABLE_PAUSE_GRID_AFTER_TAKE_PROFIT_FULL", True) # 全仓止盈/止损成交后暂停该股网格会话(仅置 enabled=False,不改网格配置)
# 说明:
# - 当止盈止损委托单提交后超过指定时间仍未成交时:
# 1. ENABLE_PENDING_ORDER_AUTO_CANCEL=True: 自动撤销旧委托单
# 2. PENDING_ORDER_AUTO_REORDER=True: 撤单后自动以新价格重新挂单
# 3. 价格模式:
# - "market": 以当前市价(最新成交价)挂单
# - "limit": 以原价格挂单(适用于价格回调情况)
# - "best": 以对手方最优价格挂单(买单用卖三价,卖单用买三价)
# - ENABLE_PENDING_ORDER_AUTO_CANCEL=False: 仅提示用户,不自动处理
# 分级动态止盈设置(已触发首次止盈后的动态止盈位)
# 格式:(最高盈利比例阈值, 止盈位系数)
# 说明:当最高盈利达到阈值后,止盈位 = 最高价 × 系数
DYNAMIC_TAKE_PROFIT = [
(0.05, 0.96), # 最高浮盈达5%时,止盈位为最高价的96%
(0.10, 0.93), # 最高浮盈达10%时,止盈位为最高价的93%
(0.15, 0.90), # 最高浮盈达15%时,止盈位为最高价的90%
(0.20, 0.87), # 最高浮盈达20%时,止盈位为最高价的87%
(0.30, 0.85), # 最高浮盈达30%时,止盈位为最高价的85%
(0.40, 0.83), # 最高浮盈达40%时,止盈位为最高价的83%
(0.50, 0.80) # 最高浮盈达50%时,止盈位为最高价的80%
]
# 止盈止损优先级说明:
# 1. 止损检查优先级最高
# 2. 未触发首次止盈时:盈利5%触发首次止盈(卖出50%)
# 3. 已触发首次止盈后:使用动态止盈位进行全仓止盈
# 4. 止损价格计算:未触发首次止盈时为成本价×(1-7%),已触发后为最高价×对应系数
# ===== 旧的网格交易参数已废弃 =====
# ENABLE_GRID_TRADING, GRID_DEFAULT_PRICE_INTERVAL, GRID_DEFAULT_POSITION_RATIO
# 已被config.py末尾的新网格交易配置替代(第504-547行)
# ======================= 指标配置 =======================
# MACD参数
MACD_FAST = 12
MACD_SLOW = 26
MACD_SIGNAL = 9
# 均线参数
MA_PERIODS = [10, 20, 30, 60]
# MACD 操盘建议(悬浮窗)配置
SHENZHEN_INDEX_CODE = "399001.SZ" # 账号级建议使用的深证成指代码
MACD_ADVICE_CACHE_TTL = 300 # 建议结果缓存时长(秒),日线日内基本不变
# ======================= 参数配置范围 =======================
# 参数范围定义,用于前后端校验
CONFIG_PARAM_RANGES = {
"singleBuyAmount": {"min": 1000, "max": 100000, "type": "float", "desc": "单只单次买入金额"},
"firstProfitSell": {"min": 1.0, "max": 20.0, "type": "float", "desc": "首次止盈比例(%)"},
"stockGainSellPencent": {"min": 1.0, "max": 100.0, "type": "float", "desc": "首次盈利平仓卖出比例(%)"},
"stopLossBuy": {"min": 1.0, "max": 20.0, "type": "float", "desc": "补仓跌幅(%)"},
"stockStopLoss": {"min": 1.0, "max": 20.0, "type": "float", "desc": "止损比例(%)"},
"singleStockMaxPosition": {"min": 10000, "max": 100000, "type": "float", "desc": "单只股票最大持仓"},
"totalMaxPosition": {"min": 50000, "max": 1000000, "type": "float", "desc": "最大总持仓"},
"connectPort": {"min": 1, "max": 65535, "type": "int", "desc": "连接端口"}
}
# 实现参数校验函数
def validate_config_param(param_name, value):
"""验证配置参数是否在有效范围内"""
if param_name not in CONFIG_PARAM_RANGES:
return True, "" # 未定义范围的参数默认通过
param_range = CONFIG_PARAM_RANGES[param_name]
param_type = param_range.get("type", "float")
param_min = param_range.get("min")
param_max = param_range.get("max")
try:
# 类型转换
if param_type == "float":
value = float(value)
elif param_type == "int":
value = int(value)
# 范围检查
if param_min is not None and value < param_min:
return False, f"{param_range['desc']}不能小于{param_min}"
if param_max is not None and value > param_max:
return False, f"{param_range['desc']}不能大于{param_max}"
return True, ""
except (ValueError, TypeError):
return False, f"{param_range['desc']}必须是{param_type}类型"
# ======================= Web服务配置 =======================
WEB_SERVER_HOST = "127.0.0.1"
WEB_SERVER_BASE_PORT = _env_int("WEB_SERVER_PORT", 5000, 1, 65535)
WEB_SERVER_PORT = WEB_SERVER_BASE_PORT
WEB_SERVER_DEBUG = False
WEB_API_TOKEN = os.environ.get("QMT_API_TOKEN", "") # 设置后启用 Token 验证,空字符串=不验证(仅限内网部署)
WEB_PUBLIC_MODE = _env_bool("WEB_PUBLIC_MODE", False) # 公网映射/反代模式:Token 为空时也拒绝 /api/*(fail-closed)
WEB_TRADE_RECORDS_DISPLAY_DAYS = 60 # 下单日志仅展示最近N天(约2个月)的交易记录,避免清单过长
# ======================= 日志清理配置 =======================
LOG_CLEANUP_DAYS = 30 # 保留最近30天的日志
LOG_CLEANUP_TIME = "00:00:00" # 每天凌晨执行清理
# ======================= 数据库维护配置 =======================
DB_MAINTENANCE_TIME = "00:10:00" # 每天非交易时段执行维护
DB_MAINTENANCE_CHECK_INTERVAL = 600 # 调度检查间隔(秒)
DB_MAINTENANCE_REQUIRE_NON_TRADE_TIME = True
DB_MAINTENANCE_ENABLE_VACUUM = True
DB_MAINTENANCE_VACUUM_MIN_DELETED_ROWS = 1000
TRADE_RECORD_RETENTION_DAYS = 1095 # 交易流水保留3年
GRID_SESSION_RETENTION_DAYS = 365 # 已停止网格会话保留1年
AUTOBUY_DECISION_LOG_RETENTION_DAYS = 90 # 自动买入决策明细保留90天
PREMARKET_HISTORY_RETENTION_DAYS = 365 # 盘前同步历史保留1年
CONFIG_HISTORY_RETENTION_DAYS = 365 # 配置变更审计保留1年
# ======================= 功能配置 =======================
# 可下单时间配置:实盘包含 09:25-09:30 早盘预挂和 11:30-13:00 午休预挂。
# DEBUG模式下使用24小时全周交易,方便测试。
if DEBUG:
TRADE_TIME = {
"morning_start": "00:00:00",
"morning_end": "23:59:59",
"afternoon_start": "00:00:00",
"afternoon_end": "23:59:59",
"trade_days": [1, 2, 3, 4, 5, 6, 7] # 周一至周日
}
else:
TRADE_TIME = {
"morning_start": "09:25:00",
"morning_end": "13:00:00",
"afternoon_start": "13:00:00",
"afternoon_end": "15:00:00",
"trade_days": [1, 2, 3, 4, 5] # 周一至周五
}
# 连续竞价时间独立于预挂和调试开关,用于委托超时计时及严格市场时段判断。
CONTINUOUS_TRADE_TIME = {
"morning_start": "09:30:00",
"morning_end": "11:30:00&
10BC8
quot;,
"afternoon_start": "13:00:00",
"afternoon_end": "15:00:00",
"trade_days": [1, 2, 3, 4, 5]
}
# ============ 新增: 盘前同步配置 ============
PREMARKET_SYNC_TIME = {
"hour": 9, # 同步时间: 9点
"minute": 25, # 25分(9:30开盘前5分钟)
"compensation_window_minutes": 5 # 补偿窗口: 9:25-9:30
}
# 盘前同步功能开关
ENABLE_PREMARKET_XTQUANT_REINIT = True # 是否在盘前重新初始化xtquant接口
PREMARKET_REINIT_XTDATA = True # 是否重新初始化xtdata行情接口
PREMARKET_REINIT_XTTRADER = True # 是否重新初始化xttrader交易接口
ENABLE_WEB_REFRESH_AFTER_REINIT = True # 接口初始化成功后是否触发Web数据刷新
# ============ 交割单数据落库配置 ============
# 归因所需的持仓快照与每日净值。positions 表是"当前持仓"会被覆盖写,
# 不能当历史用;对账基准只能取 position_snapshot。
ENABLE_SETTLEMENT_SNAPSHOT = True # 持仓快照与净值落库总开关
# 09:25 开盘快照挂在盘前同步步骤链末尾(复用 PREMARKET_SYNC_TIME,不另设时间)
SETTLEMENT_CLOSE_SNAPSHOT_TIME = "15:05:00" # 收盘快照时间(收盘后5分钟,等待结算数据稳定)
SETTLEMENT_SNAPSHOT_CHECK_INTERVAL = 300 # 收盘任务轮询间隔(秒)
SETTLEMENT_SNAPSHOT_HEALTH_LOOKBACK = 7 # 快照完整性回溯天数
# snapshot_type 固定为 open(09:25)/close(15:05) 两种,不再有 intraday。
# 恒等式 total_asset = cash + frozen_cash + market_value 的容差(元)。
# 差额说明账户含非股票资产项(逆回购/理财/未交收资金),归因时必须排除。
SETTLEMENT_ASSET_IDENTITY_TOLERANCE = 1.0
# 资产跳变告警阈值:超过 max(总资产*比例, 绝对值) 即告警。
# QMT 无任何出入金查询接口,跳变无法区分盈亏与银证转账,只能标记待人工判断。
SETTLEMENT_ASSET_JUMP_RATIO = 0.02
SETTLEMENT_ASSET_JUMP_ABSOLUTE = 5000.0
# 手续费估算费率(现行 A 股税费,用于数据库无真实手续费时的回填)。
# QMT 的 XtTrade 结构体没有手续费字段,成交回报路径拿不到真实值,
# 只能按费率估算并标 commission_source='estimated',导入券商对账单后改 'broker'。
SETTLEMENT_COMMISSION_RATE = 0.0003 # 佣金 0.03%(买卖双边)
SETTLEMENT_STAMP_DUTY_RATE = 0.0005 # 印花税 0.05%(仅卖出方缴纳)
SETTLEMENT_TRANSFER_FEE_RATE = 0.00001 # 过户费 0.001%(买卖双边)
# ============ xtquant接口鲁棒性配置 ============
XTQUANT_RECONNECT_INTERVAL = 30 # xtquant重连冷却间隔(秒),防止重连风暴
# 改为30秒:QMT重启约需60秒,30秒冷却可在1-2分钟内完成恢复
# on_disconnected触发时会重置此计时,无需担心跨轮次残余冷却
XTQUANT_CALL_TIMEOUT = 3.0 # xtquant默认调用超时(秒)
XTQUANT_NON_TRADE_TIMEOUT = 1.0 # 非交易时段超时(秒)
QMT_RECONNECT_ON_ERRORS = 3 # 连续N次持仓查询失败后触发自动重连
# ============ 线程监控配置 ============
ENABLE_THREAD_MONITOR = True # 启用线程健康监控
THREAD_CHECK_INTERVAL = 60 # 线程检查间隔(秒)
THREAD_RESTART_COOLDOWN = 60 # 重启冷却时间(秒)
# ============ 持仓监控优化配置 ============
MONITOR_LOOP_INTERVAL = 3 # 监控循环间隔(秒)
MONITOR_CALL_TIMEOUT = 8.0 # 监控调用超时(秒) - 增加到8秒,避免QMT API调用超时
MONITOR_NON_TRADE_SLEEP = 60 # 非交易时段休眠(秒)
GRID_POSITION_QUERY_TIMEOUT = 5.0 # 网格交易持仓查询超时(秒)
HISTORY_DATA_DOWNLOAD_TIMEOUT = 5 # 启动时单只股票历史数据下载超时(秒),超时则跳过
GRID_LOCK_ACQUIRE_TIMEOUT = 5.0 # 网格交易锁获取超时(秒)
def _is_in_trade_schedule(now, schedule):
weekday = now.weekday() + 1
if weekday not in schedule["trade_days"]:
return False
current_time = now.strftime("%H:%M:%S")
return (schedule["morning_start"] <= current_time <= schedule["morning_end"]) or \
(schedule["afternoon_start"] <= current_time <= schedule["afternoon_end"])
def is_continuous_trade_time(now=None):
"""判断是否处于真实连续竞价时段,不受模拟或调试开关影响。"""
return _is_in_trade_schedule(now or datetime.now(), CONTINUOUS_TRADE_TIME)
def is_market_hours(now=None):
"""按真实市场时钟判断是否交易时段,不受模拟/调试开关影响。
用于需要严格遵循真实交易时段的场景(如 autobuy 定时筛选),区别于
is_trade_time() 在模拟/调试模式下恒为 True 的旁路语义。
"""
return is_continuous_trade_time(now)
def is_trade_time(now=None):
"""判断当前是否允许交易和下单,包含早盘、午休预挂窗口。"""
if DEBUG_SIMU_STOCK_DATA or ENABLE_SIMULATION_MODE:
return True
return _is_in_trade_schedule(now or datetime.now(), TRADE_TIME)
def get_continuous_trading_seconds(start_time, end_time=None):
"""计算两个时间点之间落在连续竞价窗口内的有效秒数。"""
end_time = end_time or datetime.now()
if end_time <= start_time:
return 0
total_seconds = 0.0
current_date = start_time.date()
end_date = end_time.date()
windows = (
(CONTINUOUS_TRADE_TIME["morning_start"], CONTINUOUS_TRADE_TIME["morning_end"]),
(CONTINUOUS_TRADE_TIME["afternoon_start"], CONTINUOUS_TRADE_TIME["afternoon_end"]),
)
while current_date <= end_date:
if current_date.weekday() + 1 in CONTINUOUS_TRADE_TIME["trade_days"]:
for window_start, window_end in windows:
start_clock = datetime.strptime(window_start, "%H:%M:%S").time()
end_clock = datetime.strptime(window_end, "%H:%M:%S").time()
window_start_at = datetime.combine(current_date, start_clock)
window_end_at = datetime.combine(current_date, end_clock)
overlap_start = max(start_time, window_start_at)
overlap_end = min(end_time, window_end_at)
if overlap_end > overlap_start:
total_seconds += (overlap_end - overlap_start).total_seconds()
current_date += timedelta(days=1)
return total_seconds
# ======================= 预设股票池 =======================
# 可以在这里定义预设的股票池,也可以从外部文件加载
DEFAULT_STOCK_POOL = [
"000001.SZ", # 平安银行
"600036.SH", # 招商银行
"000333.SZ", # 美的集团
"600519.SH", # 贵州茅台
"000858.SZ", # 五粮液
]
STOCK_POOL_FILE = "stock_pool.json"
def load_stock_pool(file_path=STOCK_POOL_FILE):
"""从外部文件加载股票池"""
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
return DEFAULT_STOCK_POOL
# 实际使用的股票池
STOCK_POOL = load_stock_pool()
# ======================= 动态优先级决策函数 =======================
def determine_stop_loss_add_position_priority():
"""
根据配置参数动态确定止损和补仓的优先级顺序
设计逻辑:
- 场景A: 补仓阈值 < 止损阈值 → 先补仓,达到仓位上限后再止损
- 场景B: 止损阈值 < 补仓阈值 → 先止损,永不补仓
返回:
dict: {
'priority': str, # 'add_position_first' 或 'stop_loss_first'
'add_position_threshold': float, # 补仓阈值(正数,如0.05表示5%)
'stop_loss_threshold': float, # 止损阈值(正数,如0.07表示7%)
'scenario': str # 'A' 或 'B'
}
"""
# 计算补仓阈值:1 - BUY_GRID_LEVELS[1]
# 例如: BUY_GRID_LEVELS[1]=0.95 → 补仓阈值=1-0.95=0.05 (5%)
add_position_threshold = 1 - BUY_GRID_LEVELS[1]
# 计算止损阈值:abs(STOP_LOSS_RATIO)
# 例如: STOP_LOSS_RATIO=-0.07 → 止损阈值=0.07 (7%)
stop_loss_threshold = abs(STOP_LOSS_RATIO)
# 动态判断优先级
if add_position_threshold < stop_loss_threshold:
# 场景A: 补仓5% < 止损7% → 先补仓后止损
return {
'priority': 'add_position_first',
'add_position_threshold': add_position_threshold,
'stop_loss_threshold': stop_loss_threshold,
'scenario': 'A',
'description': f'补仓{add_position_threshold*100:.0f}% < 止损{stop_loss_threshold*100:.0f}% → 先补仓(至仓位上限)后止损'
}
else:
# 场景B: 止损5% <= 补仓7% → 先止损,永不补仓
return {
'priority': 'stop_loss_first',
'add_position_threshold': add_position_threshold,
'stop_loss_threshold': stop_loss_threshold,
'scenario': 'B',
'description': f'止损{stop_loss_threshold*100:.0f}% <= 补仓{add_position_threshold*100:.0f}% → 先止损,永不补仓'
}
def log_priority_scenario():
"""记录当前优先级场景信息(用于系统启动时打印)"""
priority_info = determine_stop_loss_add_position_priority()
scenario = priority_info['scenario']
description = priority_info['description']
print(f"\n{'='*60}")
print(f"动态优先级止损补仓系统 - 场景{scenario}")
print(f"{'='*60}")
print(f"补仓阈值: {priority_info['add_position_threshold']*100:.1f}%")
print(f"止损阈值: {priority_info['stop_loss_threshold']*100:.1f}%")
print(f"执行策略: {description}")
print(f"{'='*60}\n")
return priority_info
# ============================================================
# 性能优化配置 - 2025-12-12
# ============================================================
# QMT持仓查询间隔(秒) - 从3秒延长到10秒
QMT_POSITION_QUERY_INTERVAL = 10.0 # ↓70% API调用
# 实盘空持仓确认次数。只有在交易时段、QMT已连接、连续N次返回空持仓、
# 且无活跃委托时,才允许清空本地持仓,避免非交易时段QMT空返回误删。
REAL_POSITION_EMPTY_CONFIRM_COUNT = 3
# SQLite同步间隔(秒) - 从5秒延长到15秒
POSITION_SYNC_INTERVAL = 15.0 # ↓87% I/O操作
# SQLite同步失败后的重试策略。
# 2026-09-14 实盘故障:写锁竞争时重试上限失效,41分钟刷了1586轮重试、
# 派生同等数量 Timer 线程(线程数 18→41)。上限必须真实生效。
POSITION_SYNC_MAX_RETRY = 2 # 单次故障内最多重试次数
POSITION_SYNC_RETRY_DELAY = 5.0 # 重试延迟(秒)
# 同步连接等待写锁的超时(毫秒)。原硬编码 30 秒,远超 15 秒的同步周期,
# 锁竞争时单轮同步要卡满 30 秒才失败,监控循环随之被拖慢(实盘曾告警 31.02 秒)。
POSITION_SYNC_BUSY_TIMEOUT_MS = 8000
# 清仓残留持仓成本价告警限频(秒)。券商 miniQMT 在非交易时段可能仍返回已清仓行。
# 0 表示不限制频率;默认 30 分钟只告警一次,其余降为 DEBUG。
CLEARED_POSITION_WARNING_INTERVAL = 1800
# 缓存配置
CACHE_CONFIG = {
'positions_ttl': 5.0, # 持仓数据缓存5秒
'quotes_ttl': 3.0, # 行情数据缓存3秒
'max_cache_size': 100 # 最大缓存条目
}
# HTTP API节流
HTTP_API_MIN_INTERVAL = 1.0 # 最小请求间隔1秒
# HTTP版本号机制(用于减少无效数据传输)
ENABLE_HTTP_VERSION_CONTROL = True # 是否启用版本号机制
# 版本号定期升级间隔(秒) - 用于Web界面定期刷新
VERSION_INCREMENT_INTERVAL = 15.0 # 版本心跳间隔(秒)
ENABLE_VERSION_HEARTBEAT = True # 是否启用“定期版本号自增”(默认关闭,避免前端频繁全量刷新)
# ======================= 卖出监控配置 (2026-01-12) =======================
# 卖出监控功能开关
ENABLE_SELL_MONITOR = True # 监控总开关
ENABLE_SELL_ALERT_NOTIFICATION = False # 告警通知开关(微信/企微)
# 告警配置
SELL_ALERT_CONFIG = {
'P0_notification': True, # P0级别告警推送通知(极高风险)
'P1_notification': True, # P1级别告警推送通知(高风险)
'P2_notification': True # P2级别告警推送通知(中等风险)
}
# ======================= 网格交易高级配置 (2026-01-24) =======================
# ⭐ 网格交易总开关(独立控制,与ENABLE_AUTO_TRADING互不影响)
# - ENABLE_GRID_TRADING = True:启用网格交易检测和执行
# - ENABLE_GRID_TRADING = False:完全禁用网格交易功能
ENABLE_GRID_TRADING = True # 启用后才能使用网格交易功能
# 回调触发机制
GRID_CALLBACK_RATIO = 0.005 # 回调比例0.5%触发交易
# 档位冷却时间(秒)
GRID_LEVEL_COOLDOWN = 60 # 同一档位60秒内不重复触发
# 成功买入后的最短间隔(秒) - 防止级联买入过快
# 9:25开盘后若价格已低于下轨,连续跌穿多个档位会快速触发多次买入
# 设为300(5分钟)可避免在极端开盘波动中过快消耗 max_investment
GRID_BUY_COOLDOWN = 300 # 0=不限制(向后兼容默认值);建议实盘设为 300
# 成功卖出后的最短间隔(秒) - 防止价格在上轨附近震荡时级联触发多次卖出
# 与 GRID_BUY_COOLDOWN 对称(A-4/B-5修复):下单成功后立即记录冷却时间,
# 即使 DB 写入失败,冷却保护依然有效,阻止重复下单。
GRID_SELL_COOLDOWN = 300 # 0=不限制;建议实盘设为 300
# 网格实盘委托运行期对账:成交/委托推送漏掉时,定期用券商当日委托兜底确认 pending 订单。
GRID_ORDER_RECONCILE_INTERVAL = 15 # 秒;0=禁用运行期对账
GRID_ORDER_RECONCILE_STALE_SECONDS = 5 # pending 创建后超过该时间才参与运行期对账
ENABLE_GRID_PENDING_ORDER_AUTO_CANCEL = True # 网格实盘pending委托超时自动撤单
GRID_PENDING_ORDER_TIMEOUT_SECONDS = 90 # 网格pending委托超时阈值(秒)
GRID_PENDING_ORDER_AUTO_REORDER = True # 网格pending撤单完成后是否自动重挂
GRID_PENDING_ORDER_REORDER_MAX_ATTEMPTS = 1 # 单笔网格委托最多自动重挂次数
GRID_PENDING_CANCEL_CONFIRM_TIMEOUT_SECONDS = 60 # 撤单请求提交后等待终态提示阈值(秒)
# 卖出冷却自适应缩短阈值
# 当触发价比上次成交价高出该比例时,冷却期减半,避免单边上涨行情中错过更高档位
# 例:上次卖出7.42,当前触发价7.68,涨幅3.5%>2%,冷却期从300s缩短为150s
GRID_SELL_COOLDOWN_PRICE_THRESHOLD = 0.02 # 2%涨幅触发自适应缩短;0=禁用
# 启动条件配置
GRID_REQUIRE_PROFIT_TRIGGERED = _env_bool("GRID_REQUIRE_PROFIT_TRIGGERED", False) # 是否要求已触发止盈才能启动网格交易(True=更安全,False=更灵活)
# 混合退出机制 - 默认值
GRID_MAX_DEVIATION_RATIO = 0.15 # 网格中心最大偏离±15%
GRID_TARGET_PROFIT_RATIO = 0.10 # 目标盈利10%
GRID_STOP_LOSS_RATIO = -0.10 # 止损-10%
GRID_DEFAULT_DURATION_DAYS = 7 # 默认运行7天
# Web界面默认值
GRID_DEFAULT_PRICE_INTERVAL = 0.05 # 默认价格间隔5%
GRID_DEFAULT_POSITION_RATIO = 0.25 # 默认每档交易25%
GRID_DEFAULT_MAX_INVESTMENT_RATIO = 0.5 # 默认最大投入为持仓市值50%
# 网格单次交易份额模式
# - 'amount' 固定金额: 每次买入金额 = max_investment × position_ratio (现有行为)
# - 'shares' 固定股数: 每次买卖股数 = fixed_volume (固定股数模式仍受 max_investment 硬上限兜底)
GRID_DEFAULT_TRADE_MODE = 'amount' # 默认固定金额
# 日志级别
GRID_LOG_LEVEL = "INFO" # DEBUG时输出详细价格追踪
# 网格交易策略标识
GRID_STRATEGY_NAME = "grid" # 用于trade_records表的strategy字段
# 实盘网格委托确认机制
# True: 实盘网格下单成功后仅登记待确认委托,等成交回报到达后再更新网格统计/重建网格
# False: 兼容旧行为(不推荐实盘使用)
GRID_CONFIRM_LIVE_ORDER_BY_DEAL = True
# 网格信号执行前复核
GRID_SIGNAL_MAX_AGE_SECONDS = 60 # 网格信号最长有效期(秒)
GRID_SIGNAL_MAX_PRICE_DRIFT_RATIO = 0.01 # 执行前最新价相对触发价最大容忍偏离(1%)
# 网格对手价下单(提高成交概率)
# True: 实盘下单不指定价格,由 executor 取卖三价(买)/买三价(卖),参考动态止盈下单逻辑,提高成交概率
# False: 实盘下单使用 trigger_price 限价(兼容旧行为)
# 注意: 对手价仅在 GRID_CONFIRM_LIVE_ORDER_BY_DEAL=True 时启用,
# 因为成交以真实回报价落账(handle_deal_callback),统计才准确;
# 非确认模式下用 trigger_price 估算落账,强制回退限价以保持 V1/V1-SELL 修复的统计一致性。
GRID_USE_COUNTERPARTY_PRICE = True
GRID_COUNTERPARTY_BUY_PRICE_BUFFER_RATIO = 0.02 # 对手价买入资金预占缓冲,防止成交价高于触发价时突破最大投入
# 网格涨跌停/停牌防护(仅实盘生效)
# True: 实盘下单前检查标的涨跌停/停牌状态,封板时跳过本次交易,避免无效挂单
# - 买入: 现价已达涨停价 → 跳过(封板买不进/追涨)
# - 卖出: 现价已达跌停价 → 跳过(封板卖不出)
# - 停牌/无有效现价 → 买卖均跳过
# 涨跌停价获取失败时 fail-open(放行),仅保留停牌判断
GRID_ENABLE_PRICE_LIMIT_GUARD = True
GRID_PRICE_LIMIT_EPS = 0.001 # 涨跌停判定容差(元),补偿浮点误差
def get_grid_default_config(position_market_value: float) -> dict:
"""
获取网格交易默认配置
Args:
position_market_value: 当前持仓市值
Returns:
默认配置字典
"""
return {
'price_interval': GRID_DEFAULT_PRICE_INTERVAL,