chuan commited on
Commit
8e6a923
·
0 Parent(s):

Initial commit from Trae: Gradio Dashboard + Market Collector (Clean)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .gitignore +34 -0
  3. README.md +35 -0
  4. app.py +107 -0
  5. config.py +21 -0
  6. memory/user_global.md +62 -0
  7. requirements.txt +111 -0
  8. sitecustomize.py +27 -0
  9. 基础库/common_core/__init__.py +3 -0
  10. 基础库/common_core/backtest/__init__.py +4 -0
  11. 基础库/common_core/backtest/equity.py +360 -0
  12. 基础库/common_core/backtest/evaluate.py +97 -0
  13. 基础库/common_core/backtest/figure.py +230 -0
  14. 基础库/common_core/backtest/metrics.py +478 -0
  15. 基础库/common_core/backtest/rebalance.py +70 -0
  16. 基础库/common_core/backtest/signal.py +98 -0
  17. 基础库/common_core/backtest/simulator.py +204 -0
  18. 基础库/common_core/backtest/version.py +7 -0
  19. 基础库/common_core/backtest/进度条.py +232 -0
  20. 基础库/common_core/config/loader.py +74 -0
  21. 基础库/common_core/config/models.py +60 -0
  22. 基础库/common_core/exchange/__init__.py +2 -0
  23. 基础库/common_core/exchange/base_client.py +723 -0
  24. 基础库/common_core/exchange/binance_async.py +172 -0
  25. 基础库/common_core/exchange/standard_client.py +284 -0
  26. 基础库/common_core/pyproject.toml +25 -0
  27. 基础库/common_core/quant_unified_common_core.egg-info/PKG-INFO +15 -0
  28. 基础库/common_core/quant_unified_common_core.egg-info/SOURCES.txt +26 -0
  29. 基础库/common_core/quant_unified_common_core.egg-info/dependency_links.txt +1 -0
  30. 基础库/common_core/quant_unified_common_core.egg-info/requires.txt +7 -0
  31. 基础库/common_core/quant_unified_common_core.egg-info/top_level.txt +4 -0
  32. 基础库/common_core/risk_ctrl/liquidation.py +45 -0
  33. 基础库/common_core/risk_ctrl/test_liquidation.py +34 -0
  34. 基础库/common_core/tests/test_orderbook_replay.py +53 -0
  35. 基础库/common_core/utils/__init__.py +2 -0
  36. 基础库/common_core/utils/async_commons.py +30 -0
  37. 基础库/common_core/utils/commons.py +181 -0
  38. 基础库/common_core/utils/dingding.py +107 -0
  39. 基础库/common_core/utils/factor_hub.py +58 -0
  40. 基础库/common_core/utils/functions.py +71 -0
  41. 基础库/common_core/utils/orderbook_replay.py +92 -0
  42. 基础库/common_core/utils/path_kit.py +72 -0
  43. 基础库/通用选币回测框架/因子库/9_SharpeMomentum.py +33 -0
  44. 基础库/通用选币回测框架/因子库/ADX.py +49 -0
  45. 基础库/通用选币回测框架/因子库/AO.py +53 -0
  46. 基础库/通用选币回测框架/因子库/ATR.py +25 -0
  47. 基础库/通用选币回测框架/因子库/ActiveBuyRatio.py +23 -0
  48. 基础库/通用选币回测框架/因子库/AdxMinus.py +122 -0
  49. 基础库/通用选币回测框架/因子库/AdxPlus.py +112 -0
  50. 基础库/通用选币回测框架/因子库/AdxVolWaves.py +108 -0
.DS_Store ADDED
Binary file (10.2 kB). View file
 
.gitignore ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data and Logs
2
+ data/
3
+ cache/
4
+ 系统日志/
5
+ .deps/
6
+ 应用/
7
+ 策略仓库/
8
+ 4 号做市策略/
9
+
10
+ # Data files
11
+ *.h5
12
+ *.parquet
13
+ *.csv
14
+ *.db
15
+ *.sqlite
16
+ *.pkl
17
+
18
+ # Python
19
+ __pycache__/
20
+ *.pyc
21
+ *.pyo
22
+ *.pyd
23
+ .venv/
24
+ env/
25
+ venv/
26
+ .env
27
+
28
+ # IDEs
29
+ .vscode/
30
+ .idea/
31
+ .trae/
32
+
33
+ # Logs
34
+ *.log
README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 量化交易统一系统 (Quant Unified)
2
+
3
+ 本仓库整合了所有量化交易相关的组件,包括管理应用、策略仓库、后端服务和基础库。
4
+
5
+ ## 目录结构 (中文化重构版)
6
+
7
+ - **应用 (应用/)**: 各种管理与展示应用
8
+ - **qronos/**: 量化交易管理平台(Web 前端 + Python 后端)
9
+ - 提供策略管理、回测分析、实盘监控的网页界面。
10
+ - 启动方式:
11
+ - 后端: `cd 应用/qronos && source .venv/bin/activate && python -X utf8 main.py`
12
+ - 前端: `cd 应用/qronos && npm run dev`
13
+
14
+ - **策略仓库 (策略仓库/)**: 各种交易策略实现
15
+ - **二号网格策略/**: 网格交易策略实现,包含回测与实盘。
16
+ - 运行回测: `cd 策略仓库/二号网格策略 && python -X utf8 backtest.py`
17
+ - **一号择时策略/**: 包含选币和择时逻辑。
18
+ - **三号对冲策略/**: 双向对冲策略。
19
+
20
+ - **服务 (服务/)**: 核心实盘与回测服务
21
+ - **firm/**: 实盘交易核心服务,提供底层交易、行情和评估功能。
22
+
23
+ - **基础库 (基础库/)**: 项目通用的基础组件
24
+ - **common_core/**: 包含风控、配置加载、工具函数等核心模块。
25
+
26
+ - **测试用例 (测试用例/)**: 单元测试与集成测试脚本。
27
+
28
+ - **系统日志 (系统日志/)**: 统一存储各组件运行产生的日志。
29
+
30
+ ## 开发指南
31
+
32
+ 1. **环境准备**: 建议使用 Python 3.14+ 环境。
33
+ 2. **导入机制**: 项目使用了 `sitecustomize.py` 钩子,允许直接从顶级目录导入,例如 `import common_core` 或 `from 策略仓库.二号网格策略 import ...`。
34
+ 3. **编码规范**: 强制使用 UTF-8 编码。函数名、变量名推荐使用中文命名(符合“编程导师”教学规范)。
35
+ 4. **单体仓库**: 本仓库采用 Monorepo 结构,请直接在根目录打开 IDE 进行开发。
app.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Quant_Unified 监控面板 (Gradio 版)
4
+ 这是一个运行在 Hugging Face Spaces 上的监控应用,它会:
5
+ 1. 在后台启动数据采集服务。
6
+ 2. 实时从 Supabase 获取并显示各个服务的运行状态。
7
+ """
8
+
9
+ import gradio as gr
10
+ import os
11
+ import subprocess
12
+ import time
13
+ import pandas as pd
14
+ from supabase import create_client
15
+ from datetime import datetime
16
+ import threading
17
+
18
+ # ==========================================
19
+ # 1. 配置与初始化
20
+ # ==========================================
21
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
22
+ SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_ANON_KEY")
23
+
24
+ # 初始化 Supabase 客户端
25
+ supabase = None
26
+ if SUPABASE_URL and SUPABASE_KEY:
27
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
28
+
29
+ def 启动后台采集():
30
+ """在独立进程中启动采集脚本"""
31
+ print("🚀 正在启动后台采集服务...")
32
+ script_path = os.path.join("服务", "数据采集", "启动采集.py")
33
+ # 设置环境变量,确保子进程能找到项目根目录
34
+ env = os.environ.copy()
35
+ env["PYTHONPATH"] = os.getcwd()
36
+ # 使用 sys.executable 确保使用相同的 Python 解释器
37
+ import sys
38
+ process = subprocess.Popen(
39
+ [sys.executable, script_path],
40
+ env=env,
41
+ stdout=subprocess.PIPE,
42
+ stderr=subprocess.STDOUT,
43
+ text=True,
44
+ bufsize=1,
45
+ universal_newlines=True
46
+ )
47
+ # 实时打印日志到终端(HF 容器日志可见)
48
+ for line in process.stdout:
49
+ print(f"[Collector] {line.strip()}")
50
+
51
+ # 启动后台线程
52
+ thread = threading.Thread(target=启动后台采集, daemon=True)
53
+ thread.start()
54
+
55
+ # ==========================================
56
+ # 2. UI 逻辑
57
+ # ==========================================
58
+ def 获取监控数据():
59
+ """从 Supabase 获取 service_status 表的所有数据"""
60
+ if not supabase:
61
+ return pd.DataFrame([{"错误": "未配置 SUPABASE_URL 或 KEY"}])
62
+
63
+ try:
64
+ response = supabase.table("service_status").select("*").execute()
65
+ data = response.data
66
+ if not data:
67
+ return pd.DataFrame([{"信息": "目前没有服务在运行"}])
68
+
69
+ # 转换为 DataFrame 方便展示
70
+ df = pd.DataFrame(data)
71
+ # 简单处理下时间格式
72
+ if "updated_at" in df.columns:
73
+ df["更新时间"] = pd.to_datetime(df["updated_at"]).dt.strftime('%Y-%m-%d %H:%M:%S')
74
+ df = df.drop(columns=["updated_at"])
75
+
76
+ # 重命名列名,让高中生也能看懂
77
+ rename_map = {
78
+ "service_name": "服务名称",
79
+ "status": "状态",
80
+ "cpu_percent": "CPU使用率(%)",
81
+ "memory_percent": "内存使用率(%)",
82
+ "details": "详细信息"
83
+ }
84
+ df = df.rename(columns=rename_map)
85
+ return df
86
+ except Exception as e:
87
+ return pd.DataFrame([{"错误": f"获取数据失败: {str(e)}"}])
88
+
89
+ # ==========================================
90
+ # 3. 构建 Gradio 界面
91
+ # ==========================================
92
+ with gr.Blocks(title="Quant_Unified 监控中心", theme=gr.themes.Soft()) as demo:
93
+ gr.Markdown("# 🚀 Quant_Unified 量化系统监控中心")
94
+ gr.Markdown("实时展示部署在云端的采集服务状态。数据通过 Supabase 同步。")
95
+
96
+ with gr.Row():
97
+ status_table = gr.DataFrame(label="服务状态列表", value=获取监控数据, every=5) # 每5秒刷新一次
98
+
99
+ with gr.Row():
100
+ refresh_btn = gr.Button("手动刷新")
101
+ refresh_btn.click(获取监控数据, outputs=status_table)
102
+
103
+ gr.Markdown("---")
104
+ gr.Markdown("💡 **提示**:如果状态显示为 'ok',说明后台采集正在正常工作。")
105
+
106
+ if __name__ == "__main__":
107
+ demo.launch(server_name="0.0.0.0", server_port=7860)
config.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Quant_Unified 全局配置
4
+ 用于统一管理跨服务的常量参数
5
+ """
6
+
7
+ import os
8
+
9
+ # ==========================================
10
+ # 1. 行情采集配置
11
+ # ==========================================
12
+
13
+ # 深度图档位 (支持 5, 10, 20, 50, 100)
14
+ # 注意: 修改此值会影响采集器订阅的数据流以及数据存储的结构
15
+ # 警告: 档位越高,数据量越大,带宽消耗越高
16
+ DEPTH_LEVEL = 20
17
+
18
+ # ==========================================
19
+ # 2. 路径配置
20
+ # ==========================================
21
+ # 可以在这里统一定义数据根目录等
memory/user_global.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ trigger: always_on
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # 核心角色与最高指令 (Core Identity & Prime Directives)
7
+
8
+ ## 1. 身份定位:双重人格
9
+ 你拥有双重身份,必须同时满足以下要求:
10
+ * **顶级全栈架构师 (The Architect)**:你只写业界最先进 (SOTA)、最优雅、性能最强及其“干净”的代码。代码风格对标 Apple/Google 首席工程师。
11
+ * **金牌编程导师 (The Mentor)**:你的用户是一名**只会中文的高中生**。
12
+ * **教学义务**:你必须用“人话”和类比解释一切。
13
+ * **术语禁忌**:遇到专业术语(如 Docker, IPC, AOT, Hydration 等)或英文缩写,**必须**立即展开解释其含义和作用,严禁直接堆砌名词。
14
+
15
+ ## 2. 核心原则
16
+ * **拒绝降级**:即使面对高中生,你也必须交付 **SOTA (业界顶尖)** 的技术方案。不要因为用户是初学者就提供简化版或过时的垃圾代码(MVP)。如果技术太难,你的任务是把它**解释清楚**,而不是把它**做烂**。
17
+ * **拒绝假数据**:**永远不允许**使用模拟数据 (Mock Data)。必须连接真实接口、数据库或文件系统。
18
+ * **显式运行**:**严禁静默运行**。任何脚本或程序的启动,必须在终端(Terminal)中有实时的日志输出。用户必须看到程序在“动”。
19
+
20
+ ---
21
+
22
+ # 代码规范与工程标准 (Coding Standards)
23
+
24
+ ## 1. 中文化编程 (教学辅助)
25
+ 为了降低高中生的认知负荷,在**不导致语法错误**且**不影响运行**的前提下,强制执行:
26
+ * **中文命名**:函数名、变量名、类名**尽可能使用中文**。
27
+ * *Good*: `def 计算移动平均线(价格列表):`
28
+ * *Bad*: `def calc_ma(price_list):`
29
+ * **中文注释**:每个代码文件开头必须包含**中文文件头**,用通俗语言解释“这个文件是干嘛的”。代码内部逻辑必须通过中文注释解释“为什么这么写”。
30
+
31
+ ## 2. 前端标准 (React & UI)
32
+ * **React 编译器优先**:代码必须兼容并开启 **React Compiler**。避免使用过时的 `useMemo`/`useCallback` 手动优化(除非编译器无法处理),让代码更干净。
33
+ * **Apple 级审美**:默认扮演 Apple 顶级 UI 工程师。界面必须具有极致的审美、流畅的动画(Framer Motion)和高级的交互感。
34
+ * **TypeScript**:零容忍报错。自动修复所有红线,类型定义必须精准。
35
+ * **错误自愈**:编写前端自动化测试或脚本时,自动调用 `playwright` MCP 修复报错。
36
+
37
+ ## 3. Python 标准
38
+ * **执行环境**:默认使用 `python -X utf8` 运行,确保中文处理无乱码。
39
+ * **异常处理**:绝不“吞掉”错误。必须使用卫语句 (Guard Clauses) 提前拦截异常。
40
+
41
+ ---
42
+
43
+ # 自动化工作流 (Automated Workflow)
44
+
45
+ ## 1. 环境与执行 (每次行动前检查)
46
+ 1. **虚拟环境**:项目若无 venv,**优先**自动创建并激活。
47
+ 2. **文件占用**:删除或写入文件前,检查句柄占用 (Handle check)。
48
+ 3. **Git 自动化**:自主判断代码节点。认为有必要时(如完成一个功能模块),**自动执行 Git 提交**,无需频繁请示。
49
+
50
+ ## 2. 记忆与凭证
51
+ * **长期记忆**:自动使用 `memory` MCP 存储项目关键信息。
52
+ * **凭证管理**:记住关键密码(如 PostgreSQL 密码 `587376`),需要时自动填充,不要重复问用户。
53
+
54
+ ---
55
+
56
+ # 沟通协议 (Communication Protocol)
57
+
58
+ * **思考与输出**:你可以用英文思考(Thinking Process),但**最终回复必须完全使用中文**。
59
+ * **解释风格**:
60
+ * *场景*:解释 `Redis`。
61
+ * *错误*:“Redis 是一个基于内存的 Key-Value 存储系统。”
62
+ * *正确*:“Redis 就像是电脑的‘内存条’,也就是个**快取区**。我们要存东西时,先放这里,因为读写速度极快,比存到硬盘(数据库)里快几千倍。适合用来存那些大家频繁要看的数据。”
requirements.txt ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiodns==3.6.0
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.13.2
4
+ aiosignal==1.4.0
5
+ aiosqlite==0.21.0
6
+ annotated-doc==0.0.4
7
+ annotated-types==0.7.0
8
+ anyio==4.12.0
9
+ attrs==25.4.0
10
+ beautifulsoup4==4.14.3
11
+ bleach==6.3.0
12
+ ccxt==4.5.26
13
+ certifi==2025.11.12
14
+ cffi==2.0.0
15
+ charset-normalizer==3.4.4
16
+ click==8.3.1
17
+ colorama==0.4.6
18
+ contourpy==1.3.3
19
+ cryptography==46.0.3
20
+ cssselect==1.3.0
21
+ cssutils==2.11.1
22
+ cycler==0.12.1
23
+ dataframe_image==0.2.7
24
+ defusedxml==0.7.1
25
+ ecdsa==0.19.1
26
+ fastapi==0.124.2
27
+ fastjsonschema==2.21.2
28
+ fonttools==4.61.0
29
+ frozenlist==1.8.0
30
+ greenlet==3.3.0
31
+ h11==0.16.0
32
+ h5py==3.15.1
33
+ hdf5plugin==6.0.0
34
+ idna==3.11
35
+ Jinja2==3.1.6
36
+ jsonschema==4.25.1
37
+ jsonschema-specifications==2025.9.1
38
+ jupyter_client==8.7.0
39
+ jupyter_core==5.9.1
40
+ jupyterlab_pygments==0.3.0
41
+ kiwisolver==1.4.9
42
+ llvmlite==0.46.0
43
+ lxml==6.0.2
44
+ MarkupSafe==3.0.3
45
+ matplotlib==3.10.7
46
+ mistune==3.1.4
47
+ more-itertools==10.8.0
48
+ multidict==6.7.0
49
+ narwhals==2.13.0
50
+ nbclient==0.10.2
51
+ nbconvert==7.16.6
52
+ nbformat==5.10.4
53
+ numba==0.63.1
54
+ numpy==2.3.5
55
+ packaging==25.0
56
+ pandas==2.3.3
57
+ pandocfilters==1.5.1
58
+ pillow==12.0.0
59
+ platformdirs==4.5.1
60
+ playwright==1.57.0
61
+ plotly==6.5.0
62
+ propcache==0.4.1
63
+ pyasn1==0.6.1
64
+ pycares==4.11.0
65
+ pycparser==2.23
66
+ pycryptodome==3.23.0
67
+ pydantic==2.12.5
68
+ pydantic_core==2.41.5
69
+ pyee==13.0.0
70
+ Pygments==2.19.2
71
+ pyotp==2.9.0
72
+ pyparsing==3.2.5
73
+ python-dateutil==2.9.0.post0
74
+ python-dotenv==1.2.1
75
+ python-jose==3.5.0
76
+ python-multipart==0.0.20
77
+ python-socks==2.8.0
78
+ pytz==2025.2
79
+ pyzmq==27.1.0
80
+ referencing==0.37.0
81
+ requests==2.32.5
82
+ rpds-py==0.30.0
83
+ rsa==4.9.1
84
+ scipy==1.16.3
85
+ seaborn==0.13.2
86
+ setuptools==80.9.0
87
+ six==1.17.0
88
+ soupsieve==2.8
89
+ SQLAlchemy==2.0.45
90
+ starlette==0.50.0
91
+ tinycss2==1.4.0
92
+ tornado==6.5.2
93
+ tqdm==4.67.1
94
+ traitlets==5.14.3
95
+ typing-inspection==0.4.2
96
+ typing_extensions==4.15.0
97
+ tzdata==2025.2
98
+ urllib3==2.6.1
99
+ uvicorn==0.38.0
100
+ webencodings==0.5.1
101
+ websockets
102
+ supabase
103
+ pyarrow
104
+ gradio
105
+ psutil
106
+ python-dotenv
107
+ websockets==15.0.1
108
+ yarl==1.22.0
109
+ supabase
110
+ psutil
111
+ pyarrow
sitecustomize.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Python 环境初始化钩子
3
+
4
+ 此文件会被 Python 的 site 模块自动加载(只要它在 sys.path 中)。
5
+ 它的主要作用是动态配置 sys.path,将项目的 libs, services, strategies, apps 等目录
6
+ 加入到模块搜索路径中。
7
+
8
+ 这样做的目的是:
9
+ 1. 允许项目内的代码直接通过 import 导入这些目录下的模块(如 import common_core)。
10
+ 2. 避免在每个脚本中手动编写 sys.path.append(...)。
11
+ 3. 简化开发环境配置,无需强制设置 PYTHONPATH 环境变量。
12
+ """
13
+ import os
14
+ import sys
15
+
16
+ _ROOT = os.path.dirname(os.path.abspath(__file__))
17
+ _EXTRA = [
18
+ os.path.join(_ROOT, '基础库'),
19
+ os.path.join(_ROOT, '服务'),
20
+ os.path.join(_ROOT, '策略仓库'),
21
+ os.path.join(_ROOT, '4 号做市策略'),
22
+ os.path.join(_ROOT, '应用'),
23
+ os.path.join(_ROOT, '应用', 'qronos'),
24
+ ]
25
+ for p in _EXTRA:
26
+ if os.path.isdir(p) and p not in sys.path:
27
+ sys.path.append(p)
基础库/common_core/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Quant Unified Common Core
3
+ """
基础库/common_core/backtest/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ __init__.py
4
+ """
基础库/common_core/backtest/equity.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [核心资产计算模块]
4
+ 功能:负责根据策略信号、行情数据、费率等,计算资金曲线、持仓价值和爆仓风险。
5
+ """
6
+ import time
7
+
8
+ import numba as nb
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from core.evaluate import strategy_evaluate
13
+ from core.figure import draw_equity_curve_plotly
14
+ from core.model.backtest_config import BacktestConfig
15
+ from core.rebalance import RebAlways
16
+ from core.simulator import Simulator
17
+ from core.utils.functions import load_min_qty
18
+ from core.utils.path_kit import get_file_path
19
+ from update_min_qty import min_qty_path
20
+
21
+ pd.set_option('display.max_rows', 1000)
22
+ pd.set_option('expand_frame_repr', False) # 当列太多时不换行
23
+
24
+
25
+ def calc_equity(conf: BacktestConfig,
26
+ pivot_dict_spot: dict,
27
+ pivot_dict_swap: dict,
28
+ df_spot_ratio: pd.DataFrame,
29
+ df_swap_ratio: pd.DataFrame,
30
+ show_plot: bool = True):
31
+ """
32
+ 计算回测结果的函数
33
+ :param conf: 回测配置
34
+ :param pivot_dict_spot: 现货行情数据
35
+ :param pivot_dict_swap: 永续合约行情数据
36
+ :param df_spot_ratio: 现货目标资金占比
37
+ :param df_swap_ratio: 永续合约目标资金占比
38
+ :param show_plot: 是否显示回测图
39
+ :return: 没有返回值
40
+ """
41
+ # ====================================================================================================
42
+ # 1. 数据预检和准备数据
43
+ # 数据预检,对齐所有数据的长度(防御性编程)
44
+ # ====================================================================================================
45
+ if len(df_spot_ratio) != len(df_swap_ratio) or np.any(df_swap_ratio.index != df_spot_ratio.index):
46
+ raise RuntimeError(f'数据长度不一致,现货数据长度:{len(df_spot_ratio)}, 永续合约数据长度:{len(df_swap_ratio)}')
47
+
48
+ # 开始时间列
49
+ candle_begin_times = df_spot_ratio.index.to_series().reset_index(drop=True)
50
+
51
+ # 获取现货和永续合约的币种,并且排序
52
+ spot_symbols = sorted(df_spot_ratio.columns)
53
+ swap_symbols = sorted(df_swap_ratio.columns)
54
+
55
+ # 裁切现货数据,保证open,close,vwap1m,对应的df中,现货币种、时间长度一致
56
+ pivot_dict_spot = align_pivot_dimensions(pivot_dict_spot, spot_symbols, candle_begin_times)
57
+
58
+ # 裁切合约数据,保证open,close,vwap1m,funding_fee对应的df中,合约币种、时间长度一致
59
+ pivot_dict_swap = align_pivot_dimensions(pivot_dict_swap, swap_symbols, candle_begin_times)
60
+
61
+ # 读入最小下单量数据
62
+ spot_lot_sizes = read_lot_sizes(min_qty_path / '最小下单量_spot.csv', spot_symbols)
63
+ swap_lot_sizes = read_lot_sizes(min_qty_path / '最小下单量_swap.csv', swap_symbols)
64
+
65
+ pos_calc = RebAlways(spot_lot_sizes.to_numpy(), swap_lot_sizes.to_numpy())
66
+
67
+ # ====================================================================================================
68
+ # 2. 开始模拟交易
69
+ # 开始策马奔腾啦 🐎
70
+ # ====================================================================================================
71
+ s_time = time.perf_counter()
72
+ equities, turnovers, fees, funding_fees, margin_rates, long_pos_values, short_pos_values = start_simulation(
73
+ init_capital=conf.initial_usdt, # 初始资金,单位:USDT
74
+ leverage=conf.leverage, # 杠杆
75
+ spot_lot_sizes=spot_lot_sizes.to_numpy(), # 现货最小下单量
76
+ swap_lot_sizes=swap_lot_sizes.to_numpy(), # 永续合约最小下单量
77
+ spot_c_rate=conf.spot_c_rate, # 现货杠杆率
78
+ swap_c_rate=conf.swap_c_rate, # 永续合约杠杆率
79
+ spot_min_order_limit=float(conf.spot_min_order_limit), # 现货最小下单金额
80
+ swap_min_order_limit=float(conf.swap_min_order_limit), # 永续合约最小下单金额
81
+ min_margin_rate=conf.margin_rate, # 最低保证金比例
82
+ # 选股结果计算聚合得到的每个周期目标资金占比
83
+ spot_ratio=df_spot_ratio[spot_symbols].to_numpy(), # 现货目标资金占比
84
+ swap_ratio=df_swap_ratio[swap_symbols].to_numpy(), # 永续合约目标资金占比
85
+ # 现货行情数据
86
+ spot_open_p=pivot_dict_spot['open'].to_numpy(), # 现货开盘价
87
+ spot_close_p=pivot_dict_spot['close'].to_numpy(), # 现货收盘价
88
+ spot_vwap1m_p=pivot_dict_spot['vwap1m'].to_numpy(), # 现货开盘一分钟均价
89
+ # 永续合约行情数据
90
+ swap_open_p=pivot_dict_swap['open'].to_numpy(), # 永续合约开盘价
91
+ swap_close_p=pivot_dict_swap['close'].to_numpy(), # 永续合约收盘价
92
+ swap_vwap1m_p=pivot_dict_swap['vwap1m'].to_numpy(), # 永续合约开盘一分钟均价
93
+ funding_rates=pivot_dict_swap['funding_rate'].to_numpy(), # 永续合约资金费率
94
+ pos_calc=pos_calc, # 仓位计算
95
+ )
96
+ print(f'✅ 完成模拟交易,花费时间: {time.perf_counter() - s_time:.3f}秒')
97
+ print()
98
+
99
+ # ====================================================================================================
100
+ # 3. 回测结果汇总,并输出相关文件
101
+ # ====================================================================================================
102
+ print('🌀 开始生成回测统计结果...')
103
+ account_df = pd.DataFrame({
104
+ 'candle_begin_time': candle_begin_times,
105
+ 'equity': equities,
106
+ 'turnover': turnovers,
107
+ 'fee': fees,
108
+ 'funding_fee': funding_fees,
109
+ 'marginRatio': margin_rates,
110
+ 'long_pos_value': long_pos_values,
111
+ 'short_pos_value': short_pos_values
112
+ })
113
+
114
+ account_df['净值'] = account_df['equity'] / conf.initial_usdt
115
+ account_df['涨跌幅'] = account_df['净值'].pct_change()
116
+ account_df.loc[account_df['marginRatio'] < conf.margin_rate, '是否爆仓'] = 1
117
+ account_df['是否爆仓'].fillna(method='ffill', inplace=True)
118
+ account_df['是否爆仓'].fillna(value=0, inplace=True)
119
+
120
+ account_df.to_csv(conf.get_result_folder() / '资金曲线.csv', encoding='utf-8-sig')
121
+
122
+ # 策略评价
123
+ rtn, year_return, month_return, quarter_return = strategy_evaluate(account_df, net_col='净值', pct_col='涨跌幅')
124
+ conf.set_report(rtn.T)
125
+ rtn.to_csv(conf.get_result_folder() / '策略评价.csv', encoding='utf-8-sig')
126
+ year_return.to_csv(conf.get_result_folder() / '年度账户收益.csv', encoding='utf-8-sig')
127
+ quarter_return.to_csv(conf.get_result_folder() / '季度账户收益.csv', encoding='utf-8-sig')
128
+ month_return.to_csv(conf.get_result_folder() / '月度账户收益.csv', encoding='utf-8-sig')
129
+
130
+ if show_plot:
131
+ # 绘制资金曲线
132
+ all_swap = pd.read_pickle(get_file_path('data', 'candle_data_dict.pkl'))
133
+ btc_df = all_swap['BTC-USDT']
134
+ account_df = pd.merge(left=account_df, right=btc_df[['candle_begin_time', 'close']], on=['candle_begin_time'],
135
+ how='left')
136
+ account_df['close'].fillna(method='ffill', inplace=True)
137
+ account_df['BTC涨跌幅'] = account_df['close'].pct_change()
138
+ account_df['BTC涨跌幅'].fillna(value=0, inplace=True)
139
+ account_df['BTC资金曲线'] = (account_df['BTC涨跌幅'] + 1).cumprod()
140
+ del account_df['close'], account_df['BTC涨跌幅']
141
+
142
+ print(f"🎯 策略评价================\n{rtn}")
143
+ print(f"🗓️ 分年收益率================\n{year_return}")
144
+
145
+ print(f'💰 总手续费: {account_df["fee"].sum():,.2f}USDT')
146
+ print()
147
+
148
+ print('🌀 开始绘制资金曲线...')
149
+ eth_df = all_swap['ETH-USDT']
150
+ account_df = pd.merge(left=account_df, right=eth_df[['candle_begin_time', 'close']], on=['candle_begin_time'],
151
+ how='left')
152
+ account_df['close'].fillna(method='ffill', inplace=True)
153
+ account_df['ETH涨跌幅'] = account_df['close'].pct_change()
154
+ account_df['ETH涨跌幅'].fillna(value=0, inplace=True)
155
+ account_df['ETH资金曲线'] = (account_df['ETH涨跌幅'] + 1).cumprod()
156
+ del account_df['close'], account_df['ETH涨跌幅']
157
+
158
+ account_df['long_pos_ratio'] = account_df['long_pos_value'] / account_df['equity']
159
+ account_df['short_pos_ratio'] = account_df['short_pos_value'] / account_df['equity']
160
+ account_df['empty_ratio'] = (conf.leverage - account_df['long_pos_ratio'] - account_df['short_pos_ratio']).clip(
161
+ lower=0)
162
+ # 计算累计值,主要用于后面画图使用
163
+ account_df['long_cum'] = account_df['long_pos_ratio']
164
+ account_df['short_cum'] = account_df['long_pos_ratio'] + account_df['short_pos_ratio']
165
+ account_df['empty_cum'] = conf.leverage # 空仓占比始终为 1(顶部)
166
+ # 选币数量
167
+ df_swap_ratio = df_swap_ratio * conf.leverage
168
+ df_spot_ratio = df_spot_ratio * conf.leverage
169
+
170
+ symbol_long_num = df_spot_ratio[df_spot_ratio > 0].count(axis=1) + df_swap_ratio[df_swap_ratio > 0].count(
171
+ axis=1)
172
+ account_df['symbol_long_num'] = symbol_long_num.values
173
+ symbol_short_num = df_spot_ratio[df_spot_ratio < 0].count(axis=1) + df_swap_ratio[df_swap_ratio < 0].count(
174
+ axis=1)
175
+ account_df['symbol_short_num'] = symbol_short_num.values
176
+
177
+ # 生成画图数据字典,可以画出所有offset资金曲线以及各个offset资金曲线
178
+ data_dict = {'多空资金曲线': '净值', 'BTC资金曲线': 'BTC资金曲线', 'ETH资金曲线': 'ETH资金曲线'}
179
+ right_axis = {'多空最大回撤': '净值dd2here'}
180
+
181
+ # 如果画多头、空头资金曲线,同时也会画上回撤曲线
182
+ pic_title = f"CumNetVal:{rtn.at['累积净值', 0]}, Annual:{rtn.at['年化收益', 0]}, MaxDrawdown:{rtn.at['最大回撤', 0]}"
183
+ pic_desc = conf.get_fullname()
184
+ # 调用画图函数
185
+ draw_equity_curve_plotly(account_df, data_dict=data_dict, date_col='candle_begin_time', right_axis=right_axis,
186
+ title=pic_title, desc=pic_desc, path=conf.get_result_folder() / '资金曲线.html',
187
+ show_subplots=True)
188
+
189
+
190
+ def read_lot_sizes(path, symbols):
191
+ """
192
+ 读取每个币种的最小下单量
193
+ :param path: 文件路径
194
+ :param symbols: 币种列表
195
+ :return:
196
+ """
197
+ default_min_qty, min_qty_dict = load_min_qty(path)
198
+ lot_sizes = 0.1 ** pd.Series(min_qty_dict)
199
+ lot_sizes = lot_sizes.reindex(symbols, fill_value=0.1 ** default_min_qty)
200
+ return lot_sizes
201
+
202
+
203
+ def align_pivot_dimensions(market_pivot_dict, symbols, candle_begin_times):
204
+ """
205
+ 对不同维度的数据进行对齐
206
+ :param market_pivot_dict: 原始数据,是一个dict哦
207
+ :param symbols: 币种(列)
208
+ :param candle_begin_times: 时间(行)
209
+ :return:
210
+ """
211
+ return {k: df.loc[candle_begin_times, symbols] for k, df in market_pivot_dict.items()}
212
+
213
+
214
+ @nb.njit
215
+ def calc_lots(equity, close_prices, ratios, lot_sizes):
216
+ """
217
+ 计算每个币种的目标手数
218
+ :param equity: 总权益
219
+ :param close_prices: 收盘价
220
+ :param ratios: 每个币种的资金比例
221
+ :param lot_sizes: 每个币种的最小下单量
222
+ :return: 每个币种的目标手数
223
+ """
224
+ pos_equity = equity * ratios
225
+ mask = np.abs(pos_equity) > 0.01
226
+ target_lots = np.zeros(len(close_prices), dtype=np.int64)
227
+ target_lots[mask] = (pos_equity[mask] / close_prices[mask] / lot_sizes[mask]).astype(np.int64)
228
+ return target_lots
229
+
230
+
231
+ @nb.jit(nopython=True, boundscheck=True)
232
+ def start_simulation(init_capital, leverage, spot_lot_sizes, swap_lot_sizes, spot_c_rate, swap_c_rate,
233
+ spot_min_order_limit, swap_min_order_limit, min_margin_rate, spot_ratio, swap_ratio,
234
+ spot_open_p, spot_close_p, spot_vwap1m_p, swap_open_p, swap_close_p, swap_vwap1m_p,
235
+ funding_rates, pos_calc):
236
+ """
237
+ 模拟交易
238
+ :param init_capital: 初始资金
239
+ :param leverage: 杠杆
240
+ :param spot_lot_sizes: spot 现货的最小下单量
241
+ :param swap_lot_sizes: swap 合约的最小下单量
242
+ :param spot_c_rate: spot 现货的手续费率
243
+ :param swap_c_rate: swap 合约的手续费率
244
+ :param spot_min_order_limit: spot 现货最小下单金额
245
+ :param swap_min_order_limit: swap 合约最小下单金额
246
+ :param min_margin_rate: 维持保证金率
247
+ :param spot_ratio: spot 的仓位透视表 (numpy 矩阵)
248
+ :param swap_ratio: swap 的仓位透视表 (numpy 矩阵)
249
+ :param spot_open_p: spot 的开仓价格透视表 (numpy 矩阵)
250
+ :param spot_close_p: spot 的平仓价格透视表 (numpy 矩阵)
251
+ :param spot_vwap1m_p: spot 的 vwap1m 价格透视表 (numpy 矩阵)
252
+ :param swap_open_p: swap 的开仓价格透视表 (numpy 矩阵)
253
+ :param swap_close_p: swap 的平仓价格透视表 (numpy 矩阵)
254
+ :param swap_vwap1m_p: swap 的 vwap1m 价格透视表 (numpy 矩阵)
255
+ :param funding_rates: swap 的 funding rate 透视表 (numpy 矩阵)
256
+ :param pos_calc: 仓位计算
257
+ :return:
258
+ """
259
+ # ====================================================================================================
260
+ # 1. 初始化回测空间
261
+ # 设置几个固定长度的数组变量,并且重置为0,到时候每一个周期的数据,都按照index的顺序,依次填充进去
262
+ # ====================================================================================================
263
+ n_bars = spot_ratio.shape[0]
264
+ n_syms_spot = spot_ratio.shape[1]
265
+ n_syms_swap = swap_ratio.shape[1]
266
+
267
+ start_lots_spot = np.zeros(n_syms_spot, dtype=np.int64)
268
+ start_lots_swap = np.zeros(n_syms_swap, dtype=np.int64)
269
+ # 现货不设置资金费
270
+ funding_rates_spot = np.zeros(n_syms_spot, dtype=np.float64)
271
+
272
+ turnovers = np.zeros(n_bars, dtype=np.float64)
273
+ fees = np.zeros(n_bars, dtype=np.float64)
274
+ equities = np.zeros(n_bars, dtype=np.float64) # equity after execution
275
+ funding_fees = np.zeros(n_bars, dtype=np.float64)
276
+ margin_rates = np.zeros(n_bars, dtype=np.float64)
277
+ long_pos_values = np.zeros(n_bars, dtype=np.float64)
278
+ short_pos_values = np.zeros(n_bars, dtype=np.float64)
279
+
280
+ # ====================================================================================================
281
+ # 2. 初始化模拟对象
282
+ # ====================================================================================================
283
+ sim_spot = Simulator(init_capital, spot_lot_sizes, spot_c_rate, 0.0, start_lots_spot, spot_min_order_limit)
284
+ sim_swap = Simulator(0, swap_lot_sizes, swap_c_rate, 0.0, start_lots_swap, swap_min_order_limit)
285
+
286
+ # ====================================================================================================
287
+ # 3. 开始回测
288
+ # 每次循环包含以下四个步骤:
289
+ # 1. 模拟开盘on_open
290
+ # 2. 模拟执行on_execution
291
+ # 3. 模拟平仓on_close
292
+ # 4. 设置目标仓位set_target_lots
293
+ # 如下依次执行
294
+ # t1: on_open -> on_execution -> on_close -> set_target_lots
295
+ # t2: on_open -> on_execution -> on_close -> set_target_lots
296
+ # t3: on_open -> on_execution -> on_close -> set_target_lots
297
+ # ...
298
+ # tN: on_open -> on_execution -> on_close -> set_target_lots
299
+ # 并且在每一个t时刻,都会记录账户的截面数据,包括equity,funding_fee,margin_rate,等等
300
+ # ====================================================================================================
301
+ #
302
+ for i in range(n_bars):
303
+ """1. 模拟开盘on_open"""
304
+ # 根据开盘价格,计算账户权益,当前持仓的名义价值,以及资金费
305
+ equity_spot, _, pos_value_spot = sim_spot.on_open(spot_open_p[i], funding_rates_spot, spot_open_p[i])
306
+ equity_swap, funding_fee, pos_value_swap = sim_swap.on_open(swap_open_p[i], funding_rates[i], swap_open_p[i])
307
+
308
+ # 当前持仓的名义价值
309
+ position_val = np.sum(np.abs(pos_value_spot)) + np.sum(np.abs(pos_value_swap))
310
+ if position_val < 1e-8:
311
+ # 没有持仓
312
+ margin_rate = 10000.0
313
+ else:
314
+ margin_rate = (equity_spot + equity_swap) / float(position_val)
315
+
316
+ # 当前保证金率小于维持保证金率,爆仓 💀
317
+ if margin_rate < min_margin_rate:
318
+ margin_rates[i] = margin_rate
319
+ break
320
+
321
+ """2. 模拟开仓on_execution"""
322
+ # 根据开仓价格,计算账户权益,换手,手续费
323
+ equity_spot, turnover_spot, fee_spot = sim_spot.on_execution(spot_vwap1m_p[i])
324
+ equity_swap, turnover_swap, fee_swap = sim_swap.on_execution(swap_vwap1m_p[i])
325
+
326
+ """3. 模拟K线结束on_close"""
327
+ # 根据收盘价格,计算账户权益
328
+ equity_spot_close, pos_value_spot_close = sim_spot.on_close(spot_close_p[i])
329
+ equity_swap_close, pos_value_swap_close = sim_swap.on_close(swap_close_p[i])
330
+
331
+ long_pos_value = (np.sum(pos_value_spot_close[pos_value_spot_close > 0]) +
332
+ np.sum(pos_value_swap_close[pos_value_swap_close > 0]))
333
+
334
+ short_pos_value = -(np.sum(pos_value_spot_close[pos_value_spot_close < 0]) +
335
+ np.sum(pos_value_swap_close[pos_value_swap_close < 0]))
336
+
337
+ # 把中间结果更新到之前初始化的空间
338
+ funding_fees[i] = funding_fee
339
+ equities[i] = equity_spot + equity_swap
340
+ turnovers[i] = turnover_spot + turnover_swap
341
+ fees[i] = fee_spot + fee_swap
342
+ margin_rates[i] = margin_rate
343
+ long_pos_values[i] = long_pos_value
344
+ short_pos_values[i] = short_pos_value
345
+
346
+ # 考虑杠杆
347
+ equity_leveraged = (equity_spot_close + equity_swap_close) * leverage
348
+
349
+ """4. 计算目标持仓"""
350
+ # target_lots_spot = calc_lots(equity_leveraged, spot_close_p[i], spot_ratio[i], spot_lot_sizes)
351
+ # target_lots_swap = calc_lots(equity_leveraged, swap_close_p[i], swap_ratio[i], swap_lot_sizes)
352
+
353
+ target_lots_spot, target_lots_swap = pos_calc.calc_lots(equity_leveraged,
354
+ spot_close_p[i], sim_spot.lots, spot_ratio[i],
355
+ swap_close_p[i], sim_swap.lots, swap_ratio[i])
356
+ # 更新目标持仓
357
+ sim_spot.set_target_lots(target_lots_spot)
358
+ sim_swap.set_target_lots(target_lots_swap)
359
+
360
+ return equities, turnovers, fees, funding_fees, margin_rates, long_pos_values, short_pos_values
基础库/common_core/backtest/evaluate.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [策略绩效评估模块]
4
+ 功能:计算年化收益、最大回撤、夏普比率、胜率等关键指标,并生成分月/分年收益表。
5
+ """
6
+ import itertools
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ # 计算策略评价指标
13
+ def strategy_evaluate(equity, net_col='多空资金曲线', pct_col='本周期多空涨跌幅'):
14
+ """
15
+ 回测评价函数
16
+ :param equity: 资金曲线数据
17
+ :param net_col: 资金曲线列名
18
+ :param pct_col: 周期涨跌幅列名
19
+ :return:
20
+ """
21
+ # ===新建一个dataframe保存回测指标
22
+ results = pd.DataFrame()
23
+
24
+ # 将数字转为百分数
25
+ def num_to_pct(value):
26
+ return '%.2f%%' % (value * 100)
27
+
28
+ # ===计算累积净值
29
+ results.loc[0, '累积净值'] = round(equity[net_col].iloc[-1], 2)
30
+
31
+ # ===计算年化收益
32
+ annual_return = (equity[net_col].iloc[-1]) ** (
33
+ '1 days 00:00:00' / (equity['candle_begin_time'].iloc[-1] - equity['candle_begin_time'].iloc[0]) * 365) - 1
34
+ results.loc[0, '年化收益'] = num_to_pct(annual_return)
35
+
36
+ # ===计算最大回撤,最大回撤的含义:《如何通过3行代码计算最大回撤》https://mp.weixin.qq.com/s/Dwt4lkKR_PEnWRprLlvPVw
37
+ # 计算当日之前的资金曲线的最高点
38
+ equity[f'{net_col.split("资金曲线")[0]}max2here'] = equity[net_col].expanding().max()
39
+ # 计算到历史最高值到当日的跌幅,drowdwon
40
+ equity[f'{net_col.split("资金曲线")[0]}dd2here'] = equity[net_col] / equity[f'{net_col.split("资金曲线")[0]}max2here'] - 1
41
+ # 计算最大回撤,以及最大回撤结束时间
42
+ end_date, max_draw_down = tuple(equity.sort_values(by=[f'{net_col.split("资金曲线")[0]}dd2here']).iloc[0][['candle_begin_time', f'{net_col.split("资金曲线")[0]}dd2here']])
43
+ # 计算最大回撤开始时间
44
+ start_date = equity[equity['candle_begin_time'] <= end_date].sort_values(by=net_col, ascending=False).iloc[0]['candle_begin_time']
45
+ results.loc[0, '最大回撤'] = num_to_pct(max_draw_down)
46
+ results.loc[0, '最大回撤开始时间'] = str(start_date)
47
+ results.loc[0, '最大回撤结束时间'] = str(end_date)
48
+ # ===年化收益/回撤比:我个人比较关注的一个指标
49
+ results.loc[0, '年化收益/回撤比'] = round(annual_return / abs(max_draw_down), 2)
50
+ # ===统计每个周期
51
+ results.loc[0, '盈利周期数'] = len(equity.loc[equity[pct_col] > 0]) # 盈利笔数
52
+ results.loc[0, '亏损周期数'] = len(equity.loc[equity[pct_col] <= 0]) # 亏损笔数
53
+ results.loc[0, '胜率'] = num_to_pct(results.loc[0, '盈利周期数'] / len(equity)) # 胜率
54
+ results.loc[0, '每周期平均收益'] = num_to_pct(equity[pct_col].mean()) # 每笔交易平均盈亏
55
+ results.loc[0, '盈亏收益比'] = round(equity.loc[equity[pct_col] > 0][pct_col].mean() / equity.loc[equity[pct_col] <= 0][pct_col].mean() * (-1), 2) # 盈亏比
56
+ if 1 in equity['是否爆仓'].to_list():
57
+ results.loc[0, '盈亏收益比'] = 0
58
+ results.loc[0, '单周期最大盈利'] = num_to_pct(equity[pct_col].max()) # 单笔最大盈利
59
+ results.loc[0, '单周期大亏损'] = num_to_pct(equity[pct_col].min()) # 单笔最大亏损
60
+
61
+ # ===连续盈利亏损
62
+ results.loc[0, '最大连续盈利周期数'] = max(
63
+ [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] > 0, 1, np.nan))]) # 最大连续盈利次数
64
+ results.loc[0, '最大连续亏损周期数'] = max(
65
+ [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] <= 0, 1, np.nan))]) # 最大连续亏损次数
66
+
67
+ # ===其他评价指标
68
+ results.loc[0, '收益率标准差'] = num_to_pct(equity[pct_col].std())
69
+
70
+ # ===每年、每月收益率
71
+ temp = equity.copy()
72
+ temp.set_index('candle_begin_time', inplace=True)
73
+ year_return = temp[[pct_col]].resample(rule='YE').apply(lambda x: (1 + x).prod() - 1)
74
+ month_return = temp[[pct_col]].resample(rule='ME').apply(lambda x: (1 + x).prod() - 1)
75
+ quarter_return = temp[[pct_col]].resample(rule='QE').apply(lambda x: (1 + x).prod() - 1)
76
+
77
+ def num2pct(x):
78
+ if str(x) != 'nan':
79
+ return str(round(x * 100, 2)) + '%'
80
+ else:
81
+ return x
82
+
83
+ year_return['涨跌幅'] = year_return[pct_col].apply(num2pct)
84
+ month_return['涨跌幅'] = month_return[pct_col].apply(num2pct)
85
+ quarter_return['涨跌幅'] = quarter_return[pct_col].apply(num2pct)
86
+
87
+ # # 对每月收益进行处理,做成二维表
88
+ # month_return.reset_index(inplace=True)
89
+ # month_return['year'] = month_return['candle_begin_time'].dt.year
90
+ # month_return['month'] = month_return['candle_begin_time'].dt.month
91
+ # month_return.set_index(['year', 'month'], inplace=True)
92
+ # del month_return['candle_begin_time']
93
+ # month_return_all = month_return[pct_col].unstack()
94
+ # month_return_all.loc['mean'] = month_return_all.mean(axis=0)
95
+ # month_return_all = month_return_all.apply(lambda x: x.apply(num2pct))
96
+
97
+ return results.T, year_return, month_return, quarter_return
基础库/common_core/backtest/figure.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ figure.py
4
+ """
5
+
6
+ import pandas as pd
7
+ import plotly.graph_objects as go
8
+ import seaborn as sns
9
+ from matplotlib import pyplot as plt
10
+ from plotly import subplots
11
+ from plotly.offline import plot
12
+ from plotly.subplots import make_subplots
13
+ from pathlib import Path
14
+
15
+
16
+ def draw_equity_curve_plotly(df, data_dict, date_col=None, right_axis=None, pic_size=None, chg=False,
17
+ title=None, path=Path('data') / 'pic.html', show=True, desc=None,
18
+ show_subplots=False, markers=None, right_axis_lines=None):
19
+ """
20
+ 绘制策略曲线
21
+ :param df: 包含净值数据的df
22
+ :param data_dict: 要展示的数据字典格式:{图片上显示的名字:df中的列名}
23
+ :param date_col: 时间列的名字,如果为None将用索引作为时间列
24
+ :param right_axis: 右轴数据 (区域图/面积图) {图片上显示的名字:df中的列名}
25
+ :param pic_size: 图片的尺寸
26
+ :param chg: datadict中的数据是否为涨跌幅,True表示涨跌幅,False表示净值
27
+ :param title: 标题
28
+ :param path: 图片路径
29
+ :param show: 是否打开图片
30
+ :param right_axis_lines: 右轴数据 (线图) {图片上显示的名字:df中的列名}
31
+ :return:
32
+ """
33
+ if pic_size is None:
34
+ pic_size = [1500, 800]
35
+
36
+ draw_df = df.copy()
37
+
38
+ # 设置时间序列
39
+ if date_col:
40
+ time_data = draw_df[date_col]
41
+ else:
42
+ time_data = draw_df.index
43
+
44
+ # 绘制左轴数据
45
+ fig = make_subplots(
46
+ rows=3 if show_subplots else 1, cols=1,
47
+ shared_xaxes=True, # 共享 x 轴,主,子图共同变化
48
+ vertical_spacing=0.02, # 减少主图和子图之间的间距
49
+ row_heights=[0.8, 0.1, 0.1] if show_subplots else [1.0], # 主图高度占 70%,子图各占 10%
50
+ specs=[[{"secondary_y": True}], [{"secondary_y": False}], [{"secondary_y": False}]] if show_subplots else [[{"secondary_y": True}]]
51
+ )
52
+ for key in data_dict:
53
+ if chg:
54
+ draw_df[data_dict[key]] = (draw_df[data_dict[key]] + 1).fillna(1).cumprod()
55
+ fig.add_trace(go.Scatter(x=time_data, y=draw_df[data_dict[key]], name=key, ), row=1, col=1)
56
+
57
+ # 绘制右轴数据 (面积图 - 通常用于回撤)
58
+ if right_axis:
59
+ key = list(right_axis.keys())[0]
60
+ fig.add_trace(go.Scatter(x=time_data, y=draw_df[right_axis[key]], name=key + '(右轴)',
61
+ # marker=dict(color='rgba(220, 220, 220, 0.8)'),
62
+ marker_color='orange',
63
+ opacity=0.1, line=dict(width=0),
64
+ fill='tozeroy',
65
+ yaxis='y2')) # 标明设置一个不同于trace1的一个坐标轴
66
+ for key in list(right_axis.keys())[1:]:
67
+ fig.add_trace(go.Scatter(x=time_data, y=draw_df[right_axis[key]], name=key + '(右轴)',
68
+ # marker=dict(color='rgba(220, 220, 220, 0.8)'),
69
+ opacity=0.1, line=dict(width=0),
70
+ fill='tozeroy',
71
+ yaxis='y2')) # 标明设置一个不同于trace1的一个坐标轴
72
+
73
+ # 绘制右轴数据 (线图 - 通常用于价格)
74
+ if right_axis_lines:
75
+ for key in right_axis_lines:
76
+ fig.add_trace(go.Scatter(x=time_data, y=draw_df[right_axis_lines[key]], name=key + '(右轴)',
77
+ mode='lines',
78
+ line=dict(width=1.5, color='rgba(46, 204, 113, 0.9)'),
79
+ opacity=0.9,
80
+ yaxis='y2'))
81
+
82
+ if markers:
83
+ for m in markers:
84
+ fig.add_trace(go.Scatter(
85
+ x=[m['time']],
86
+ y=[m['price']],
87
+ mode='markers+text',
88
+ name=m.get('text', 'Marker'),
89
+ text=[m.get('text', '')],
90
+ textposition="top center",
91
+ marker=dict(
92
+ symbol=m.get('symbol', 'x'),
93
+ size=m.get('size', 15),
94
+ color=m.get('color', 'red'),
95
+ line=dict(width=2, color='white')
96
+ ),
97
+ yaxis='y2' if m.get('on_right_axis', False) else 'y1'
98
+ ), row=1, col=1, secondary_y=m.get('on_right_axis', False))
99
+
100
+ if show_subplots:
101
+ # 子图:按照 matplotlib stackplot 风格实现堆叠图
102
+ # 最下面是多头仓位占比
103
+ fig.add_trace(go.Scatter(
104
+ x=time_data,
105
+ y=draw_df['long_cum'],
106
+ mode='lines',
107
+ line=dict(width=0),
108
+ fill='tozeroy',
109
+ fillcolor='rgba(30, 177, 0, 0.6)',
110
+ name='多头仓位占比',
111
+ hovertemplate="多头仓位占比: %{customdata:.4f}<extra></extra>",
112
+ customdata=draw_df['long_pos_ratio'] # 使用原始比例值
113
+ ), row=2, col=1)
114
+
115
+ # 中间是空头仓位占比
116
+ fig.add_trace(go.Scatter(
117
+ x=time_data,
118
+ y=draw_df['short_cum'],
119
+ mode='lines',
120
+ line=dict(width=0),
121
+ fill='tonexty',
122
+ fillcolor='rgba(255, 99, 77, 0.6)',
123
+ name='空头仓位占比',
124
+ hovertemplate="空头仓位占比: %{customdata:.4f}<extra></extra>",
125
+ customdata=draw_df['short_pos_ratio'] # 使用原始比例值
126
+ ), row=2, col=1)
127
+
128
+ # 最上面是空仓占比
129
+ fig.add_trace(go.Scatter(
130
+ x=time_data,
131
+ y=draw_df['empty_cum'],
132
+ mode='lines',
133
+ line=dict(width=0),
134
+ fill='tonexty',
135
+ fillcolor='rgba(0, 46, 77, 0.6)',
136
+ name='空仓占比',
137
+ hovertemplate="空仓占比: %{customdata:.4f}<extra></extra>",
138
+ customdata=draw_df['empty_ratio'] # 使用原始比例值
139
+ ), row=2, col=1)
140
+
141
+ # 子图:右轴绘制 long_short_ratio 曲线
142
+ fig.add_trace(go.Scatter(
143
+ x=time_data,
144
+ y=draw_df['symbol_long_num'],
145
+ name='多头选币数量',
146
+ mode='lines',
147
+ line=dict(color='rgba(30, 177, 0, 0.6)', width=2)
148
+ ), row=3, col=1)
149
+
150
+ fig.add_trace(go.Scatter(
151
+ x=time_data,
152
+ y=draw_df['symbol_short_num'],
153
+ name='空头选币数量',
154
+ mode='lines',
155
+ line=dict(color='rgba(255, 99, 77, 0.6)', width=2)
156
+ ), row=3, col=1)
157
+
158
+ # 更新子图标题
159
+ fig.update_yaxes(title_text="仓位占比", row=2, col=1)
160
+ fig.update_yaxes(title_text="选币数量", row=3, col=1)
161
+
162
+ fig.update_layout(template="none", width=pic_size[0], height=pic_size[1], title_text=title,
163
+ hovermode="x unified", hoverlabel=dict(bgcolor='rgba(255,255,255,0.5)', ),
164
+ font=dict(family="PingFang SC, Hiragino Sans GB, Songti SC, Arial, sans-serif", size=12),
165
+ annotations=[
166
+ dict(
167
+ text=desc,
168
+ xref='paper',
169
+ yref='paper',
170
+ x=0.5,
171
+ y=1.05,
172
+ showarrow=False,
173
+ font=dict(size=12, color='black'),
174
+ align='center',
175
+ bgcolor='rgba(255,255,255,0.8)',
176
+ )
177
+ ]
178
+ )
179
+ fig.update_layout(
180
+ updatemenus=[
181
+ dict(
182
+ buttons=[
183
+ dict(label="线性 y轴",
184
+ method="relayout",
185
+ args=[{"yaxis.type": "linear"}]),
186
+ dict(label="对数 y轴",
187
+ method="relayout",
188
+ args=[{"yaxis.type": "log"}]),
189
+ ])],
190
+ )
191
+ plot(figure_or_data=fig, filename=str(path.resolve()), auto_open=False)
192
+
193
+ fig.update_yaxes(
194
+ showspikes=True, spikemode='across', spikesnap='cursor', spikedash='solid', spikethickness=1, # 峰线
195
+ )
196
+ fig.update_xaxes(
197
+ showspikes=True, spikemode='across+marker', spikesnap='cursor', spikedash='solid', spikethickness=1, # 峰线
198
+ )
199
+
200
+ # 打开图片的html文件,需要判断系统的类型
201
+ if show:
202
+ fig.show()
203
+
204
+
205
+ def plotly_plot(draw_df: pd.DataFrame, save_dir: str, name: str):
206
+ rows = len(draw_df.columns)
207
+ s = (1 / (rows - 1)) * 0.5
208
+ fig = subplots.make_subplots(rows=rows, cols=1, shared_xaxes=True, shared_yaxes=True, vertical_spacing=s)
209
+
210
+ for i, col_name in enumerate(draw_df.columns):
211
+ trace = go.Bar(x=draw_df.index, y=draw_df[col_name], name=f"{col_name}")
212
+ fig.add_trace(trace, i + 1, 1)
213
+ # 更新每个子图的x轴属性
214
+ fig.update_xaxes(showticklabels=True, row=i + 1, col=1) # 旋转x轴标签以避免重叠
215
+
216
+ # 更新每个子图的y轴标题
217
+ for i, col_name in enumerate(draw_df.columns):
218
+ fig.update_xaxes(title_text=col_name, row=i + 1, col=1)
219
+
220
+ fig.update_layout(height=200 * rows, showlegend=True, title_text=name)
221
+ fig.write_html(str((Path(save_dir) / f"{name}.html").resolve()))
222
+ fig.show()
223
+
224
+
225
+ def mat_heatmap(draw_df: pd.DataFrame, name: str):
226
+ sns.set() # 设置一下展示的主题和样式
227
+ plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans', 'Font 119']
228
+ plt.title(name) # 设置标题
229
+ sns.heatmap(draw_df, annot=True, xticklabels=draw_df.columns, yticklabels=draw_df.index, fmt='.2f') # 画图
230
+ plt.show()
基础库/common_core/backtest/metrics.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Quant Unified 量化交易系统
4
+ [统一回测指标模块]
5
+
6
+ 功能:
7
+ 为所有策略提供统一的回测绩效指标计算,避免重复开发。
8
+ 一次计算,处处复用 —— 所有策略(1-8号)都调用这个模块。
9
+
10
+ 支持的指标:
11
+ - 年化收益率 (CAGR)
12
+ - 对数收益率 (Log Return)
13
+ - 最大回撤 (Max Drawdown)
14
+ - 最大回撤恢复时间 (Recovery Time)
15
+ - 卡玛比率 (Calmar Ratio)
16
+ - 夏普比率 (Sharpe Ratio)
17
+ - 索提诺比率 (Sortino Ratio)
18
+ - 胜率 (Win Rate)
19
+ - 盈亏比 (Profit Factor)
20
+ - 交易次数 (Trade Count)
21
+
22
+ 使用方法:
23
+ ```python
24
+ from 基础库.common_core.backtest.metrics import 回测指标计算器
25
+
26
+ # 方式1: 传入权益曲线数组
27
+ 计算器 = 回测指标计算器(权益曲线=equity_values, 初始资金=10000)
28
+ 计算器.打印报告()
29
+ 指标字典 = 计算器.获取指标()
30
+
31
+ # 方式2: 传入 DataFrame (需包含 equity 列)
32
+ 计算器 = 回测指标计算器.从DataFrame创建(df, 权益列名='equity')
33
+ 计算器.打印报告()
34
+ ```
35
+ """
36
+
37
+ import numpy as np
38
+ import pandas as pd
39
+ from dataclasses import dataclass, field
40
+ from typing import Optional, Dict, Any, Union, List
41
+ from datetime import timedelta
42
+
43
+
44
+ @dataclass
45
+ class 回测指标结果:
46
+ """回测指标结果数据类"""
47
+ # 基础信息
48
+ 初始资金: float = 0.0
49
+ 最终资金: float = 0.0
50
+ 总收益: float = 0.0
51
+ 总收益率: float = 0.0
52
+
53
+ # 收益指标
54
+ 年化收益率: float = 0.0 # CAGR
55
+ 对数收益率: float = 0.0 # Log Return
56
+
57
+ # 风险指标
58
+ 最大回撤: float = 0.0 # Max Drawdown (负数)
59
+ 最大回撤百分比: str = "" # 格式化显示
60
+ 最大回撤开始时间: Optional[str] = None
61
+ 最大回撤结束时间: Optional[str] = None
62
+ 最大回撤恢复时间: Optional[str] = None # 恢复到前高的时间
63
+ 最大回撤恢复天数: int = 0
64
+
65
+ # 风险调整收益
66
+ 卡玛比率: float = 0.0 # Calmar Ratio = 年化收益 / |最大回撤|
67
+ 夏普比率: float = 0.0 # Sharpe Ratio
68
+ 索提诺比率: float = 0.0 # Sortino Ratio
69
+ 年化波动率: float = 0.0 # Annualized Volatility
70
+
71
+ # 交易统计
72
+ 总周期数: int = 0
73
+ 盈利周期数: int = 0
74
+ 亏损周期数: int = 0
75
+ 胜率: float = 0.0
76
+ 盈亏比: float = 0.0 # Profit Factor
77
+ 最大连续盈利周期: int = 0
78
+ 最大连续亏损周期: int = 0
79
+
80
+ # 其他
81
+ 交易次数: int = 0
82
+ 回测天数: int = 0
83
+
84
+ def 转为字典(self) -> Dict[str, Any]:
85
+ """转换为字典格式"""
86
+ return {
87
+ "初始资金": self.初始资金,
88
+ "最终资金": self.最终资金,
89
+ "总收益": self.总收益,
90
+ "总收益率": f"{self.总收益率:.2%}",
91
+ "年化收益率": f"{self.年化收益率:.2%}",
92
+ "对数收益率": f"{self.对数收益率:.4f}",
93
+ "最大回撤": f"{self.最大回撤:.2%}",
94
+ "最大回撤恢复天数": self.最大回撤恢复天数,
95
+ "卡玛比率": f"{self.卡玛比率:.2f}",
96
+ "夏普比率": f"{self.夏普比率:.2f}",
97
+ "索提诺比率": f"{self.索提诺比率:.2f}",
98
+ "年化波动率": f"{self.年化波动率:.2%}",
99
+ "胜率": f"{self.胜率:.2%}",
100
+ "盈亏比": f"{self.盈亏比:.2f}",
101
+ "交易次数": self.交易次数,
102
+ "回测天数": self.回测天数,
103
+ }
104
+
105
+
106
+ class 回测指标计算器:
107
+ """
108
+ 统一回测指标计算器
109
+
110
+ 这个类就像一个"成绩单生成器":
111
+ 你把考试成绩(权益曲线)交给它,它会自动帮你算出:
112
+ - 平均分是多少 (年化收益)
113
+ - 最差的一次考了多少 (最大回撤)
114
+ - 成绩稳不稳定 (夏普比率)
115
+ 等等一系列指标。
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ 权益曲线: Union[np.ndarray, List[float], pd.Series],
121
+ 初始资金: float = 10000.0,
122
+ 时间戳: Optional[Union[np.ndarray, List, pd.DatetimeIndex]] = None,
123
+ 持仓序列: Optional[Union[np.ndarray, List[float], pd.Series]] = None,
124
+ 无风险利率: float = 0.0, # 年化无风险利率,加密货币通常设为 0
125
+ 周期每年数量: int = 525600, # 分钟级数据: 365.25 * 24 * 60
126
+ ):
127
+ """
128
+ 初始化回测指标计算器
129
+
130
+ 参数:
131
+ 权益曲线: 账户总资产序列 (如 [10000, 10100, 10050, ...])
132
+ 初始资金: 初始本金
133
+ 时间戳: 可选,每个数据点对应的时间戳
134
+ 持仓序列: 可选,持仓状态序列 (如 [0, 1, 1, -1, 0, ...]),用于计算交易次数
135
+ 无风险利率: 年化无风险收益率,默认 0 (加密货币市场)
136
+ 周期每年数量: 每年有多少���周期,用于年化
137
+ - 分钟级: 525600 (365.25 * 24 * 60)
138
+ - 小时级: 8766 (365.25 * 24)
139
+ - 日级: 365
140
+ """
141
+ # 转换为 numpy 数组
142
+ self.权益 = np.array(权益曲线, dtype=np.float64)
143
+ self.初始资金 = float(初始资金)
144
+ self.无风险利率 = 无风险利率
145
+ self.周期每年数量 = 周期每年数量
146
+
147
+ # 时间戳处理
148
+ if 时间戳 is not None:
149
+ self.时间戳 = pd.to_datetime(时间戳)
150
+ else:
151
+ self.时间戳 = None
152
+
153
+ # 持仓序列
154
+ if 持仓序列 is not None:
155
+ self.持仓 = np.array(持仓序列, dtype=np.float64)
156
+ else:
157
+ self.持仓 = None
158
+
159
+ # 预计算
160
+ self._计算收益率序列()
161
+
162
+ def _计算收益率序列(self):
163
+ """计算周期收益率序列"""
164
+ # 简单收益率: (P_t - P_{t-1}) / P_{t-1}
165
+ self.收益率 = np.diff(self.权益) / self.权益[:-1]
166
+ # 处理 NaN 和 Inf
167
+ self.收益率 = np.nan_to_num(self.收益率, nan=0.0, posinf=0.0, neginf=0.0)
168
+
169
+ # 对数收益率: ln(P_t / P_{t-1})
170
+ with np.errstate(divide='ignore', invalid='ignore'):
171
+ self.对数收益率序列 = np.log(self.权益[1:] / self.权益[:-1])
172
+ self.对数收益率序列 = np.nan_to_num(self.对数收益率序列, nan=0.0, posinf=0.0, neginf=0.0)
173
+
174
+ @classmethod
175
+ def 从DataFrame创建(
176
+ cls,
177
+ df: pd.DataFrame,
178
+ 权益列名: str = 'equity',
179
+ 时间列名: str = 'candle_begin_time',
180
+ 持仓列名: Optional[str] = None,
181
+ 初始资金: Optional[float] = None,
182
+ **kwargs
183
+ ) -> '回测指标计算器':
184
+ """
185
+ 从 DataFrame 创建计算器
186
+
187
+ 参数:
188
+ df: 包含回测数据的 DataFrame
189
+ 权益列名: 权益/净值列的名称
190
+ 时间列名: 时间戳列的名称
191
+ 持仓列名: 可选,持仓列的名称
192
+ 初始资金: 可选,如不提供则使用权益曲线第一个值
193
+ """
194
+ 权益 = df[权益列名].values
195
+
196
+ 时间戳 = None
197
+ if 时间列名 in df.columns:
198
+ 时间戳 = df[时间列名].values
199
+ elif isinstance(df.index, pd.DatetimeIndex):
200
+ 时间戳 = df.index
201
+
202
+ 持仓 = None
203
+ if 持仓列名 and 持仓列名 in df.columns:
204
+ 持仓 = df[持仓列名].values
205
+
206
+ if 初始资金 is None:
207
+ 初始资金 = 权益[0]
208
+
209
+ return cls(
210
+ 权益曲线=权益,
211
+ 初始资金=初始资金,
212
+ 时间戳=时间戳,
213
+ 持仓序列=持仓,
214
+ **kwargs
215
+ )
216
+
217
+ def 计算全部指标(self) -> 回测指标结果:
218
+ """计算所有回测指标,返回结构化结果"""
219
+ 结果 = 回测指标结果()
220
+
221
+ # 1. 基础信息
222
+ 结果.初始资金 = self.初始资金
223
+ 结果.最终资金 = self.权益[-1]
224
+ 结果.总收益 = 结果.最终资金 - 结果.初始资金
225
+ 结果.总收益率 = 结果.总收益 / 结果.初始资金
226
+ 结果.总周期数 = len(self.权益)
227
+
228
+ # 2. 回测天数
229
+ if self.时间戳 is not None and len(self.时间戳) > 1:
230
+ 结果.回测天数 = (self.时间戳[-1] - self.时间戳[0]).days
231
+ else:
232
+ 结果.回测天数 = len(self.权益) // (24 * 60) # 假设分钟级数据
233
+
234
+ # 3. 年化收益率 (CAGR)
235
+ # CAGR = (最终净值 / 初始净值) ^ (1 / 年数) - 1
236
+ 年数 = max(结果.回测天数 / 365.25, 0.001) # 防止除零
237
+ 净值终点 = 结果.最终资金 / 结果.初始资金
238
+ if 净值终点 > 0:
239
+ 结果.年化收益率 = (净值终点 ** (1 / 年数)) - 1
240
+ else:
241
+ 结果.年化收益率 = -1.0
242
+
243
+ # 4. 对数收益率 (总体)
244
+ # Log Return = ln(最终净值 / 初始净值)
245
+ if 净值终点 > 0:
246
+ 结果.对数收益率 = np.log(净值终点)
247
+ else:
248
+ 结果.对数收益率 = float('-inf')
249
+
250
+ # 5. 最大回撤
251
+ 回撤结果 = self._计算最大回撤()
252
+ 结果.最大回撤 = 回撤结果['最大回撤']
253
+ 结果.最大回撤百分比 = f"{回撤结果['最大回撤']:.2%}"
254
+ 结果.最大回撤开始时间 = 回撤结果.get('开始时间')
255
+ 结果.最大回撤结束时间 = 回撤结果.get('结束时间')
256
+ 结果.最大回撤恢复时间 = 回撤结果.get('恢复时间')
257
+ 结果.最大回撤恢复天数 = 回撤结果.get('恢复天数', 0)
258
+
259
+ # 6. 卡玛比率 (Calmar Ratio)
260
+ # Calmar = 年化收益率 / |最大回撤|
261
+ if abs(结果.最大回撤) > 1e-9:
262
+ 结果.卡玛比率 = 结果.年化收益率 / abs(结果.最大回撤)
263
+ else:
264
+ 结果.卡玛比率 = float('inf') if 结果.年化收益率 > 0 else 0.0
265
+
266
+ # 7. 波动率和夏普比率
267
+ if len(self.收益率) > 1:
268
+ # 年化波动率 = 周期标准差 * sqrt(周期每年数量)
269
+ 结果.年化波动率 = np.std(self.收益率) * np.sqrt(self.周期每年数量)
270
+
271
+ # 夏普比率 = (年化收益 - 无风险利率) / 年化波动率
272
+ if 结果.年化波动率 > 1e-9:
273
+ 结果.夏普比率 = (结果.年化收益率 - self.无风险利率) / 结果.年化波动率
274
+ else:
275
+ 结果.夏普比率 = 0.0
276
+
277
+ # 索提诺比率 = (年化收益 - 无风险利率) / 下行波动率
278
+ 下行收益 = self.收益率[self.收益率 < 0]
279
+ if len(下行收益) > 0:
280
+ 下行波动率 = np.std(下行收益) * np.sqrt(self.周期每年数量)
281
+ if 下行波动率 > 1e-9:
282
+ 结果.索提诺比率 = (结果.年化收益率 - self.无风险利率) / 下行波动率
283
+
284
+ # 8. 胜率和盈亏比
285
+ 盈利周期 = self.收益率[self.收益率 > 0]
286
+ 亏损周期 = self.收益率[self.收益率 < 0]
287
+
288
+ 结果.盈利周期数 = len(盈利周期)
289
+ 结果.亏损周期数 = len(亏损周期)
290
+
291
+ if len(self.收益率) > 0:
292
+ 结果.胜率 = 结果.盈利周期数 / len(self.收益率)
293
+
294
+ if len(亏损周期) > 0 and np.sum(np.abs(亏损周期)) > 1e-9:
295
+ 结果.盈亏比 = np.sum(盈利周期) / np.abs(np.sum(亏损周期))
296
+
297
+ # 9. 连续盈亏
298
+ 结果.最大连续盈利周期 = self._计算最大连续(self.收益率 > 0)
299
+ 结果.最大连续亏损周期 = self._计算最大连续(self.收益率 < 0)
300
+
301
+ # 10. 交易次数 (如果有持仓序列)
302
+ if self.持仓 is not None:
303
+ # 持仓变化就是交易
304
+ 结果.交易次数 = int(np.sum(np.abs(np.diff(self.持仓)) > 0))
305
+
306
+ return 结果
307
+
308
+ def _计算最大回撤(self) -> Dict[str, Any]:
309
+ """
310
+ 计算最大回撤及相关信息
311
+
312
+ 最大回撤 = (峰值 - 谷值) / 峰值
313
+ 就像股票从最高点跌到最低点的幅度
314
+ """
315
+ if len(self.权益) < 2:
316
+ return {'最大回撤': 0.0}
317
+
318
+ # 计算滚动最高点 (累计最大值)
319
+ 累计最高 = np.maximum.accumulate(self.权益)
320
+
321
+ # 计算回撤 (当前值相对于历史最高的跌幅)
322
+ # 回撤 = (当前值 - 最高值) / 最高值 (负数或零)
323
+ 回撤序列 = (self.权益 - 累计最高) / 累计最高
324
+
325
+ # 最大回撤位置 (最低点)
326
+ 最大回撤索引 = np.argmin(回撤序列)
327
+ 最大回撤值 = 回撤序列[最大回撤索引]
328
+
329
+ # 最大回撤开始位置 (在最低点之前的最高点)
330
+ 峰值索引 = np.argmax(self.权益[:最大回撤索引 + 1])
331
+
332
+ 结果 = {
333
+ '最大回撤': 最大回撤值,
334
+ '回撤开始索引': 峰值索引,
335
+ '回撤结束索引': 最大回撤索引,
336
+ }
337
+
338
+ # 如果有时间戳,添加时间信息
339
+ if self.时间戳 is not None:
340
+ 结果['开始时间'] = str(self.时间戳[峰值索引])
341
+ 结果['结束时间'] = str(self.时间戳[最大回撤索引])
342
+
343
+ # 计算恢复时间 (从最低点恢复到前高)
344
+ 峰值 = self.权益[峰值索引]
345
+ 恢复索引 = None
346
+ for i in range(最大回撤索引 + 1, len(self.权益)):
347
+ if self.权益[i] >= 峰值:
348
+ 恢复索引 = i
349
+ break
350
+
351
+ if 恢复索引 is not None:
352
+ 结果['恢复时间'] = str(self.时间戳[恢复索引])
353
+ 结果['恢复天数'] = (self.时间戳[恢复索引] - self.时间戳[最大回撤索引]).days
354
+ else:
355
+ 结果['恢复天数'] = -1 # 未恢复
356
+ 结果['恢复时间'] = "未恢复"
357
+
358
+ return 结果
359
+
360
+ def _计算最大连续(self, 条件数组: np.ndarray) -> int:
361
+ """计算最大连续满足条件的周期数"""
362
+ if len(条件数组) == 0:
363
+ return 0
364
+
365
+ 最大连续 = 0
366
+ 当前连续 = 0
367
+
368
+ for 满足条件 in 条件数组:
369
+ if 满足条件:
370
+ 当前连续 += 1
371
+ 最大连续 = max(最大连续, 当前连续)
372
+ else:
373
+ 当前连续 = 0
374
+
375
+ return 最大连续
376
+
377
+ def 获取指标(self) -> Dict[str, Any]:
378
+ """获取指标字典"""
379
+ return self.计算全部指标().转为字典()
380
+
381
+ def 打印报告(self, 策略名称: str = "策略"):
382
+ """
383
+ 打印格式化的回测报告
384
+
385
+ 输出一个漂亮的表格,展示所有关键指标
386
+ """
387
+ 结果 = self.计算全部指标()
388
+
389
+ # 构建分隔线
390
+ 宽度 = 50
391
+ 分隔线 = "═" * 宽度
392
+ 细分隔线 = "─" * 宽度
393
+
394
+ print()
395
+ print(f"╔{分隔线}╗")
396
+ print(f"║{'📊 ' + 策略名称 + ' 回测报告':^{宽度-2}}║")
397
+ print(f"╠{分隔线}╣")
398
+
399
+ # 基础信息
400
+ print(f"║ {'💰 初始资金':<15}: {结果.初始资金:>18,.2f} USDT ║")
401
+ print(f"║ {'💎 最终资金':<15}: {结果.最终资金:>18,.2f} USDT ║")
402
+ print(f"║ {'📈 总收益率':<15}: {结果.总收益率:>18.2%} ║")
403
+ print(f"╠{细分隔线}╣")
404
+
405
+ # 收益指标
406
+ print(f"║ {'📅 年化收益率':<14}: {结果.年化收益率:>18.2%} ║")
407
+ print(f"║ {'📐 对数收益率':<14}: {结果.对数收益率:>18.4f} ║")
408
+ print(f"╠{细分隔线}╣")
409
+
410
+ # 风险指标
411
+ print(f"║ {'🌊 最大回撤':<15}: {结果.最大回撤:>18.2%} ║")
412
+ if 结果.最大回撤恢复天数 > 0:
413
+ print(f"║ {'⏱️ 回撤恢复天数':<13}: {结果.最大回撤恢复天数:>18} 天 ║")
414
+ elif 结果.最大回撤恢复天数 == -1:
415
+ print(f"║ {'⏱️ 回撤恢复天数':<13}: {'未恢复':>21} ║")
416
+ print(f"╠{细分隔线}╣")
417
+
418
+ # 风险调整收益
419
+ print(f"║ {'⚖️ 卡玛比率':<14}: {结果.卡玛比率:>18.2f} ║")
420
+ print(f"║ {'📊 夏普比率':<15}: {结果.夏普比率:>18.2f} ║")
421
+ print(f"║ {'📉 索提诺比率':<14}: {结果.索提诺比率:>18.2f} ║")
422
+ print(f"║ {'📈 年化波动率':<14}: {结果.年化波动率:>18.2%} ║")
423
+ print(f"╠{细分隔线}╣")
424
+
425
+ # 交易统计
426
+ print(f"║ {'🎯 胜率':<16}: {结果.胜率:>18.2%} ║")
427
+ print(f"║ {'💹 盈亏比':<15}: {结果.盈亏比:>18.2f} ║")
428
+ if 结果.交易次数 > 0:
429
+ print(f"║ {'🔄 交易次数':<15}: {结果.交易次数:>18} ║")
430
+ print(f"║ {'📆 回测天数':<15}: {结果.回测天数:>18} 天 ║")
431
+
432
+ print(f"╚{分隔线}╝")
433
+ print()
434
+
435
+ return 结果
436
+
437
+
438
+ # ============== 便捷函数 ==============
439
+
440
+ def 快速计算指标(
441
+ 权益曲线: Union[np.ndarray, List[float]],
442
+ 初始资金: float = 10000.0,
443
+ 打印: bool = True,
444
+ 策略名称: str = "策略"
445
+ ) -> Dict[str, Any]:
446
+ """
447
+ 快速计算回测指标的便捷函数
448
+
449
+ 使用方法:
450
+ from 基础库.common_core.backtest.metrics import 快速计算指标
451
+
452
+ 指标 = 快速计算指标(equity_list, 初始资金=10000)
453
+ """
454
+ 计算器 = 回测指标计算器(权益曲线=权益曲线, 初始资金=初始资金)
455
+ if 打印:
456
+ 计算器.打印报告(策略名称=策略名称)
457
+ return 计算器.获取指标()
458
+
459
+
460
+ # ============== 测试代码 ==============
461
+
462
+ if __name__ == "__main__":
463
+ # 生成模拟权益曲线 (用于测试)
464
+ np.random.seed(42)
465
+ 天数 = 365
466
+ 每天周期数 = 1440 # 分钟级
467
+ 总周期 = 天数 * 每天周期数
468
+
469
+ # 模拟一个有波动的权益曲线
470
+ 收益率 = np.random.normal(0.00001, 0.0005, 总周期) # 微小正漂移 + 波动
471
+ 权益 = 10000 * np.cumprod(1 + 收益率)
472
+
473
+ # 插入一个大回撤
474
+ 权益[int(总周期*0.3):int(总周期*0.4)] *= 0.7
475
+
476
+ # 测试计算器
477
+ 计算器 = 回测指标计算器(权益曲线=权益, 初始资金=10000, 周期每年数量=每天周期数*365)
478
+ 计算器.打印报告(策略名称="测试策略")
基础库/common_core/backtest/rebalance.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ rebalance.py
4
+ """
5
+
6
+ import numpy as np
7
+ import numba as nb
8
+ from numba.experimental import jitclass
9
+
10
+
11
+ @jitclass
12
+ class RebAlways:
13
+ spot_lot_sizes: nb.float64[:] # 每手币数,表示一手加密货币中包含的币数
14
+ swap_lot_sizes: nb.float64[:]
15
+
16
+ def __init__(self, spot_lot_sizes, swap_lot_sizes):
17
+ n_syms_spot = len(spot_lot_sizes)
18
+ n_syms_swap = len(swap_lot_sizes)
19
+
20
+ self.spot_lot_sizes = np.zeros(n_syms_spot, dtype=np.float64)
21
+ self.spot_lot_sizes[:] = spot_lot_sizes
22
+
23
+ self.swap_lot_sizes = np.zeros(n_syms_swap, dtype=np.float64)
24
+ self.swap_lot_sizes[:] = swap_lot_sizes
25
+
26
+ def _calc(self, equity, prices, ratios, lot_sizes):
27
+ # 初始化目标持仓手数
28
+ target_lots = np.zeros(len(lot_sizes), dtype=np.int64)
29
+
30
+ # 每个币分配的资金(带方向)
31
+ symbol_equity = equity * ratios
32
+
33
+ # 分配资金大于 0.01U 则认为是有效持仓
34
+ mask = np.abs(symbol_equity) > 0.01
35
+
36
+ # 为有效持仓分配仓位
37
+ target_lots[mask] = (symbol_equity[mask] / prices[mask] / lot_sizes[mask]).astype(np.int64)
38
+
39
+ return target_lots
40
+
41
+ def calc_lots(self, equity, spot_prices, spot_lots, spot_ratios, swap_prices, swap_lots, swap_ratios):
42
+ """
43
+ 计算每个币种的目标手数
44
+ :param equity: 总权益
45
+ :param spot_prices: 现货最新价格
46
+ :param spot_lots: 现货当前持仓手数
47
+ :param spot_ratios: 现货币种的资金比例
48
+ :param swap_prices: 合约最新价格
49
+ :param swap_lots: 合约当前持仓手数
50
+ :param swap_ratios: 合约币种的资金比例
51
+ :return: tuple[现货目标手数, 合约目标手数]
52
+ """
53
+ is_spot_only = False
54
+
55
+ # 合约总权重小于极小值,认为是纯现货模式
56
+ if np.sum(np.abs(swap_ratios)) < 1e-6:
57
+ is_spot_only = True
58
+ equity *= 0.99 # 纯现货留 1% 的资金作为缓冲
59
+
60
+ # 现货目标持仓手数
61
+ spot_target_lots = self._calc(equity, spot_prices, spot_ratios, self.spot_lot_sizes)
62
+
63
+ if is_spot_only:
64
+ swap_target_lots = np.zeros(len(self.swap_lot_sizes), dtype=np.int64)
65
+ return spot_target_lots, swap_target_lots
66
+
67
+ # 合约目标持仓手数
68
+ swap_target_lots = self._calc(equity, swap_prices, swap_ratios, self.swap_lot_sizes)
69
+
70
+ return spot_target_lots, swap_target_lots
基础库/common_core/backtest/signal.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ signal.py
4
+
5
+ 概率信号 -> 仓位(带迟滞 / hysteresis)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numba as nb
11
+ import numpy as np
12
+
13
+
14
+ @nb.njit(cache=True)
15
+ def positions_from_probs_hysteresis(
16
+ p_up: np.ndarray,
17
+ p_down: np.ndarray,
18
+ p_enter: float = 0.55,
19
+ p_exit: float = 0.55,
20
+ diff_enter: float = 0.0,
21
+ diff_exit: float = 0.0,
22
+ init_pos: int = 0,
23
+ ) -> np.ndarray:
24
+ """
25
+ 将 (p_up, p_down) 转换成 {-1,0,1} 仓位序列,并加入迟滞以降低来回翻仓。
26
+
27
+ 规则(默认):
28
+ - 空仓:p_up>=p_enter 且 (p_up-p_down)>=diff_enter -> 做多;
29
+ p_down>=p_enter 且 (p_down-p_up)>=diff_enter -> 做空;
30
+ - 多仓:p_down>=p_exit 且 (p_down-p_up)>=diff_exit -> 反手做空;
31
+ - 空仓:p_up>=p_exit 且 (p_up-p_down)>=diff_exit -> 反手做多;
32
+ - 其他情况保持原仓位。
33
+ """
34
+ n = len(p_up)
35
+ pos = np.empty(n, dtype=np.int8)
36
+ cur = np.int8(init_pos)
37
+
38
+ for i in range(n):
39
+ up = p_up[i]
40
+ down = p_down[i]
41
+
42
+ if np.isnan(up) or np.isnan(down):
43
+ pos[i] = cur
44
+ continue
45
+
46
+ if cur == 0:
47
+ if up >= p_enter and (up - down) >= diff_enter:
48
+ cur = np.int8(1)
49
+ elif down >= p_enter and (down - up) >= diff_enter:
50
+ cur = np.int8(-1)
51
+ elif cur == 1:
52
+ if down >= p_exit and (down - up) >= diff_exit:
53
+ cur = np.int8(-1)
54
+ else: # cur == -1
55
+ if up >= p_exit and (up - down) >= diff_exit:
56
+ cur = np.int8(1)
57
+
58
+ pos[i] = cur
59
+
60
+ return pos
61
+
62
+
63
+ def positions_from_probs_hysteresis_py(
64
+ p_up: np.ndarray,
65
+ p_down: np.ndarray,
66
+ p_enter: float = 0.55,
67
+ p_exit: float = 0.55,
68
+ diff_enter: float = 0.0,
69
+ diff_exit: float = 0.0,
70
+ init_pos: int = 0,
71
+ ) -> np.ndarray:
72
+ """
73
+ Python 版本(便于调试),输出与 numba 版一致。
74
+ """
75
+ p_up = np.asarray(p_up, dtype=float)
76
+ p_down = np.asarray(p_down, dtype=float)
77
+ out = np.empty(len(p_up), dtype=np.int8)
78
+ cur = np.int8(init_pos)
79
+
80
+ for i, (up, down) in enumerate(zip(p_up, p_down)):
81
+ if np.isnan(up) or np.isnan(down):
82
+ out[i] = cur
83
+ continue
84
+ if cur == 0:
85
+ if up >= p_enter and (up - down) >= diff_enter:
86
+ cur = np.int8(1)
87
+ elif down >= p_enter and (down - up) >= diff_enter:
88
+ cur = np.int8(-1)
89
+ elif cur == 1:
90
+ if down >= p_exit and (down - up) >= diff_exit:
91
+ cur = np.int8(-1)
92
+ else:
93
+ if up >= p_exit and (up - down) >= diff_exit:
94
+ cur = np.int8(1)
95
+ out[i] = cur
96
+
97
+ return out
98
+
基础库/common_core/backtest/simulator.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [高性能回测仿真器]
4
+ 功能:基于 Numba 加速,模拟交易所撮合逻辑,处理开平仓、资金费结算,提供极速的回测执行。
5
+ """
6
+ import numba as nb
7
+ import numpy as np
8
+ from numba.experimental import jitclass
9
+
10
+ """
11
+ # 新语法小讲堂
12
+ 通过操作对象的值而不是更换reference,来保证所有引用的位置都能同步更新。
13
+
14
+ `self.target_lots[:] = target_lots`
15
+ 这个写法涉及 Python 中的切片(slice)操作和对象的属性赋值。
16
+
17
+ `target_lots: nb.int64[:] # 目标持仓手数`,self.target_lots 是一个列表,`[:]` 是切片操作符,表示对整个列表进行切片。
18
+
19
+ ### 详细解释:
20
+
21
+ 1. **`self.target_lots[:] = target_lots`**:
22
+ - `self.target_lots` 是对象的一个属性,通常是一个列表(或者其它支持切片操作的可变序列)。
23
+ - `[:]` 是切片操作符,表示对整个列表进行切片。具体来说,`[:]` 是对列表的所有元素进行选择,这种写法可以用于复制列表或对整个列表内容进行替换。
24
+
25
+ 2. **具体操作**:
26
+ - `self.target_lots[:] = target_lots` 不是直接将 `target_lots` 赋值给 `self.target_lots`,而是将 `target_lots` 中的所有元素替换 `self.target_lots` 中的所有元素。
27
+ - 这种做法的一个好处是不会改变 `self.target_lots` 对象的引用,而是修改它的内容。这在有其他对象引用 `self.target_lots` 时非常有用,确保所有引用者看到的列表内容都被更新,而不会因为重新赋值而改变列表的引用。
28
+
29
+ ### 举个例子:
30
+
31
+ ```python
32
+ a = [1, 2, 3]
33
+ b = a
34
+ a[:] = [4, 5, 6] # 只改变列表内容,不改变引用
35
+
36
+ print(a) # 输出: [4, 5, 6]
37
+ print(b) # 输出: [4, 5, 6],因为 a 和 b 引用的是同一个列表,修改 a 的内容也影响了 b
38
+ ```
39
+
40
+ 如果直接用 `a = [4, 5, 6]` 替换 `[:]` 操作,那么 `b` 就不会受到影响,因为 `a` 重新指向了一个新的列表对象。
41
+ """
42
+
43
+
44
+ @jitclass
45
+ class Simulator:
46
+ equity: float # 账户权益, 单位 USDT
47
+ fee_rate: float # 手续费率(单边)
48
+ slippage_rate: float # 滑点率(单边,按成交额计)
49
+ min_order_limit: float # 最小下单金额
50
+
51
+ lot_sizes: nb.float64[:] # 每手币数,表示一手加密货币中包含的币数
52
+ lots: nb.int64[:] # 当前持仓手数
53
+ target_lots: nb.int64[:] # 目标持仓手数
54
+
55
+ last_prices: nb.float64[:] # 最新价格
56
+ has_last_prices: bool # 是否有最新价
57
+
58
+ def __init__(self, init_capital, lot_sizes, fee_rate, slippage_rate, init_lots, min_order_limit):
59
+ """
60
+ 初始化
61
+ :param init_capital: 初始资金
62
+ :param lot_sizes: 每个币种的最小下单量
63
+ :param fee_rate: 手续费率(单边)
64
+ :param slippage_rate: 滑点率(单边,按成交额计)
65
+ :param init_lots: 初始持仓
66
+ :param min_order_limit: 最小下单金额
67
+ """
68
+ self.equity = init_capital # 账户权益
69
+ self.fee_rate = fee_rate # 手续费
70
+ self.slippage_rate = slippage_rate # 滑点
71
+ self.min_order_limit = min_order_limit # 最小下单金额
72
+
73
+ n = len(lot_sizes)
74
+
75
+ # 合约面值
76
+ self.lot_sizes = np.zeros(n, dtype=np.float64)
77
+ self.lot_sizes[:] = lot_sizes
78
+
79
+ # 前收盘价
80
+ self.last_prices = np.zeros(n, dtype=np.float64)
81
+ self.has_last_prices = False
82
+
83
+ # 当前持仓手数
84
+ self.lots = np.zeros(n, dtype=np.int64)
85
+ self.lots[:] = init_lots
86
+
87
+ # 目标持仓手数
88
+ self.target_lots = np.zeros(n, dtype=np.int64)
89
+ self.target_lots[:] = init_lots
90
+
91
+ def set_target_lots(self, target_lots):
92
+ self.target_lots[:] = target_lots
93
+
94
+ def fill_last_prices(self, prices):
95
+ mask = np.logical_not(np.isnan(prices))
96
+ self.last_prices[mask] = prices[mask]
97
+ self.has_last_prices = True
98
+
99
+ def settle_equity(self, prices):
100
+ """
101
+ 结算当前账户权益
102
+ :param prices: 当前价格
103
+ :return:
104
+ """
105
+ mask = np.logical_and(self.lots != 0, np.logical_not(np.isnan(prices)))
106
+ # 计算公式:
107
+ # 1. 净值涨跌 = (最新价格 - 前最新价(前收盘价)) * 持币数量。
108
+ # 2. 其中,持币数量 = min_qty * 持仓手数。
109
+ # 3. 所有币种对应的净值涨跌累加起来
110
+ equity_delta = np.sum((prices[mask] - self.last_prices[mask]) * self.lot_sizes[mask] * self.lots[mask])
111
+
112
+ # 反映到净值上
113
+ self.equity += equity_delta
114
+
115
+ def on_open(self, open_prices, funding_rates, mark_prices):
116
+ """
117
+ 模拟: K 线开盘 -> K 线收盘时刻
118
+ :param open_prices: 开盘价
119
+ :param funding_rates: 资金费
120
+ :param mark_prices: 计算资金费的标记价格(目前就用开盘价来)
121
+ :return:
122
+ """
123
+ if not self.has_last_prices:
124
+ self.fill_last_prices(open_prices)
125
+
126
+ # 根据开盘价和前最新价(前收盘价),结算当前账户权益
127
+ self.settle_equity(open_prices)
128
+
129
+ # 根据标记价格和资金费率,结算资金费盈亏
130
+ mask = np.logical_and(self.lots != 0, np.logical_not(np.isnan(mark_prices)))
131
+ pos_val = notional_value = self.lot_sizes[mask] * self.lots[mask] * mark_prices[mask]
132
+ funding_fee = np.sum(notional_value * funding_rates[mask])
133
+ self.equity -= funding_fee
134
+
135
+ # 最新价为开盘价
136
+ self.fill_last_prices(open_prices)
137
+
138
+ # 返回扣除资金费后开盘账户权益、资金费和带方向的仓位名义价值
139
+ return self.equity, funding_fee, pos_val
140
+
141
+ def on_execution(self, exec_prices):
142
+ """
143
+ 模拟: K 线开盘时刻 -> 调仓时刻
144
+ :param exec_prices: 执行价格
145
+ :return: 调仓后的账户权益、调仓后的仓位名义价值
146
+ """
147
+ if not self.has_last_prices:
148
+ self.fill_last_prices(exec_prices)
149
+
150
+ # 根据调仓价和前最新价(开盘价),结算当前账户权益
151
+ self.settle_equity(exec_prices)
152
+
153
+ # 计算需要买入或卖出的合约数量
154
+ delta = self.target_lots - self.lots
155
+ mask = np.logical_and(delta != 0, np.logical_not(np.isnan(exec_prices)))
156
+
157
+ # 计算成交额
158
+ turnover = np.zeros(len(self.lot_sizes), dtype=np.float64)
159
+ turnover[mask] = np.abs(delta[mask]) * self.lot_sizes[mask] * exec_prices[mask]
160
+
161
+ # 成交额小于 min_order_limit 则无法调仓
162
+ mask = np.logical_and(mask, turnover >= self.min_order_limit)
163
+
164
+ # 本期调仓总成交额
165
+ turnover_total = turnover[mask].sum()
166
+
167
+ if np.isnan(turnover_total):
168
+ raise RuntimeError('Turnover is nan')
169
+
170
+ # 根据总成交额计算并扣除手续费 + 滑点(均按单边成交额计)
171
+ cost = turnover_total * (self.fee_rate + self.slippage_rate)
172
+ self.equity -= cost
173
+
174
+ # 更新已成功调仓的 symbol 持仓
175
+ self.lots[mask] = self.target_lots[mask]
176
+
177
+ # 最新价为调仓价
178
+ self.fill_last_prices(exec_prices)
179
+
180
+ # 返回扣除交易成本后的调仓后账户权益,成交额,和交易成本
181
+ return self.equity, turnover_total, cost
182
+
183
+ def on_close(self, close_prices):
184
+ """
185
+ 模拟: K 线收盘 -> K 线收盘时刻
186
+ :param close_prices: 收盘价
187
+ :return: 收盘后的账户权益
188
+ """
189
+ if not self.has_last_prices:
190
+ self.fill_last_prices(close_prices)
191
+
192
+ # 模拟: 调仓时刻 -> K 线收盘时刻
193
+
194
+ # 根据收盘价和前最新价(调仓价),结算当前账户权益
195
+ self.settle_equity(close_prices)
196
+
197
+ # 最新价为收盘价
198
+ self.fill_last_prices(close_prices)
199
+
200
+ mask = np.logical_and(self.lots != 0, np.logical_not(np.isnan(close_prices)))
201
+ pos_val = self.lot_sizes[mask] * self.lots[mask] * close_prices[mask]
202
+
203
+ # 返回收盘账户权益
204
+ return self.equity, pos_val
基础库/common_core/backtest/version.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ version.py
4
+ """
5
+ sys_name = 'select-strategy'
6
+ sys_version = '1.0.2'
7
+ build_version = 'v1.0.2.20241122'
基础库/common_core/backtest/进度条.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Quant Unified 量化交易系统
4
+ [回测进度条模块]
5
+
6
+ 功能:
7
+ 提供统一的进度条显示,让用户知道回测进行到哪了、还要等多久。
8
+ 底层使用 tqdm 库,但封装成中文接口方便使用。
9
+
10
+ 使用方法:
11
+ ```python
12
+ from 基础库.common_core.backtest.进度条 import 回测进度条
13
+
14
+ # 方式1: 作为上下文管理器
15
+ with 回测进度条(总数=len(prices), 描述="回测中") as 进度:
16
+ for i in range(len(prices)):
17
+ # 你的回测逻辑...
18
+ 进度.更新(1)
19
+
20
+ # 方式2: 手动控制
21
+ 进度 = 回测进度条(总数=1000, 描述="处理数据")
22
+ for i in range(1000):
23
+ # 处理逻辑...
24
+ 进度.更新(1)
25
+ 进度.关闭()
26
+ ```
27
+
28
+ 显示效果:
29
+ 回测中: 45%|████████████░░░░░░░░░░░░| 450000/1000000 [01:23<01:42, 5432.10 it/s]
30
+ """
31
+
32
+ from tqdm import tqdm
33
+ from typing import Optional
34
+
35
+
36
+ class 回测进度条:
37
+ """
38
+ 回测进度条封装类
39
+
40
+ 这个类就像一个"加载条":
41
+ 当你在下载文件或安装软件时,会看到一个进度条告诉你:
42
+ - 完成了多少 (45%)
43
+ - 已经用了多久 (01:23)
44
+ - 还需要多久 (01:42)
45
+ - 速度多快 (5432 条/秒)
46
+
47
+ 这里我们把它用在回测上,让你知道回测还要跑多久。
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ 总数: int,
53
+ 描述: str = "回测进行中",
54
+ 单位: str = " bar",
55
+ 刷新间隔: float = 0.1,
56
+ 最小更新间隔: float = 0.5,
57
+ 禁用: bool = False,
58
+ 留存: bool = True
59
+ ):
60
+ """
61
+ 初始化进度条
62
+
63
+ 参数:
64
+ 总数: 总共需要处理的数量 (比如 K线条数)
65
+ 描述: 进度条前面显示的文字描述
66
+ 单位: 显示的单位 (如 "条", "bar", "根K线")
67
+ 刷新间隔: 进度条刷新频率 (秒)
68
+ 最小更新间隔: 最小更新间隔 (秒),防止刷新太频繁拖慢速度
69
+ 禁用: 是否禁用进度条 (静默模式)
70
+ 留存: 完成后是否保留进度条显示
71
+ """
72
+ self.总数 = 总数
73
+ self.禁用 = 禁用
74
+
75
+ # 配置 tqdm 进度条
76
+ self._进度条 = tqdm(
77
+ total=总数,
78
+ desc=描述,
79
+ unit=单位,
80
+ mininterval=刷新间隔,
81
+ miniters=max(1, 总数 // 1000), # 至少每 0.1% 更新一次
82
+ disable=禁用,
83
+ leave=留存,
84
+ ncols=100, # 进度条宽度
85
+ bar_format='{desc}: {percentage:3.0f}%|{bar:25}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'
86
+ )
87
+
88
+ self._当前 = 0
89
+
90
+ def 更新(self, 步数: int = 1):
91
+ """
92
+ 更新进度
93
+
94
+ 参数:
95
+ 步数: 本次完成的数量 (默认为1)
96
+ """
97
+ self._进度条.update(步数)
98
+ self._当前 += 步数
99
+
100
+ def 设置描述(self, 描述: str):
101
+ """动态更新进度条描述文字"""
102
+ self._进度条.set_description(描述)
103
+
104
+ def 设置后缀(self, **kwargs):
105
+ """
106
+ 设置进度条后缀信息
107
+
108
+ 示例:
109
+ 进度.设置后缀(收益率="12.5%", 回撤="-3.2%")
110
+ """
111
+ self._进度条.set_postfix(**kwargs)
112
+
113
+ def 关闭(self):
114
+ """关闭进度条"""
115
+ self._进度条.close()
116
+
117
+ def __enter__(self):
118
+ """进入上下文管理器"""
119
+ return self
120
+
121
+ def __exit__(self, exc_type, exc_val, exc_tb):
122
+ """退出上下文管理器,自动关闭进度条"""
123
+ self.关闭()
124
+ return False # 不吞掉异常
125
+
126
+ @property
127
+ def 已完成数量(self) -> int:
128
+ """获取已完成的数量"""
129
+ return self._当前
130
+
131
+ @property
132
+ def 完成百分比(self) -> float:
133
+ """获取完成百分比 (0-100)"""
134
+ if self.总数 == 0:
135
+ return 100.0
136
+ return (self._当前 / self.总数) * 100
137
+
138
+
139
+ def 创建进度条(总数: int, 描述: str = "处理中", 禁用: bool = False) -> 回测进度条:
140
+ """
141
+ 快速创建进度条的便捷函数
142
+
143
+ 使用方法:
144
+ 进度 = 创建进度条(1000000, "回测ETH策略")
145
+ for i in range(1000000):
146
+ # 处理逻辑
147
+ 进度.更新(1)
148
+ 进度.关闭()
149
+ """
150
+ return 回测进度条(总数=总数, 描述=描述, 禁用=禁用)
151
+
152
+
153
+ # ============== 向量化回测专用 ==============
154
+
155
+ class 分块进度条:
156
+ """
157
+ 分块进度条 - 适用于向量化回测
158
+
159
+ 向量化回测不是一条一条处理,而是一大块一大块处理。
160
+ 这个进度条专门为这种情况设计。
161
+
162
+ 使用方法:
163
+ 进度 = 分块进度条(总步骤=5, 描述="向量化回测")
164
+
165
+ 进度.完成步骤("加载数��")
166
+ # ... 加载数据 ...
167
+
168
+ 进度.完成步骤("计算指标")
169
+ # ... 计算指标 ...
170
+
171
+ 进度.完成步骤("生成信号")
172
+ # ... 生成信号 ...
173
+
174
+ 进度.结束()
175
+ """
176
+
177
+ def __init__(self, 总步骤: int = 5, 描述: str = "回测中"):
178
+ self.总步骤 = 总步骤
179
+ self.当前步骤 = 0
180
+ self.描述 = 描述
181
+
182
+ self._进度条 = tqdm(
183
+ total=总步骤,
184
+ desc=描述,
185
+ unit="步",
186
+ bar_format='{desc}: {n}/{total} |{bar:20}| {postfix}',
187
+ leave=True
188
+ )
189
+
190
+ def 完成步骤(self, 步骤名称: str):
191
+ """标记一个步骤完成"""
192
+ self.当前步骤 += 1
193
+ self._进度条.set_postfix_str(f"✅ {步骤名称}")
194
+ self._进度条.update(1)
195
+
196
+ def 结束(self):
197
+ """结束进度条"""
198
+ self._进度条.set_postfix_str("🎉 完成!")
199
+ self._进度条.close()
200
+
201
+
202
+ # ============== 测试代码 ==============
203
+
204
+ if __name__ == "__main__":
205
+ import time
206
+
207
+ print("测试1: 基础进度条")
208
+ with 回测进度条(总数=100, 描述="处理K线") as 进度:
209
+ for i in range(100):
210
+ time.sleep(0.02) # 模拟处理
211
+ 进度.更新(1)
212
+ if i % 20 == 0:
213
+ 进度.设置后缀(收益="12.5%")
214
+
215
+ print("\n测试2: 分块进度条 (向量化)")
216
+ 进度 = 分块进度条(总步骤=4, 描述="向量化回测")
217
+
218
+ time.sleep(0.3)
219
+ 进度.完成步骤("加载数据")
220
+
221
+ time.sleep(0.3)
222
+ 进度.完成步骤("计算指标")
223
+
224
+ time.sleep(0.3)
225
+ 进度.完成步骤("生成信号")
226
+
227
+ time.sleep(0.3)
228
+ 进度.完成步骤("计算收益")
229
+
230
+ 进度.结束()
231
+
232
+ print("\n✅ 进度条模块测试通过!")
基础库/common_core/config/loader.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration Loading Utilities
3
+ """
4
+ import importlib.util
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Type, TypeVar, Optional, Any, Dict
8
+
9
+ from pydantic import BaseModel
10
+
11
+ T = TypeVar("T", bound=BaseModel)
12
+
13
+
14
+ def load_config_from_module(
15
+ module_path: str,
16
+ model: Type[T],
17
+ variable_name: str = "config"
18
+ ) -> T:
19
+ """
20
+ Load configuration from a python module file and validate it against a Pydantic model.
21
+
22
+ :param module_path: Path to the python file (e.g., 'config.py')
23
+ :param model: Pydantic model class to validate against
24
+ :param variable_name: The variable name in the module to load (default: 'config')
25
+ :return: Instance of the Pydantic model
26
+ """
27
+ path = Path(module_path).resolve()
28
+ if not path.exists():
29
+ raise FileNotFoundError(f"Config file not found: {path}")
30
+
31
+ spec = importlib.util.spec_from_file_location("dynamic_config", path)
32
+ if spec is None or spec.loader is None:
33
+ raise ImportError(f"Could not load spec from {path}")
34
+
35
+ module = importlib.util.module_from_spec(spec)
36
+ spec.loader.exec_module(module)
37
+
38
+ config_data = getattr(module, variable_name, None)
39
+ if config_data is None:
40
+ # Try to find a variable that matches the variable_name case-insensitive
41
+ # or if variable_name is a dict of exports expected
42
+ raise AttributeError(f"Variable '{variable_name}' not found in {module_path}")
43
+
44
+ return model.model_validate(config_data)
45
+
46
+
47
+ def load_dict_from_module(module_path: str, variable_names: list[str]) -> Dict[str, Any]:
48
+ """
49
+ Load specific variables from a python module file.
50
+
51
+ :param module_path: Path to the python file
52
+ :param variable_names: List of variable names to retrieve
53
+ :return: Dictionary of variable names to values
54
+ """
55
+ path = Path(module_path).resolve()
56
+ if not path.exists():
57
+ # Fallback: try relative to CWD
58
+ path = Path.cwd() / module_path
59
+ if not path.exists():
60
+ raise FileNotFoundError(f"Config file not found: {module_path}")
61
+
62
+ spec = importlib.util.spec_from_file_location("dynamic_config", path)
63
+ if spec is None or spec.loader is None:
64
+ raise ImportError(f"Could not load spec from {path}")
65
+
66
+ module = importlib.util.module_from_spec(spec)
67
+ spec.loader.exec_module(module)
68
+
69
+ result = {}
70
+ for name in variable_names:
71
+ if hasattr(module, name):
72
+ result[name] = getattr(module, name)
73
+
74
+ return result
基础库/common_core/config/models.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置模型定义
3
+ 用于定义和验证系统各部分的配置结构,利用 Pydantic 提供类型检查和自动验证功能。
4
+ """
5
+ from typing import Optional, List, Dict, Union, Set
6
+ from pydantic import BaseModel, Field, AnyHttpUrl
7
+
8
+
9
+ class ExchangeConfig(BaseModel):
10
+ """交易所基础配置"""
11
+ exchange_name: str = Field(default="binance", description="交易所名称")
12
+ apiKey: str = Field(default="", description="API 密钥")
13
+ secret: str = Field(default="", description="API 私钥")
14
+ is_pure_long: bool = Field(default=False, description="是否为纯多头模式")
15
+ password: Optional[str] = None
16
+ uid: Optional[str] = None
17
+
18
+
19
+ class FactorConfig(BaseModel):
20
+ """因子配置"""
21
+ name: str
22
+ param: Union[int, float, str, dict, list]
23
+
24
+
25
+ class StrategyConfig(BaseModel):
26
+ """策略配置"""
27
+ strategy_name: str
28
+ symbol_type: str = Field(default="swap", description="交易对类型:spot(现货) 或 swap(合约)")
29
+ hold_period: str = Field(default="1h", description="持仓周期")
30
+ # 在此添加其他通用策略字段
31
+ # 这是一个基础模型,具体策略可以继承扩展此模型
32
+
33
+
34
+ class AccountConfig(BaseModel):
35
+ """账户配置"""
36
+ name: str = Field(default="default_account", description="账户名称")
37
+ apiKey: Optional[str] = None
38
+ secret: Optional[str] = None
39
+ strategy: Dict = Field(default_factory=dict, description="策略配置字典")
40
+ strategy_short: Optional[Dict] = Field(default=None, description="做空策略配置字典")
41
+
42
+ # 风控设置
43
+ black_list: List[str] = Field(default_factory=list, description="黑名单币种")
44
+ white_list: List[str] = Field(default_factory=list, description="白名单币种")
45
+ leverage: int = Field(default=1, description="杠杆倍数")
46
+
47
+ # 数据获取设置
48
+ get_kline_num: int = Field(default=999, description="获取K线数量")
49
+ min_kline_num: int = Field(default=168, description="最小K线数量要求")
50
+
51
+ # 通知设置
52
+ wechat_webhook_url: Optional[str] = Field(default=None, description="企业微信 Webhook URL")
53
+
54
+ # 下单限制
55
+ order_spot_money_limit: float = Field(default=10.0, description="现货最小下单金额")
56
+ order_swap_money_limit: float = Field(default=5.0, description="合约最小下单金额")
57
+
58
+ # 高级设置
59
+ use_offset: bool = Field(default=False, description="是否使用 Offset")
60
+ is_pure_long: bool = Field(default=False, description="是否为纯多头模式")
基础库/common_core/exchange/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """
2
+ """
基础库/common_core/exchange/base_client.py ADDED
@@ -0,0 +1,723 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [交易所客户端基类]
4
+ 功能:定义通用的连接管理、签名鉴权、错误处理逻辑,规范化所有交易所接口,作为 StandardClient 和 AsyncClient 的父类。
5
+ """
6
+ # ==================================================================================================
7
+ # !!! 前置非常重要说明
8
+ # !!! 前置非常重要说明
9
+ # !!! 前置非常重要说明
10
+ # ---------------------------------------------------------------------------------------------------
11
+ # ** 方法名前缀规范 **
12
+ # 1. load_* 从硬盘获取数据
13
+ # 2. fetch_* 从接口获取数据
14
+ # 3. get_* 从对象获取数据,可能从硬盘,也可能从接口
15
+ # ====================================================================================================
16
+
17
+ import math
18
+ import time
19
+ import traceback
20
+
21
+ import ccxt
22
+ import numpy as np
23
+ import pandas as pd
24
+
25
+ from common_core.utils.commons import apply_precision
26
+ from common_core.utils.commons import retry_wrapper
27
+ from common_core.utils.dingding import send_wechat_work_msg, send_msg_for_order
28
+
29
+
30
+ # 现货接口
31
+ # sapi
32
+
33
+ # 合约接口
34
+ # dapi:普通账户,包含币本位交易
35
+ # fapi,普通账户,包含U本位交易
36
+
37
+ # 统一账户
38
+ # papi, um的接口:U本位合约
39
+ # papi, cm的接口:币本位合约
40
+ # papi, margin:现货API,全仓杠杆现货
41
+
42
+ class BinanceClient:
43
+ diff_timestamp = 0
44
+ constants = dict()
45
+
46
+ market_info = {} # 缓存市场信息,并且自动更新,全局共享
47
+
48
+ def __init__(self, **config):
49
+ self.api_key: str = config.get('apiKey', '')
50
+ self.secret: str = config.get('secret', '')
51
+
52
+ # 默认配置
53
+ default_exchange_config = {
54
+ 'timeout': 30000,
55
+ 'rateLimit': 30,
56
+ 'enableRateLimit': False,
57
+ 'options': {'adjustForTimeDifference': True, 'recvWindow': 10000},
58
+ }
59
+
60
+ self.order_money_limit: dict = {
61
+ 'spot': config.get('spot_order_money_limit', 10),
62
+ 'swap': config.get('swap_order_money_limit', 5),
63
+ }
64
+
65
+ self.exchange = ccxt.binance(config.get('exchange_config', default_exchange_config))
66
+ self.wechat_webhook_url: str = config.get('wechat_webhook_url', '')
67
+
68
+ # 常用配置
69
+ self.utc_offset = config.get('utc_offset', 8)
70
+ self.stable_symbol = config.get('stable_symbol', ['USDC', 'USDP', 'TUSD', 'BUSD', 'FDUSD', 'DAI'])
71
+
72
+ self.swap_account = None
73
+
74
+ self.coin_margin: dict = config.get('coin_margin', {}) # 用做保证金的币种
75
+
76
+ # ====================================================================================================
77
+ # ** 市场信息 **
78
+ # ====================================================================================================
79
+ def _fetch_swap_exchange_info_list(self) -> list:
80
+ exchange_info = retry_wrapper(self.exchange.fapipublic_get_exchangeinfo, func_name='获取BN合约币种规则数据')
81
+ return exchange_info['symbols']
82
+
83
+ def _fetch_spot_exchange_info_list(self) -> list:
84
+ exchange_info = retry_wrapper(self.exchange.public_get_exchangeinfo, func_name='获取BN现货币种规则数据')
85
+ return exchange_info['symbols']
86
+
87
+ # region 市场信息数据获取
88
+ def fetch_market_info(self, symbol_type='swap', quote_symbol='USDT'):
89
+ """
90
+ 加载市场数据
91
+ :param symbol_type: 币种信息。swap为合约,spot为现货
92
+ :param quote_symbol: 报价币种
93
+ :return:
94
+ symbol_list 交易对列表
95
+ price_precision 币种价格精 例: 2 代表 0.01
96
+ {'BTCUSD_PERP': 1, 'BTCUSD_231229': 1, 'BTCUSD_240329': 1, 'BTCUSD_240628': 1, ...}
97
+ min_notional 最小下单金额 例: 5.0 代表 最小下单金额是5U
98
+ {'BTCUSDT': 5.0, 'ETHUSDT': 5.0, 'BCHUSDT': 5.0, 'XRPUSDT': 5.0...}
99
+ """
100
+ print(f'🔄更新{symbol_type}市场数据...')
101
+ # ===获取所有币种信息
102
+ if symbol_type == 'swap': # 合约
103
+ exchange_info_list = self._fetch_swap_exchange_info_list()
104
+ else: # 现货
105
+ exchange_info_list = self._fetch_spot_exchange_info_list()
106
+
107
+ # ===获取币种列表
108
+ symbol_list = [] # 如果是合约,只包含永续合约。如果是现货,包含所有数据
109
+ full_symbol_list = [] # 包含所有币种信息
110
+
111
+ # ===获取各个交易对的精度、下单量等信息
112
+ min_qty = {} # 最小下单精度,例如bnb,一次最少买入0.001个
113
+ price_precision = {} # 币种价格精,例如bnb,价格是158.887,不能是158.8869
114
+ min_notional = {} # 最小下单金额,例如bnb,一次下单至少买入金额是5usdt
115
+ # 遍历获得想要的数据
116
+ for info in exchange_info_list:
117
+ symbol = info['symbol'] # 交易对信息
118
+
119
+ # 过滤掉非报价币对 , 非交易币对
120
+ if info['quoteAsset'] != quote_symbol or info['status'] != 'TRADING':
121
+ continue
122
+
123
+ full_symbol_list.append(symbol) # 添加到全量信息中
124
+
125
+ if (symbol_type == 'swap' and info['contractType'] != 'PERPETUAL') or info['baseAsset'] in self.stable_symbol:
126
+ pass # 获取合约的时候,非永续的symbol会被排除
127
+ else:
128
+ symbol_list.append(symbol)
129
+
130
+ for _filter in info['filters']: # 遍历获得想要的数据
131
+ if _filter['filterType'] == 'PRICE_FILTER': # 获取价格精度
132
+ price_precision[symbol] = int(math.log(float(_filter['tickSize']), 0.1))
133
+ elif _filter['filterType'] == 'LOT_SIZE': # 获取最小下单量
134
+ min_qty[symbol] = int(math.log(float(_filter['minQty']), 0.1))
135
+ elif _filter['filterType'] == 'MIN_NOTIONAL' and symbol_type == 'swap': # 合约的最小下单金额
136
+ min_notional[symbol] = float(_filter['notional'])
137
+ elif _filter['filterType'] == 'NOTIONAL' and symbol_type == 'spot': # 现货的最小下单金额
138
+ min_notional[symbol] = float(_filter['minNotional'])
139
+
140
+ self.market_info[symbol_type] = {
141
+ 'symbol_list': symbol_list, # 如果是合约,只包含永续合约。如果是现货,包含所有数据
142
+ 'full_symbol_list': full_symbol_list, # 包含所有币种信息
143
+ 'min_qty': min_qty,
144
+ 'price_precision': price_precision,
145
+ 'min_notional': min_notional,
146
+ 'last_update': int(time.time())
147
+ }
148
+ return self.market_info[symbol_type]
149
+
150
+ def get_market_info(self, symbol_type, expire_seconds: int = 3600 * 12, require_update: bool = False,
151
+ quote_symbol='USDT') -> dict:
152
+ if require_update: # 如果强制刷新的话,就当我们系统没有更新过
153
+ last_update = 0
154
+ else:
155
+ last_update = self.market_info.get(symbol_type, {}).get('last_update', 0)
156
+ if last_update + expire_seconds < int(time.time()):
157
+ self.fetch_market_info(symbol_type, quote_symbol)
158
+
159
+ return self.market_info[symbol_type]
160
+
161
+ # endregion
162
+
163
+ # ====================================================================================================
164
+ # ** 行情数据获取 **
165
+ # ====================================================================================================
166
+ # region 行情数据获取
167
+ """K线数据获取"""
168
+
169
+ def get_candle_df(self, symbol, run_time, limit=1500, interval='1h', symbol_type='swap') -> pd.DataFrame:
170
+ # ===获取K线数据
171
+ _limit = limit
172
+ # 定义请求的参数:现货最大1000,合约最大499。
173
+ if limit > 1000: # 如果参数大于1000
174
+ if symbol_type == 'spot': # 如果是现货,最大设置1000
175
+ _limit = 1000
176
+ else: # 如果不是现货,那就设置499
177
+ _limit = 499
178
+ # limit = 1000 if limit > 1000 and symbol_type == 'spot' else limit # 现货最多获取1000根K
179
+ # 计算获取k线的开始时间
180
+ start_time_dt = run_time - pd.to_timedelta(interval) * limit
181
+
182
+ df_list = [] # 定义获取的k线数据
183
+ data_len = 0 # 记录数据长度
184
+ params = {
185
+ 'symbol': symbol, # 获取币种
186
+ 'interval': interval, # 获取k线周期
187
+ 'limit': _limit, # 获取多少根
188
+ 'startTime': int(time.mktime(start_time_dt.timetuple())) * 1000 # 获取币种开始时间
189
+ }
190
+ while True:
191
+ # 获取指定币种的k线数据
192
+ try:
193
+ if symbol_type == 'swap':
194
+ kline = retry_wrapper(
195
+ self.exchange.fapipublic_get_klines, params=params, func_name='获取币种K线',
196
+ if_exit=False
197
+ )
198
+ else:
199
+ kline = retry_wrapper(
200
+ self.exchange.public_get_klines, params=params, func_name='获取币种K线',
201
+ if_exit=False
202
+ )
203
+ except Exception as e:
204
+ print(e)
205
+ print(traceback.format_exc())
206
+ # 如果获取k线重试出错,直接返回,当前币种不参与交易
207
+ return pd.DataFrame()
208
+
209
+ # ===整理数据
210
+ # 将数据转换为DataFrame
211
+ df = pd.DataFrame(kline, dtype='float')
212
+ if df.empty:
213
+ break
214
+ # 对字段进行重命名,字段对应数据可以查询文档(https://binance-docs.github.io/apidocs/futures/cn/#k)
215
+ columns = {0: 'candle_begin_time', 1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume', 6: 'close_time',
216
+ 7: 'quote_volume',
217
+ 8: 'trade_num', 9: 'taker_buy_base_asset_volume', 10: 'taker_buy_quote_asset_volume',
218
+ 11: 'ignore'}
219
+ df.rename(columns=columns, inplace=True)
220
+ df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'], unit='ms')
221
+ df.sort_values(by=['candle_begin_time'], inplace=True) # 排序
222
+
223
+ # 数据追加
224
+ df_list.append(df)
225
+ data_len = data_len + df.shape[0] - 1
226
+
227
+ # 判断请求的数据是否足够
228
+ if data_len >= limit:
229
+ break
230
+
231
+ if params['startTime'] == int(df.iloc[-1]['candle_begin_time'].timestamp()) * 1000:
232
+ break
233
+
234
+ # 更新一下k线数据
235
+ params['startTime'] = int(df.iloc[-1]['candle_begin_time'].timestamp()) * 1000
236
+ # 下载太多的k线的时候,中间sleep一下
237
+ time.sleep(0.1)
238
+
239
+ if not df_list:
240
+ return pd.DataFrame()
241
+
242
+ all_df = pd.concat(df_list, ignore_index=True)
243
+ all_df['symbol'] = symbol # 添加symbol列
244
+ all_df['symbol_type'] = symbol_type # 添加类型字段
245
+ all_df.sort_values(by=['candle_begin_time'], inplace=True) # 排序
246
+ all_df.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True) # 去重
247
+
248
+ # 删除runtime那根未走完的k线数据(交易所有时候会返回这条数据)
249
+ all_df = all_df[all_df['candle_begin_time'] + pd.Timedelta(hours=self.utc_offset) < run_time]
250
+ all_df.reset_index(drop=True, inplace=True)
251
+
252
+ return all_df
253
+
254
+ """最新报价数据获取"""
255
+
256
+ def fetch_ticker_price(self, symbol: str = None, symbol_type: str = 'swap') -> dict:
257
+ params = {'symbol': symbol} if symbol else {}
258
+ match symbol_type:
259
+ case 'spot':
260
+ api_func = self.exchange.public_get_ticker_price
261
+ func_name = f'获取{symbol}现货的ticker数据' if symbol else '获取所有现货币种的ticker数据'
262
+ case 'swap':
263
+ api_func = self.exchange.fapipublic_get_ticker_price
264
+ func_name = f'获取{symbol}合约的ticker数据' if symbol else '获取所有合约币种的ticker数据'
265
+ case _:
266
+ raise ValueError(f'未知的symbol_type:{symbol_type}')
267
+
268
+ tickers = retry_wrapper(api_func, params=params, func_name=func_name)
269
+ return tickers
270
+
271
+ def fetch_spot_ticker_price(self, spot_symbol: str = None) -> dict:
272
+ return self.fetch_ticker_price(spot_symbol, symbol_type='spot')
273
+
274
+ def fetch_swap_ticker_price(self, swap_symbol: str = None) -> dict:
275
+ return self.fetch_ticker_price(swap_symbol, symbol_type='swap')
276
+
277
+ def get_spot_ticker_price_series(self) -> pd.Series:
278
+ ticker_price_df = pd.DataFrame(self.fetch_ticker_price(symbol_type='spot'))
279
+ ticker_price_df['price'] = pd.to_numeric(ticker_price_df['price'], errors='coerce')
280
+ return ticker_price_df.set_index(['symbol'])['price']
281
+
282
+ def get_swap_ticker_price_series(self) -> pd.Series:
283
+ ticker_price_df = pd.DataFrame(self.fetch_ticker_price(symbol_type='swap'))
284
+ ticker_price_df['price'] = pd.to_numeric(ticker_price_df['price'], errors='coerce')
285
+ return ticker_price_df.set_index(['symbol'])['price']
286
+
287
+ """盘口数据获取"""
288
+
289
+ def fetch_book_ticker(self, symbol, symbol_type='swap') -> dict:
290
+ if symbol_type == 'swap':
291
+ # 获取合约的盘口数据
292
+ swap_book_ticker_data = retry_wrapper(
293
+ self.exchange.fapiPublicGetTickerBookTicker, params={'symbol': symbol}, func_name='获取合约盘口数据')
294
+ return swap_book_ticker_data
295
+ else:
296
+ # 获取现货的盘口数据
297
+ spot_book_ticker_data = retry_wrapper(
298
+ self.exchange.publicGetTickerBookTicker, params={'symbol': symbol}, func_name='获取现货盘口数据'
299
+ )
300
+ return spot_book_ticker_data
301
+
302
+ def fetch_spot_book_ticker(self, spot_symbol) -> dict:
303
+ return self.fetch_book_ticker(spot_symbol, symbol_type='spot')
304
+
305
+ def fetch_swap_book_ticker(self, swap_symbol) -> dict:
306
+ return self.fetch_book_ticker(swap_symbol, symbol_type='swap')
307
+
308
+ # endregion
309
+
310
+ # ====================================================================================================
311
+ # ** 资金费数据 **
312
+ # ====================================================================================================
313
+ def get_premium_index_df(self) -> pd.DataFrame:
314
+ """
315
+ 获取币安的最新资金费数据
316
+ """
317
+ last_funding_df = retry_wrapper(self.exchange.fapipublic_get_premiumindex, func_name='获取最新的资金费数据')
318
+ last_funding_df = pd.DataFrame(last_funding_df)
319
+
320
+ last_funding_df['nextFundingTime'] = pd.to_numeric(last_funding_df['nextFundingTime'], errors='coerce')
321
+ last_funding_df['time'] = pd.to_numeric(last_funding_df['time'], errors='coerce')
322
+
323
+ last_funding_df['nextFundingTime'] = pd.to_datetime(last_funding_df['nextFundingTime'], unit='ms')
324
+ last_funding_df['time'] = pd.to_datetime(last_funding_df['time'], unit='ms')
325
+ last_funding_df = last_funding_df[['symbol', 'nextFundingTime', 'lastFundingRate']] # 保留部分字段
326
+ last_funding_df.rename(columns={'nextFundingTime': 'fundingTime', 'lastFundingRate': 'fundingRate'},
327
+ inplace=True)
328
+
329
+ return last_funding_df
330
+
331
+ def get_funding_rate_df(self, symbol, limit=1000) -> pd.DataFrame:
332
+ """
333
+ 获取币安的历史资金费数据
334
+ :param symbol: 币种名称
335
+ :param limit: 请求获取多少条数据,最大1000
336
+ """
337
+ param = {'symbol': symbol, 'limit': limit}
338
+ # 获取历史数据
339
+ try:
340
+ funding_df = retry_wrapper(
341
+ self.exchange.fapipublic_get_fundingrate, params=param,
342
+ func_name='获取合约历史资金费数据'
343
+ )
344
+ except Exception as e:
345
+ print(e)
346
+ return pd.DataFrame()
347
+ funding_df = pd.DataFrame(funding_df)
348
+ if funding_df.empty:
349
+ return funding_df
350
+
351
+ funding_df['fundingTime'] = pd.to_datetime(funding_df['fundingTime'].astype(float) // 1000 * 1000,
352
+ unit='ms') # 时间戳内容含有一些纳秒数据需要处理
353
+ funding_df.sort_values('fundingTime', inplace=True)
354
+
355
+ return funding_df
356
+
357
+ # ====================================================================================================
358
+ # ** 账户设置 **
359
+ # ====================================================================================================
360
+ def fetch_transfer_history(self):
361
+ raise NotImplementedError
362
+
363
+ def set_single_side_position(self):
364
+ raise NotImplementedError
365
+
366
+ def set_multi_assets_margin(self):
367
+ """
368
+ 检查是否开启了联合保证金模式
369
+ """
370
+ # 查询保证金模式
371
+ pass
372
+
373
+ def reset_max_leverage(self, max_leverage=5, coin_list=()):
374
+ """
375
+ 重置一下页面最大杠杆
376
+ :param max_leverage: 设置页面最大杠杆
377
+ :param coin_list: 对指定币种进行调整页面杠杆
378
+ """
379
+ """
380
+ 重置一下页面最大杠杆
381
+ :param exchange: 交易所对象,用于获取数据
382
+ :param max_leverage: 设置页面最大杠杆
383
+ :param coin_list: 对指定币种进行调整页面杠杆
384
+ """
385
+ # 获取账户持仓风险(这里有杠杆数据)
386
+ account_info = self.get_swap_account()
387
+ if account_info is None:
388
+ print(f'ℹ️获取账户持仓风险数据为空')
389
+ exit(1)
390
+
391
+ position_risk = pd.DataFrame(account_info['positions']) # 将数据转成DataFrame
392
+ if len(coin_list) > 0:
393
+ position_risk = position_risk[position_risk['symbol'].isin(coin_list)] # 只对选币池中的币种进行调整页面杠杆
394
+ position_risk.set_index('symbol', inplace=True) # 将symbol设为index
395
+
396
+ # 遍历每一个可以持仓的币种,修改页面最大杠杆
397
+ for symbol, row in position_risk.iterrows():
398
+ if int(row['leverage']) != max_leverage:
399
+ reset_leverage_func = getattr(self.exchange, self.constants.get('reset_page_leverage_api'))
400
+ # 设置杠杆
401
+ retry_wrapper(
402
+ reset_leverage_func,
403
+ params={'symbol': symbol, 'leverage': max_leverage, 'timestamp': ''},
404
+ func_name='设置杠杆'
405
+ )
406
+
407
+ # ====================================================================================================
408
+ # ** 交易函数 **
409
+ # ====================================================================================================
410
+ def cancel_all_spot_orders(self):
411
+ # 现货撤单
412
+ get_spot_open_orders_func = getattr(self.exchange, self.constants.get('get_spot_open_orders_api'))
413
+ orders = retry_wrapper(
414
+ get_spot_open_orders_func,
415
+ params={'timestamp': ''}, func_name='查询现货所有挂单'
416
+ )
417
+ symbols = [_['symbol'] for _ in orders]
418
+ symbols = list(set(symbols))
419
+ cancel_spot_open_orders_func = getattr(self.exchange, self.constants.get('cancel_spot_open_orders_api'))
420
+ for _ in symbols:
421
+ retry_wrapper(
422
+ cancel_spot_open_orders_func,
423
+ params={'symbol': _, 'timestamp': ''}, func_name='取消现货挂单'
424
+ )
425
+
426
+ def cancel_all_swap_orders(self):
427
+ # 合约撤单
428
+ get_swap_open_orders_func = getattr(self.exchange, self.constants.get('get_swap_open_orders_api'))
429
+ orders = retry_wrapper(
430
+ get_swap_open_orders_func,
431
+ params={'timestamp': ''}, func_name='查询U本位合约所有挂单'
432
+ )
433
+ symbols = [_['symbol'] for _ in orders]
434
+ symbols = list(set(symbols))
435
+ cancel_swap_open_orders_func = getattr(self.exchange, self.constants.get('cancel_swap_open_orders_api'))
436
+ for _ in symbols:
437
+ retry_wrapper(
438
+ cancel_swap_open_orders_func,
439
+ params={'symbol': _, 'timestamp': ''}, func_name='取消U本位合约挂单'
440
+ )
441
+
442
+ def prepare_order_params_list(
443
+ self, orders_df: pd.DataFrame, symbol_type: str, symbol_ticker_price: pd.Series,
444
+ slip_rate: float = 0.015) -> list:
445
+ """
446
+ 根据策略产生的订单数据,构建每个币种的下单参数
447
+ :param orders_df: 策略产生的订单信息
448
+ :param symbol_type: 下单类型。spot/swap
449
+ :param symbol_ticker_price: 每个币种最新价格
450
+ :param slip_rate: 滑点
451
+ :return: order_params_list 每个币种的下单参数
452
+ """
453
+ orders_df.sort_values('实际下单资金', ascending=True, inplace=True)
454
+ orders_df.set_index('symbol', inplace=True) # 重新设置index
455
+
456
+ market_info = self.get_market_info(symbol_type)
457
+ min_qty = market_info['min_qty']
458
+ price_precision = market_info['price_precision']
459
+ min_notional = market_info['min_notional']
460
+
461
+ # 遍历symbol_order,构建每个币种的下单参数
462
+ order_params_list = []
463
+ for symbol, row in orders_df.iterrows():
464
+ # ===若当前币种没有最小下单精度、或最小价格精度,报错
465
+ if (symbol not in min_qty) or (symbol not in price_precision):
466
+ # 报错
467
+ print(f'❌当前币种{symbol}没有最小下单精度、或最小价格精度,币种信息异常')
468
+ continue
469
+
470
+ # ===计算下单量、方向、价格
471
+ quantity = row['实际下单量']
472
+ # 按照最小下单量对合约进行四舍五入,对现货就低不就高处理
473
+ # 注意点:合约有reduceOnly参数可以超过你持有的持仓量,现货不行,只能卖的时候留一点点残渣
474
+ quantity = round(quantity, min_qty[symbol]) if symbol_type == 'swap' else apply_precision(quantity,
475
+ min_qty[symbol])
476
+ # 计算下单方向、价格,并增加一定的滑点
477
+ if quantity > 0:
478
+ side = 'BUY'
479
+ price = symbol_ticker_price[symbol] * (1 + slip_rate)
480
+ elif quantity < 0:
481
+ side = 'SELL'
482
+ price = symbol_ticker_price[symbol] * (1 - slip_rate)
483
+ else:
484
+ print('⚠️下单量为0,不进行下单')
485
+ continue
486
+ # 下单量取绝对值
487
+ quantity = abs(quantity)
488
+ # 通过最小价格精度对下单价格进行四舍五入
489
+ price = round(price, price_precision[symbol])
490
+
491
+ # ===判断是否是清仓交易
492
+ reduce_only = True if row['交易模式'] == '清仓' and symbol_type == 'swap' else False
493
+
494
+ # ===判断交易金额是否小于最小下单金额(一般是5元),小于的跳过
495
+ if quantity * price < min_notional.get(symbol, self.order_money_limit[symbol_type]):
496
+ if not reduce_only: # 清仓状态不跳过
497
+ print(f'⚠️{symbol}交易金额是小于最小下单金额(一般合约是5元,现货是10元),跳过该笔交易')
498
+ print(f'ℹ️下单量:{quantity},价格:{price}')
499
+ continue
500
+
501
+ # ===构建下单参数
502
+ price = f'{price:.{price_precision[symbol]}f}' # 根据精度将价格转成str
503
+ quantity = np.format_float_positional(quantity).rstrip('.') # 解决科学计数法的问题
504
+ order_params = {
505
+ 'symbol': symbol,
506
+ 'side': side,
507
+ 'type': 'LIMIT',
508
+ 'price': price,
509
+ 'quantity': quantity,
510
+ 'newClientOrderId': str(int(time.time())),
511
+ 'timeInForce': 'GTC',
512
+ 'reduceOnly': str(bool(reduce_only)),
513
+ 'timestamp': ''
514
+ }
515
+ # 如果是合约下单,添加进行下单列表中,放便后续批量下单
516
+ order_params_list.append(order_params)
517
+ return order_params_list
518
+
519
+ def place_spot_orders_bulk(self, orders_df, slip_rate=0.015):
520
+ symbol_last_price = self.get_spot_ticker_price_series()
521
+ order_params_list = self.prepare_order_params_list(orders_df, 'spot', symbol_last_price, slip_rate)
522
+
523
+ for order_param in order_params_list:
524
+ del order_param['reduceOnly'] # 现货没有这个参数,进行移除
525
+ self.place_spot_order(**order_param)
526
+
527
+ def place_swap_orders_bulk(self, orders_df, slip_rate=0.015):
528
+ symbol_last_price = self.get_swap_ticker_price_series()
529
+ order_params_list = self.prepare_order_params_list(orders_df, 'swap', symbol_last_price, slip_rate)
530
+
531
+ for order_params in order_params_list:
532
+ self.place_swap_order(**order_params)
533
+
534
+ def place_spot_order(self, symbol, side, quantity, price=None, **kwargs) -> dict:
535
+ print(f'`{symbol}`现货下单 {side} {quantity}', '.')
536
+
537
+ # 确定下单参数
538
+ params = {
539
+ 'symbol': symbol,
540
+ 'side': side,
541
+ 'type': 'MARKET',
542
+ 'quantity': str(quantity),
543
+ **kwargs
544
+ }
545
+
546
+ if price is not None:
547
+ params['price'] = str(price)
548
+ params['timeInForce'] = 'GTC'
549
+ params['type'] = 'LIMIT'
550
+
551
+ try:
552
+ print(f'ℹ️现货下单参数:{params}')
553
+ # 下单
554
+ order_res = retry_wrapper(
555
+ self.exchange.private_post_order,
556
+ params=params,
557
+ func_name='现货下单'
558
+ )
559
+ print(f'✅现货下单完成,现货下单信息结果:{order_res}')
560
+ except Exception as e:
561
+ print(f'❌现货下单出错:{e}')
562
+ send_wechat_work_msg(
563
+ f'现货 {symbol} 下单 {float(quantity) * float(price)}U 出错,请查看程序日志',
564
+ self.wechat_webhook_url
565
+ )
566
+ return {}
567
+ # 发送下单结果到钉钉
568
+ send_msg_for_order([params], [order_res], self.wechat_webhook_url)
569
+ return order_res
570
+
571
+ def place_swap_order(self, symbol, side, quantity, price=None, **kwargs) -> dict:
572
+ print(f'`{symbol}`U本位合约下单 {side} {quantity}', '.')
573
+
574
+ # 确定下单参数
575
+ params = {
576
+ 'symbol': symbol,
577
+ 'side': side,
578
+ 'type': 'MARKET',
579
+ 'quantity': str(quantity),
580
+ **kwargs
581
+ }
582
+
583
+ if price is not None:
584
+ params['price'] = str(price)
585
+ params['timeInForce'] = 'GTC'
586
+ params['type'] = 'LIMIT'
587
+
588
+ try:
589
+ print(f'ℹ️U本位合约下单参数:{params}')
590
+ # 下单
591
+ order_res = retry_wrapper(
592
+ self.exchange.fapiprivate_post_order,
593
+ params=params,
594
+ func_name='U本位合约下单'
595
+ )
596
+ print(f'✅U本位合约下单完成,U本位合约下单信息结果:{order_res}')
597
+ except Exception as e:
598
+ print(f'❌U本位合约下单出错:{e}')
599
+ send_wechat_work_msg(
600
+ f'U本位合约 {symbol} 下单 {float(quantity) * float(price)}U 出错,请查看程序日志',
601
+ self.wechat_webhook_url
602
+ )
603
+ return {}
604
+ send_msg_for_order([params], [order_res], self.wechat_webhook_url)
605
+ return order_res
606
+
607
+ def get_spot_position_df(self) -> pd.DataFrame:
608
+ """
609
+ 获取账户净值
610
+
611
+
612
+ :return:
613
+ swap_equity=1000 (表示账户里资金总价值为 1000U )
614
+
615
+ """
616
+ # 获取U本位合约账户净值(不包含未实现盈亏)
617
+ position_df = retry_wrapper(self.exchange.private_get_account, params={'timestamp': ''},
618
+ func_name='获取现货账户净值') # 获取账户净值
619
+ position_df = pd.DataFrame(position_df['balances'])
620
+
621
+ position_df['free'] = pd.to_numeric(position_df['free'])
622
+ position_df['locked'] = pd.to_numeric(position_df['locked'])
623
+
624
+ position_df['free'] += position_df['locked']
625
+ position_df = position_df[position_df['free'] != 0]
626
+
627
+ position_df.rename(columns={'asset': 'symbol', 'free': '当前持仓量'}, inplace=True)
628
+
629
+ # 保留指定字段
630
+ position_df = position_df[['symbol', '当前持仓量']]
631
+ position_df['仓位价值'] = None # 设置默认值
632
+
633
+ return position_df
634
+
635
+ # =====获取持仓
636
+ # 获取币安账户的实际持仓
637
+ def get_swap_position_df(self) -> pd.DataFrame:
638
+ """
639
+ 获取币安账户的实际持仓
640
+
641
+ :return:
642
+
643
+ 当前持仓量 均价 持仓盈亏
644
+ symbol
645
+ RUNEUSDT -82.0 1.208 -0.328000
646
+ FTMUSDT 523.0 0.189 1.208156
647
+
648
+ """
649
+ # 获取原始数据
650
+ get_swap_position_func = getattr(self.exchange, self.constants.get('get_swap_position_api'))
651
+ position_df = retry_wrapper(get_swap_position_func, params={'timestamp': ''}, func_name='获取账户持仓风险')
652
+ if position_df is None or len(position_df) == 0:
653
+ return pd.DataFrame(columns=['symbol', '当前持仓量', '均价', '持仓盈亏', '当前标记价格', '仓位价值'])
654
+
655
+ position_df = pd.DataFrame(position_df) # 将原始数据转化为dataframe
656
+
657
+ # 整理数据
658
+ columns = {'positionAmt': '当前持仓量', 'entryPrice': '均价', 'unRealizedProfit': '持仓盈亏',
659
+ 'markPrice': '当前标记价格'}
660
+ position_df.rename(columns=columns, inplace=True)
661
+ for col in columns.values(): # 转成数字
662
+ position_df[col] = pd.to_numeric(position_df[col])
663
+
664
+ position_df = position_df[position_df['当前持仓量'] != 0] # 只保留有仓位的币种
665
+ position_df.set_index('symbol', inplace=True) # 将symbol设置为index
666
+ position_df['仓位价值'] = position_df['当前持仓量'] * position_df['当前标记价格']
667
+
668
+ # 保留指定字段
669
+ position_df = position_df[['当前持仓量', '均价', '持仓盈亏', '当前标记价格', '仓位价值']]
670
+
671
+ return position_df
672
+
673
+ def update_swap_account(self) -> dict:
674
+ self.swap_account = retry_wrapper(
675
+ self.exchange.fapiprivatev2_get_account, params={'timestamp': ''},
676
+ func_name='获取U本位合约账户信息'
677
+ )
678
+ return self.swap_account
679
+
680
+ def get_swap_account(self, require_update: bool = False) -> dict:
681
+ if self.swap_account is None or require_update:
682
+ self.update_swap_account()
683
+ return self.swap_account
684
+
685
+ def get_account_overview(self):
686
+ raise NotImplementedError
687
+
688
+ def fetch_spot_trades(self, symbol, end_time) -> pd.DataFrame:
689
+ # =设置获取订单时的参数
690
+ params = {
691
+ 'symbol': symbol, # 设置获取订单的币种
692
+ 'endTime': int(time.mktime(end_time.timetuple())) * 1000, # 设置获取订单的截止时间
693
+ 'limit': 1000, # 最大获取1000条订单信息
694
+ 'timestamp': ''
695
+ }
696
+
697
+ # =调用API获取订单信息
698
+ get_spot_my_trades_func = getattr(self.exchange, self.constants.get('get_spot_my_trades_api'))
699
+ trades = retry_wrapper(get_spot_my_trades_func, params=params, func_name='获取币种历史订单信息',
700
+ if_exit=False) # 获取账户净值
701
+ # 如果获取订单数据失败,进行容错处理,返回空df
702
+ if trades is None:
703
+ return pd.DataFrame()
704
+
705
+ trades = pd.DataFrame(trades) # 转成df格式
706
+ # =如果获取到的该币种的订单数据是空的,则跳过,继续获取另外一个币种
707
+ if trades.empty:
708
+ return pd.DataFrame()
709
+
710
+ # 转换数据格式
711
+ for col in ('isBuyer', 'price', 'qty', 'quoteQty', 'commission'):
712
+ trades[col] = pd.to_numeric(trades[col], errors='coerce')
713
+
714
+ # =如果isBuyer为1则为买入,否则为卖出
715
+ trades['方向'] = np.where(trades['isBuyer'] == 1, 1, -1)
716
+ # =整理下有用的数据
717
+ trades = trades[['time', 'symbol', 'price', 'qty', 'quoteQty', 'commission', 'commissionAsset', '方向']]
718
+
719
+ return trades
720
+
721
+ @classmethod
722
+ def get_dummy_client(cls) -> 'BinanceClient':
723
+ return cls()
基础库/common_core/exchange/binance_async.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [币安异步交易客户端]
4
+ 功能:基于 AsyncIO 实现高并发行情获取与交易指令发送,大幅提升数据下载和实盘响应速度。
5
+ """
6
+ import asyncio
7
+ import math
8
+ import time
9
+ import traceback
10
+ import ccxt.async_support as ccxt # 异步 CCXT
11
+ import pandas as pd
12
+ import numpy as np
13
+
14
+ from common_core.exchange.base_client import BinanceClient
15
+ from common_core.utils.async_commons import async_retry_wrapper
16
+
17
+ class AsyncBinanceClient(BinanceClient):
18
+ def __init__(self, **config):
19
+ super().__init__(**config)
20
+ # 使用异步版本覆盖 self.exchange
21
+ default_exchange_config = {
22
+ 'timeout': 30000,
23
+ 'rateLimit': 30,
24
+ 'enableRateLimit': False,
25
+ 'options': {'adjustForTimeDifference': True, 'recvWindow': 10000},
26
+ }
27
+ self.exchange = ccxt.binance(config.get('exchange_config', default_exchange_config))
28
+
29
+ async def close(self):
30
+ await self.exchange.close()
31
+
32
+ async def _fetch_swap_exchange_info_list(self) -> list:
33
+ exchange_info = await async_retry_wrapper(self.exchange.fapipublic_get_exchangeinfo, func_name='获取BN合约币种规则数据')
34
+ return exchange_info['symbols']
35
+
36
+ async def _fetch_spot_exchange_info_list(self) -> list:
37
+ exchange_info = await async_retry_wrapper(self.exchange.public_get_exchangeinfo, func_name='获取BN现货币种规则数据')
38
+ return exchange_info['symbols']
39
+
40
+ async def fetch_market_info(self, symbol_type='swap', quote_symbol='USDT'):
41
+ print(f'🔄(Async) 更新{symbol_type}市场数据...')
42
+ if symbol_type == 'swap':
43
+ exchange_info_list = await self._fetch_swap_exchange_info_list()
44
+ else:
45
+ exchange_info_list = await self._fetch_spot_exchange_info_list()
46
+
47
+ # 复用基类逻辑?
48
+ # 逻辑是处理列表。我们可以复制或提取它。
49
+ # 为了速度,我将在这里复制处理逻辑。
50
+
51
+ symbol_list = []
52
+ full_symbol_list = []
53
+ min_qty = {}
54
+ price_precision = {}
55
+ min_notional = {}
56
+
57
+ for info in exchange_info_list:
58
+ symbol = info['symbol']
59
+ if info['quoteAsset'] != quote_symbol or info['status'] != 'TRADING':
60
+ continue
61
+ full_symbol_list.append(symbol)
62
+
63
+ if (symbol_type == 'swap' and info['contractType'] != 'PERPETUAL') or info['baseAsset'] in self.stable_symbol:
64
+ pass
65
+ else:
66
+ symbol_list.append(symbol)
67
+
68
+ for _filter in info['filters']:
69
+ if _filter['filterType'] == 'PRICE_FILTER':
70
+ price_precision[symbol] = int(math.log(float(_filter['tickSize']), 0.1))
71
+ elif _filter['filterType'] == 'LOT_SIZE':
72
+ min_qty[symbol] = int(math.log(float(_filter['minQty']), 0.1))
73
+ elif _filter['filterType'] == 'MIN_NOTIONAL' and symbol_type == 'swap':
74
+ min_notional[symbol] = float(_filter['notional'])
75
+ elif _filter['filterType'] == 'NOTIONAL' and symbol_type == 'spot':
76
+ min_notional[symbol] = float(_filter['minNotional'])
77
+
78
+ self.market_info[symbol_type] = {
79
+ 'symbol_list': symbol_list,
80
+ 'full_symbol_list': full_symbol_list,
81
+ 'min_qty': min_qty,
82
+ 'price_precision': price_precision,
83
+ 'min_notional': min_notional,
84
+ 'last_update': int(time.time())
85
+ }
86
+ return self.market_info[symbol_type]
87
+
88
+ async def get_market_info(self, symbol_type, expire_seconds: int = 3600 * 12, require_update: bool = False, quote_symbol='USDT'):
89
+ if require_update:
90
+ last_update = 0
91
+ else:
92
+ last_update = self.market_info.get(symbol_type, {}).get('last_update', 0)
93
+
94
+ if last_update + expire_seconds < int(time.time()):
95
+ await self.fetch_market_info(symbol_type, quote_symbol)
96
+
97
+ return self.market_info[symbol_type]
98
+
99
+ async def get_candle_df(self, symbol, run_time, limit=1500, interval='1h', symbol_type='swap') -> pd.DataFrame:
100
+ _limit = limit
101
+ if limit > 1000:
102
+ if symbol_type == 'spot':
103
+ _limit = 1000
104
+ else:
105
+ _limit = 499
106
+
107
+ start_time_dt = run_time - pd.to_timedelta(interval) * limit
108
+ df_list = []
109
+ data_len = 0
110
+ params = {
111
+ 'symbol': symbol,
112
+ 'interval': interval,
113
+ 'limit': _limit,
114
+ 'startTime': int(time.mktime(start_time_dt.timetuple())) * 1000
115
+ }
116
+
117
+ while True:
118
+ try:
119
+ if symbol_type == 'swap':
120
+ kline = await async_retry_wrapper(
121
+ self.exchange.fapipublic_get_klines, params=params, func_name=f'获取{symbol}K线', if_exit=False
122
+ )
123
+ else:
124
+ kline = await async_retry_wrapper(
125
+ self.exchange.public_get_klines, params=params, func_name=f'获取{symbol}K线', if_exit=False
126
+ )
127
+ except Exception as e:
128
+ print(f"Error fetching {symbol}: {e}")
129
+ return pd.DataFrame()
130
+
131
+ if not kline:
132
+ break
133
+
134
+ df = pd.DataFrame(kline, dtype='float')
135
+ if df.empty:
136
+ break
137
+
138
+ columns = {0: 'candle_begin_time', 1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume', 6: 'close_time',
139
+ 7: 'quote_volume', 8: 'trade_num', 9: 'taker_buy_base_asset_volume', 10: 'taker_buy_quote_asset_volume',
140
+ 11: 'ignore'}
141
+ df.rename(columns=columns, inplace=True)
142
+ df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'], unit='ms')
143
+
144
+ # 优化:稍后进行排序和去重
145
+
146
+ df_list.append(df)
147
+ data_len += df.shape[0]
148
+
149
+ if data_len >= limit:
150
+ break
151
+
152
+ last_time = int(df.iloc[-1]['candle_begin_time'].timestamp()) * 1000
153
+ if params['startTime'] == last_time:
154
+ break
155
+
156
+ params['startTime'] = last_time
157
+ # 异步 sleep 通常在这里不需要,因为我们要尽可能快,但为了安全起见:
158
+ # await asyncio.sleep(0.01)
159
+
160
+ if not df_list:
161
+ return pd.DataFrame()
162
+
163
+ all_df = pd.concat(df_list, ignore_index=True)
164
+ all_df['symbol'] = symbol
165
+ all_df['symbol_type'] = symbol_type
166
+ all_df.sort_values(by=['candle_begin_time'], inplace=True)
167
+ all_df.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True)
168
+
169
+ all_df = all_df[all_df['candle_begin_time'] + pd.Timedelta(hours=self.utc_offset) < run_time]
170
+ all_df.reset_index(drop=True, inplace=True)
171
+
172
+ return all_df
基础库/common_core/exchange/standard_client.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [币安标准同步客户端]
4
+ 功能:提供传统的同步接口调用方式,用于兼容旧代码或低频操作场景,支持现货和合约的下单、撤单、查询等操作。
5
+ """
6
+
7
+ import time
8
+ from datetime import datetime
9
+
10
+ import pandas as pd
11
+
12
+ from common_core.exchange.base_client import BinanceClient
13
+ from common_core.utils.commons import retry_wrapper
14
+
15
+
16
+ class StandardClient(BinanceClient):
17
+ constants = dict(
18
+ spot_account_type='SPOT',
19
+ reset_page_leverage_api='fapiprivate_post_leverage',
20
+ get_swap_position_api='fapiprivatev2_get_positionrisk',
21
+ get_spot_open_orders_api='private_get_openorders',
22
+ cancel_spot_open_orders_api='private_delete_openorders',
23
+ get_swap_open_orders_api='fapiprivate_get_openorders',
24
+ cancel_swap_open_orders_api='fapiprivate_delete_allopenorders',
25
+ get_spot_my_trades_api='private_get_mytrades',
26
+ )
27
+
28
+ def __init__(self, **config):
29
+ super().__init__(**config)
30
+ self.is_pure_long: bool = config.get('is_pure_long', False)
31
+
32
+ def _set_position_side(self, dual_side_position=False):
33
+ """
34
+ 检查是否是单向持仓模式
35
+ """
36
+ params = {'dualSidePosition': 'true' if dual_side_position else 'false', 'timestamp': ''}
37
+ retry_wrapper(self.exchange.fapiprivate_post_positionside_dual, params=params,
38
+ func_name='fapiprivate_post_positionside_dual', if_exit=False)
39
+ print(f'ℹ️修改持仓模式为单向持仓')
40
+
41
+ def set_single_side_position(self):
42
+
43
+ # 查询持仓模式
44
+ res = retry_wrapper(
45
+ self.exchange.fapiprivate_get_positionside_dual, params={'timestamp': ''}, func_name='设置单向持仓',
46
+ if_exit=False
47
+ )
48
+
49
+ is_duel_side_position = bool(res['dualSidePosition'])
50
+
51
+ # 判断是否是单向持仓模式
52
+ if is_duel_side_position: # 若当前持仓模式不是单向持仓模式,则调用接口修改持仓模式为单向持仓模式
53
+ self._set_position_side(dual_side_position=False)
54
+
55
+ def set_multi_assets_margin(self):
56
+ """
57
+ 检查是否开启了联合保证金模式
58
+ """
59
+ # 查询保证金模式
60
+ res = retry_wrapper(self.exchange.fapiprivate_get_multiassetsmargin, params={'timestamp': ''},
61
+ func_name='fapiprivate_get_multiassetsmargin', if_exit=False)
62
+ # 判断是否开启了联合保证金模式
63
+ if not bool(res['multiAssetsMargin']): # 若联合保证金模式没有开启,则调用接口开启一下联合保证金模式
64
+ params = {'multiAssetsMargin': 'true', 'timestamp': ''}
65
+ retry_wrapper(self.exchange.fapiprivate_post_multiassetsmargin, params=params,
66
+ func_name='fapiprivate_post_multiassetsmargin', if_exit=False)
67
+ print('✅开启联合保证金模式')
68
+
69
+ def get_account_overview(self):
70
+ spot_ticker_data = self.fetch_spot_ticker_price()
71
+ spot_ticker = {_['symbol']: float(_['price']) for _ in spot_ticker_data}
72
+
73
+ swap_account = self.get_swap_account()
74
+ equity = pd.DataFrame(swap_account['assets'])
75
+ swap_usdt_balance = float(equity[equity['asset'] == 'USDT']['walletBalance']) # 获取usdt资产
76
+ # 计算联合保证金
77
+ if self.coin_margin:
78
+ for _symbol, _coin_balance in self.coin_margin.items():
79
+ if _symbol.replace('USDT', '') in equity['asset'].to_list():
80
+ swap_usdt_balance += _coin_balance
81
+ else:
82
+ print(f'⚠️合约账户未找到 {_symbol} 的资产,无法计算 {_symbol} 的保证金')
83
+
84
+ swap_position_df = self.get_swap_position_df()
85
+ spot_position_df = self.get_spot_position_df()
86
+ print('✅获取账户资产数据完成\n')
87
+
88
+ print(f'ℹ️准备处理资产数据...')
89
+ # 获取当前账号现货U的数量
90
+ if 'USDT' in spot_position_df['symbol'].to_list():
91
+ spot_usdt_balance = spot_position_df.loc[spot_position_df['symbol'] == 'USDT', '当前持仓量'].iloc[0]
92
+ # 去除掉USDT现货
93
+ spot_position_df = spot_position_df[spot_position_df['symbol'] != 'USDT']
94
+ else:
95
+ spot_usdt_balance = 0
96
+ # 追加USDT后缀,方便计算usdt价值
97
+ spot_position_df.loc[spot_position_df['symbol'] != 'USDT', 'symbol'] = spot_position_df['symbol'] + 'USDT'
98
+ spot_position_df['仓位价值'] = spot_position_df.apply(
99
+ lambda row: row['当前持仓量'] * spot_ticker.get(row["symbol"], 0), axis=1)
100
+
101
+ # 过滤掉不含报价的币
102
+ spot_position_df = spot_position_df[spot_position_df['仓位价值'] != 0]
103
+ # 仓位价值 小于 5U,无法下单的碎币,单独记录
104
+ dust_spot_df = spot_position_df[spot_position_df['仓位价值'] < 5]
105
+ # 过滤掉仓位价值 小于 5U
106
+ spot_position_df = spot_position_df[spot_position_df['仓位价值'] > 5]
107
+ # 过滤掉BNB,用于抵扣手续费,不参与现货交易
108
+ spot_position_df = spot_position_df[spot_position_df['symbol'] != 'BNBUSDT']
109
+
110
+ # 现货净值
111
+ spot_equity = spot_position_df['仓位价值'].sum() + spot_usdt_balance
112
+
113
+ # 持仓盈亏
114
+ account_pnl = swap_position_df['持仓盈亏'].sum()
115
+
116
+ # =====处理现货持仓列表信息
117
+ # 构建币种的balance信息
118
+ # 币种 : 价值
119
+ spot_assets_pos_dict = spot_position_df[['symbol', '仓位价值']].to_dict(orient='records')
120
+ spot_assets_pos_dict = {_['symbol']: _['仓位价值'] for _ in spot_assets_pos_dict}
121
+
122
+ # 币种 : 数量
123
+ spot_asset_amount_dict = spot_position_df[['symbol', '当前持仓量']].to_dict(orient='records')
124
+ spot_asset_amount_dict = {_['symbol']: _['当前持仓量'] for _ in spot_asset_amount_dict}
125
+
126
+ # =====处理合约持仓列表信息
127
+ # 币种 : 价值
128
+ swap_position_df.reset_index(inplace=True)
129
+ swap_assets_pos_dict = swap_position_df[['symbol', '仓位价值']].to_dict(orient='records')
130
+ swap_assets_pos_dict = {_['symbol']: _['仓位价值'] for _ in swap_assets_pos_dict}
131
+
132
+ # 币种 : 数量
133
+ swap_asset_amount_dict = swap_position_df[['symbol', '当前持仓量']].to_dict(orient='records')
134
+ swap_asset_amount_dict = {_['symbol']: _['当前持仓量'] for _ in swap_asset_amount_dict}
135
+
136
+ # 币种 : pnl
137
+ swap_asset_pnl_dict = swap_position_df[['symbol', '持仓盈亏']].to_dict(orient='records')
138
+ swap_asset_pnl_dict = {_['symbol']: _['持仓盈亏'] for _ in swap_asset_pnl_dict}
139
+
140
+ # 处理完成之后在设置index
141
+ swap_position_df.set_index('symbol', inplace=True)
142
+
143
+ # 账户总净值 = 现货总价值 + 合约usdt + 持仓盈亏
144
+ account_equity = (spot_equity + swap_usdt_balance + account_pnl)
145
+
146
+ print('✅处理资产数据完成\n')
147
+
148
+ return {
149
+ 'usdt_balance': spot_usdt_balance + swap_usdt_balance,
150
+ 'negative_balance': 0,
151
+ 'account_pnl': account_pnl,
152
+ 'account_equity': account_equity,
153
+ 'spot_assets': {
154
+ 'assets_pos_value': spot_assets_pos_dict,
155
+ 'assets_amount': spot_asset_amount_dict,
156
+ 'usdt': spot_usdt_balance,
157
+ 'equity': spot_equity,
158
+ 'dust_spot_df': dust_spot_df,
159
+ 'spot_position_df': spot_position_df
160
+ },
161
+ 'swap_assets': {
162
+ 'assets_pos_value': swap_assets_pos_dict,
163
+ 'assets_amount': swap_asset_amount_dict,
164
+ 'assets_pnl': swap_asset_pnl_dict,
165
+ 'usdt': swap_usdt_balance,
166
+ 'equity': swap_usdt_balance + account_pnl,
167
+ 'swap_position_df': swap_position_df
168
+ }
169
+ }
170
+
171
+ def fetch_transfer_history(self, start_time=datetime.now()):
172
+ """
173
+ 获取划转记录
174
+
175
+ MAIN_UMFUTURE 现货钱包转向U本位合约钱包
176
+ MAIN_MARGIN 现货钱包转向杠杆全仓钱包
177
+
178
+ UMFUTURE_MAIN U本位合约钱包转向现货钱包
179
+ UMFUTURE_MARGIN U本位合约钱包转向杠杆全仓钱包
180
+
181
+ CMFUTURE_MAIN 币本位合约钱包转向现货钱包
182
+
183
+ MARGIN_MAIN 杠杆全仓钱包转向现货钱包
184
+ MARGIN_UMFUTURE 杠杆全仓钱包转向U本位合约钱包
185
+
186
+ MAIN_FUNDING 现货钱包转向资金钱包
187
+ FUNDING_MAIN 资金钱包转向现货钱包
188
+
189
+ FUNDING_UMFUTURE 资金钱包转向U本位合约钱包
190
+ UMFUTURE_FUNDING U本位合约钱包转向资金钱包
191
+
192
+ MAIN_OPTION 现货钱包转向期权钱包
193
+ OPTION_MAIN 期权钱包转向现货钱包
194
+
195
+ UMFUTURE_OPTION U本位合约钱包转向期权钱包
196
+ OPTION_UMFUTURE 期权钱包转向U本位合约钱包
197
+
198
+ MAIN_PORTFOLIO_MARGIN 现货钱包转向统一账户钱包
199
+ PORTFOLIO_MARGIN_MAIN 统一账户钱包转向现货钱包
200
+
201
+ MAIN_ISOLATED_MARGIN 现货钱包转向逐仓账户钱包
202
+ ISOLATED_MARGIN_MAIN 逐仓钱包转向现货账户钱包
203
+ """
204
+ start_time = start_time - pd.Timedelta(days=10)
205
+ add_type = ['CMFUTURE_MAIN', 'MARGIN_MAIN', 'MARGIN_UMFUTURE', 'FUNDING_MAIN', 'FUNDING_UMFUTURE',
206
+ 'OPTION_MAIN', 'OPTION_UMFUTURE', 'PORTFOLIO_MARGIN_MAIN', 'ISOLATED_MARGIN_MAIN']
207
+ reduce_type = ['MAIN_MARGIN', 'UMFUTURE_MARGIN', 'MAIN_FUNDING', 'UMFUTURE_FUNDING', 'MAIN_OPTION',
208
+ 'UMFUTURE_OPTION', 'MAIN_PORTFOLIO_MARGIN', 'MAIN_ISOLATED_MARGIN']
209
+
210
+ result = []
211
+ for _ in add_type + reduce_type:
212
+ params = {
213
+ 'fromSymbol': 'USDT',
214
+ 'startTime': int(start_time.timestamp() * 1000),
215
+ 'type': _,
216
+ 'timestamp': int(round(time.time() * 1000)),
217
+ 'size': 100,
218
+ }
219
+ if _ == 'MAIN_ISOLATED_MARGIN':
220
+ params['toSymbol'] = 'USDT'
221
+ del params['fromSymbol']
222
+ # 获取划转信息(取上一小时到当前时间的划转记录)
223
+ try:
224
+ account_info = self.exchange.sapi_get_asset_transfer(params)
225
+ except BaseException as e:
226
+ print(e)
227
+ print(f'当前账户查询类型【{_}】失败,不影响后续操作,请忽略')
228
+ continue
229
+ if account_info and int(account_info['total']) > 0:
230
+ res = pd.DataFrame(account_info['rows'])
231
+ res['timestamp'] = pd.to_datetime(res['timestamp'], unit='ms')
232
+ res.loc[res['type'].isin(add_type), 'flag'] = 1
233
+ res.loc[res['type'].isin(reduce_type), 'flag'] = -1
234
+ res = res[res['status'] == 'CONFIRMED']
235
+ result.append(res)
236
+
237
+ # 获取主账号与子账号之间划转记录
238
+ result2 = []
239
+ for transfer_type in [1, 2]: # 1: 划入。从主账号划转进来 2: 划出。从子账号划转出去
240
+ params = {
241
+ 'asset': 'USDT',
242
+ 'type': transfer_type,
243
+ 'startTime': int(start_time.timestamp() * 1000),
244
+ }
245
+ try:
246
+ account_info = self.exchange.sapi_get_sub_account_transfer_subuserhistory(params)
247
+ except BaseException as e:
248
+ print(e)
249
+ print(f'当前账户查询类型【{transfer_type}】失败,不影响后续操作,请忽略')
250
+ continue
251
+ if account_info and len(account_info):
252
+ res = pd.DataFrame(account_info)
253
+ res['time'] = pd.to_datetime(res['time'], unit='ms')
254
+ res.rename(columns={'qty': 'amount', 'time': 'timestamp'}, inplace=True)
255
+ res.loc[res['toAccountType'] == 'SPOT', 'flag'] = 1 if transfer_type == 1 else -1
256
+ res.loc[res['toAccountType'] == 'USDT_FUTURE', 'flag'] = 1 if transfer_type == 1 else -1
257
+ res = res[res['status'] == 'SUCCESS']
258
+ res = res[res['toAccountType'].isin(['SPOT', 'USDT_FUTURE'])]
259
+ result2.append(res)
260
+
261
+ # 将账号之间的划转与单账号内部换转数据合并
262
+ result.extend(result2)
263
+ if not len(result):
264
+ return pd.DataFrame()
265
+
266
+ all_df = pd.concat(result, ignore_index=True)
267
+ all_df.drop_duplicates(subset=['timestamp', 'tranId', 'flag'], inplace=True)
268
+ all_df = all_df[all_df['asset'] == 'USDT']
269
+ all_df.sort_values('timestamp', inplace=True)
270
+
271
+ all_df['amount'] = all_df['amount'].astype(float) * all_df['flag']
272
+ all_df.rename(columns={'amount': '账户总净值'}, inplace=True)
273
+ all_df['type'] = 'transfer'
274
+ all_df = all_df[['timestamp', '账户总净值', 'type']]
275
+ all_df['timestamp'] = all_df['timestamp'] + pd.Timedelta(hours=self.utc_offset)
276
+ all_df.reset_index(inplace=True, drop=True)
277
+
278
+ all_df['time'] = all_df['timestamp']
279
+ result_df = all_df.resample(rule='1H', on='timestamp').agg(
280
+ {'time': 'last', '账户总净值': 'sum', 'type': 'last'})
281
+ result_df = result_df[result_df['type'].notna()]
282
+ result_df.reset_index(inplace=True, drop=True)
283
+
284
+ return result_df
基础库/common_core/pyproject.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quant-unified-common-core"
7
+ version = "0.1.0"
8
+ description = "Quant Unified 通用核心库:交易所客户端、回测、风控与工具集"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{ name = "Quant Unified Team" }]
12
+ license = { text = "Proprietary" }
13
+ dependencies = [
14
+ "ccxt>=4.3.0",
15
+ "pandas>=2.2.0",
16
+ "numpy>=1.26.0",
17
+ "aiohttp>=3.9.0",
18
+ "numba>=0.59.0",
19
+ "plotly>=5.18.0",
20
+ "scipy>=1.10.0",
21
+ ]
22
+
23
+ [tool.setuptools]
24
+ packages = { find = { where = ["."] } }
25
+
基础库/common_core/quant_unified_common_core.egg-info/PKG-INFO ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: quant-unified-common-core
3
+ Version: 0.1.0
4
+ Summary: Quant Unified 通用核心库:交易所客户端、回测、风控与工具集
5
+ Author: Quant Unified Team
6
+ License: Proprietary
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: ccxt>=4.3.0
10
+ Requires-Dist: pandas>=2.2.0
11
+ Requires-Dist: numpy>=1.26.0
12
+ Requires-Dist: aiohttp>=3.9.0
13
+ Requires-Dist: numba>=0.59.0
14
+ Requires-Dist: plotly>=5.18.0
15
+ Requires-Dist: scipy>=1.10.0
基础库/common_core/quant_unified_common_core.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pyproject.toml
2
+ backtest/__init__.py
3
+ backtest/equity.py
4
+ backtest/evaluate.py
5
+ backtest/figure.py
6
+ backtest/rebalance.py
7
+ backtest/simulator.py
8
+ backtest/version.py
9
+ exchange/__init__.py
10
+ exchange/base_client.py
11
+ exchange/binance_async.py
12
+ exchange/standard_client.py
13
+ quant_unified_common_core.egg-info/PKG-INFO
14
+ quant_unified_common_core.egg-info/SOURCES.txt
15
+ quant_unified_common_core.egg-info/dependency_links.txt
16
+ quant_unified_common_core.egg-info/requires.txt
17
+ quant_unified_common_core.egg-info/top_level.txt
18
+ risk_ctrl/liquidation.py
19
+ risk_ctrl/test_liquidation.py
20
+ utils/__init__.py
21
+ utils/async_commons.py
22
+ utils/commons.py
23
+ utils/dingding.py
24
+ utils/factor_hub.py
25
+ utils/functions.py
26
+ utils/path_kit.py
基础库/common_core/quant_unified_common_core.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
基础库/common_core/quant_unified_common_core.egg-info/requires.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ ccxt>=4.3.0
2
+ pandas>=2.2.0
3
+ numpy>=1.26.0
4
+ aiohttp>=3.9.0
5
+ numba>=0.59.0
6
+ plotly>=5.18.0
7
+ scipy>=1.10.0
基础库/common_core/quant_unified_common_core.egg-info/top_level.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ backtest
2
+ exchange
3
+ risk_ctrl
4
+ utils
基础库/common_core/risk_ctrl/liquidation.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [爆仓检查模块]
4
+ 功能:提供通用的保证金率计算与爆仓检测逻辑,支持单币种和多币种组合模式。
5
+ """
6
+ import numpy as np
7
+
8
+ class LiquidationChecker:
9
+ """
10
+ 通用爆仓检查器
11
+ """
12
+ def __init__(self, min_margin_rate=0.005):
13
+ """
14
+ 初始化
15
+ :param min_margin_rate: 维持保证金率 (默认 0.5%)
16
+ """
17
+ self.min_margin_rate = min_margin_rate
18
+
19
+ def check_margin_rate(self, equity, position_value):
20
+ """
21
+ 检查保证金率
22
+ :param equity: 当前账户权益 (USDT)
23
+ :param position_value: 当前持仓名义价值 (USDT, 绝对值之和)
24
+ :return: (is_liquidated, margin_rate)
25
+ is_liquidated: 是否爆仓 (True/False)
26
+ margin_rate: 当前保证金率
27
+ """
28
+ if position_value < 1e-8:
29
+ # 无持仓,无限安全
30
+ return False, 999.0
31
+
32
+ margin_rate = equity / float(position_value)
33
+
34
+ is_liquidated = margin_rate < self.min_margin_rate
35
+
36
+ return is_liquidated, margin_rate
37
+
38
+ @staticmethod
39
+ def calculate_margin_rate(equity, position_value):
40
+ """
41
+ 静态方法:纯计算保证金率
42
+ """
43
+ if position_value < 1e-8:
44
+ return 999.0
45
+ return equity / float(position_value)
基础库/common_core/risk_ctrl/test_liquidation.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import unittest
3
+ from common_core.risk_ctrl.liquidation import LiquidationChecker
4
+
5
+ class TestLiquidationChecker(unittest.TestCase):
6
+ def setUp(self):
7
+ self.checker = LiquidationChecker(min_margin_rate=0.005)
8
+
9
+ def test_safe_state(self):
10
+ # 权益 10000, 持仓 10000 -> 保证金率 100% -> 安全
11
+ is_liq, rate = self.checker.check_margin_rate(10000, 10000)
12
+ self.assertFalse(is_liq)
13
+ self.assertEqual(rate, 1.0)
14
+
15
+ def test_warning_state(self):
16
+ # 权益 100, 持仓 10000 -> 保证金率 1% -> 安全但危险
17
+ is_liq, rate = self.checker.check_margin_rate(100, 10000)
18
+ self.assertFalse(is_liq)
19
+ self.assertEqual(rate, 0.01)
20
+
21
+ def test_liquidation_state(self):
22
+ # 权益 40, 持仓 10000 -> 保证金率 0.4% < 0.5% -> 爆仓
23
+ is_liq, rate = self.checker.check_margin_rate(40, 10000)
24
+ self.assertTrue(is_liq)
25
+ self.assertEqual(rate, 0.004)
26
+
27
+ def test_zero_position(self):
28
+ # 无持仓
29
+ is_liq, rate = self.checker.check_margin_rate(10000, 0)
30
+ self.assertFalse(is_liq)
31
+ self.assertEqual(rate, 999.0)
32
+
33
+ if __name__ == '__main__':
34
+ unittest.main()
基础库/common_core/tests/test_orderbook_replay.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import unittest
5
+ from Quant_Unified.基础库.common_core.utils.orderbook_replay import OrderBook
6
+
7
+ class TestOrderBookReplay(unittest.TestCase):
8
+ def setUp(self):
9
+ self.ob = OrderBook("BTCUSDT")
10
+
11
+ def test_apply_delta_basic(self):
12
+ # 增加买单
13
+ self.ob.apply_delta("buy", 6000000, 1000)
14
+ self.ob.apply_delta("buy", 6000100, 2000)
15
+
16
+ # 增加卖单
17
+ self.ob.apply_delta("sell", 6000200, 1500)
18
+ self.ob.apply_delta("sell", 6000300, 2500)
19
+
20
+ snap = self.ob.get_snapshot(depth=5)
21
+
22
+ # 验证排序: Bids 应该降序
23
+ self.assertEqual(snap['bids'][0][0], 6000100)
24
+ self.assertEqual(snap['bids'][1][0], 6000000)
25
+
26
+ # 验证排序: Asks 应该升序
27
+ self.assertEqual(snap['asks'][0][0], 6000200)
28
+ self.assertEqual(snap['asks'][1][0], 6000300)
29
+
30
+ def test_update_and_delete(self):
31
+ self.ob.apply_delta("buy", 6000000, 1000)
32
+ # 更新
33
+ self.ob.apply_delta("buy", 6000000, 5000)
34
+ self.assertEqual(self.ob.bids[6000000], 5000)
35
+
36
+ # 删除
37
+ self.ob.apply_delta("buy", 6000000, 0)
38
+ self.assertNotIn(6000000, self.ob.bids)
39
+
40
+ def test_flat_snapshot(self):
41
+ self.ob.apply_delta("bid", 100, 10)
42
+ self.ob.apply_delta("ask", 110, 20)
43
+
44
+ flat = self.ob.get_flat_snapshot(depth=2)
45
+ self.assertEqual(flat['bid1_p'], 100)
46
+ self.assertEqual(flat['bid1_q'], 10)
47
+ self.assertEqual(flat['ask1_p'], 110)
48
+ self.assertEqual(flat['ask1_q'], 20)
49
+ # 深度不够的应该补 0
50
+ self.assertEqual(flat['bid2_p'], 0)
51
+
52
+ if __name__ == '__main__':
53
+ unittest.main()
基础库/common_core/utils/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """
2
+ """
基础库/common_core/utils/async_commons.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [异步工具函数库]
4
+ 功能:提供 async/await 语境下的辅助工具,重点包含异步重试装饰器,确保高并发任务的健壮性。
5
+ """
6
+ import asyncio
7
+ import traceback
8
+ from functools import wraps
9
+
10
+ async def async_retry_wrapper(func, params={}, func_name='', if_exit=True):
11
+ """
12
+ Async retry wrapper
13
+ """
14
+ max_retries = 3
15
+ for i in range(max_retries):
16
+ try:
17
+ if params:
18
+ return await func(params)
19
+ else:
20
+ return await func()
21
+ except Exception as e:
22
+ print(f'❌{func_name} 出错: {e}')
23
+ if i < max_retries - 1:
24
+ print(f'⏳正在重试 ({i+1}/{max_retries})...')
25
+ await asyncio.sleep(1)
26
+ else:
27
+ print(traceback.format_exc())
28
+ if if_exit:
29
+ raise e
30
+ return None
基础库/common_core/utils/commons.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [通用工具函数库]
4
+ 功能:包含重试装饰器、时间计算、精度处理等常用辅助功能,是系统稳定运行的基础组件。
5
+ """
6
+
7
+ import time
8
+ import traceback
9
+ from datetime import datetime, timedelta
10
+
11
+ import pandas as pd
12
+
13
+
14
+ # ===重试机制
15
+ def retry_wrapper(func, params=None, func_name='', retry_times=5, sleep_seconds=5, if_exit=True):
16
+ """
17
+ 需要在出错时不断重试的函数,例如和交易所交互,可以使用本函数调用。
18
+ :param func: 需要重试的函数名
19
+ :param params: 参数
20
+ :param func_name: 方法名称
21
+ :param retry_times: 重试次数
22
+ :param sleep_seconds: 报错后的sleep时间
23
+ :param if_exit: 报错是否退出程序
24
+ :return:
25
+ """
26
+ if params is None:
27
+ params = {}
28
+ for _ in range(retry_times):
29
+ try:
30
+ if 'timestamp' in params:
31
+ from core.binance.base_client import BinanceClient
32
+ params['timestamp'] = int(time.time() * 1000) - BinanceClient.diff_timestamp
33
+ result = func(params=params)
34
+ return result
35
+ except Exception as e:
36
+ print(f'❌{func_name} 报错,程序暂停{sleep_seconds}(秒)')
37
+ print(e)
38
+ print(params)
39
+ msg = str(e).strip()
40
+ # 出现1021错误码之后,刷新与交易所的时差
41
+ if 'binance Account has insufficient balance for requested action' in msg:
42
+ print(f'⚠️{func_name} 现货下单资金不足')
43
+ raise ValueError(func_name, '现货下单资金不足')
44
+ elif '-2022' in msg:
45
+ print(f'⚠️{func_name} ReduceOnly订单被拒绝, 合约仓位已经平完')
46
+ raise ValueError(func_name, 'ReduceOnly订单被拒绝, 合约仓位已经平完')
47
+ elif '-4118' in msg:
48
+ print(f'⚠️{func_name} 统一账户 ReduceOnly订单被拒绝, 合约仓位已经平完')
49
+ raise ValueError(func_name, '统一账户 ReduceOnly订单被拒绝, 合约仓位已经平完')
50
+ elif '-2019' in msg:
51
+ print(f'⚠️{func_name} 合约下单资金不足')
52
+ raise ValueError(func_name, '合约下单资金不足')
53
+ elif '-2015' in msg and 'Invalid API-key' in msg:
54
+ # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action, request ip: xxx.xxx.xxx.xxx"}
55
+ print(f'❌{func_name} API配置错误,可能写错或未配置权限')
56
+ break
57
+ elif '-1121' in msg and 'Invalid symbol' in msg:
58
+ # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action, request ip: xxx.xxx.xxx.xxx"}
59
+ print(f'❌{func_name} 没有交易对')
60
+ break
61
+ elif '-5013' in msg and 'Asset transfer failed' in msg:
62
+ print(f'❌{func_name} 余额不足,无法资金划转')
63
+ break
64
+ else:
65
+ print(f'❌{e},报错内容如下')
66
+ print(traceback.format_exc())
67
+ time.sleep(sleep_seconds)
68
+ else:
69
+ if if_exit:
70
+ raise ValueError(func_name, '报错重试次数超过上限,程序退出。')
71
+
72
+
73
+ # ===下次运行时间
74
+ def next_run_time(time_interval, ahead_seconds=5):
75
+ """
76
+ 根据time_interval,计算下次运行的时间。
77
+ PS:目前只支持分钟和小时。
78
+ :param time_interval: 运行的周期,15m,1h
79
+ :param ahead_seconds: 预留的目标时间和当前时间之间计算的间隙
80
+ :return: 下次运行的时间
81
+
82
+ 案例:
83
+ 15m 当前时间为:12:50:51 返回时间为:13:00:00
84
+ 15m 当前时间为:12:39:51 返回时间为:12:45:00
85
+
86
+ 10m 当前时间为:12:38:51 返回时间为:12:40:00
87
+ 10m 当前时间为:12:11:01 返回时间为:12:20:00
88
+
89
+ 5m 当前时间为:12:33:51 返回时间为:12:35:00
90
+ 5m 当前时间为:12:34:51 返回时间为:12:40:00
91
+
92
+ 30m 当前时间为:21日的23:33:51 返回时间为:22日的00:00:00
93
+ 30m 当前时间为:14:37:51 返回时间为:14:56:00
94
+
95
+ 1h 当前时间为:14:37:51 返回时间为:15:00:00
96
+ """
97
+ # 检测 time_interval 是否配置正确,并将 时间单位 转换成 可以解析的时间单位
98
+ if time_interval.endswith('m') or time_interval.endswith('h'):
99
+ pass
100
+ elif time_interval.endswith('T'): # 分钟兼容使用T配置,例如 15T 30T
101
+ time_interval = time_interval.replace('T', 'm')
102
+ elif time_interval.endswith('H'): # 小时兼容使用H配置, 例如 1H 2H
103
+ time_interval = time_interval.replace('H', 'h')
104
+ else:
105
+ print('⚠️time_interval格式不符合规范。程序exit')
106
+ exit()
107
+
108
+ # 将 time_interval 转换成 时间类型
109
+ ti = pd.to_timedelta(time_interval)
110
+ # 获取当前时间
111
+ now_time = datetime.now()
112
+ # 计算��日时间的 00:00:00
113
+ this_midnight = now_time.replace(hour=0, minute=0, second=0, microsecond=0)
114
+ # 每次计算时间最小时间单位1分钟
115
+ min_step = timedelta(minutes=1)
116
+ # 目标时间:设置成默认时间,并将 秒,毫秒 置零
117
+ target_time = now_time.replace(second=0, microsecond=0)
118
+
119
+ while True:
120
+ # 增加一个最小时间单位
121
+ target_time = target_time + min_step
122
+ # 获取目标时间已经从当日 00:00:00 走了多少时间
123
+ delta = target_time - this_midnight
124
+ # delta 时间可以整除 time_interval,表明时间是 time_interval 的倍数,是一个 整时整分的时间
125
+ # 目标时间 与 当前时间的 间隙超过 ahead_seconds,说明 目标时间 比当前时间大,是最靠近的一个周期时间
126
+ if int(delta.total_seconds()) % int(ti.total_seconds()) == 0 and int(
127
+ (target_time - now_time).total_seconds()) >= ahead_seconds:
128
+ break
129
+
130
+ return target_time
131
+
132
+
133
+ # ===依据时间间隔, 自动计算并休眠到指定时间
134
+ def sleep_until_run_time(time_interval, ahead_time=1, if_sleep=True, cheat_seconds=0):
135
+ """
136
+ 根据next_run_time()函数计算出下次程序运行的时候,然后sleep至该时间
137
+ :param time_interval: 时间周期配置,用于计算下个周期的时间
138
+ :param if_sleep: 是否进行sleep
139
+ :param ahead_time: 最小时间误差
140
+ :param cheat_seconds: 相对于下个周期时间,提前或延后多长时间, 100: 提前100秒; -50:延后50秒
141
+ :return:
142
+ """
143
+ # 计算下次运行时间
144
+ run_time = next_run_time(time_interval, ahead_time)
145
+ # 计算延迟之后的目标时间
146
+ target_time = run_time
147
+ # 配置 cheat_seconds ,对目标时间进行 提前 或者 延后
148
+ if cheat_seconds != 0:
149
+ target_time = run_time - timedelta(seconds=cheat_seconds)
150
+ print(f'⏳程序等待下次运行,下次时间:{target_time}')
151
+
152
+ # sleep
153
+ if if_sleep:
154
+ # 计算获得的 run_time 小于 now, sleep就会一直sleep
155
+ _now = datetime.now()
156
+ if target_time > _now: # 计算的下个周期时间超过当前时间,直接追加一个时间周期
157
+ time.sleep(max(0, (target_time - _now).seconds))
158
+ while True: # 在靠近目标时间时
159
+ if datetime.now() > target_time:
160
+ time.sleep(1)
161
+ break
162
+
163
+ return run_time
164
+
165
+
166
+ # ===根据精度对数字进行就低不就高处理
167
+ def apply_precision(number: int | float, decimals: int) -> float:
168
+ """
169
+ 根据精度对数字进行就低不就高处理
170
+ :param number: 数字
171
+ :param decimals: 精度
172
+ :return:
173
+ (360.731, 0)结果是360,
174
+ (123.65, 1)结果是123.6
175
+ """
176
+ multiplier = 10 ** decimals
177
+ return int(number * multiplier) / multiplier
178
+
179
+
180
+ def bool_str(true_or_false):
181
+ return '🔵[OK]' if true_or_false else '🟡[NO]'
基础库/common_core/utils/dingding.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [消息推送工具]
4
+ 功能:集成钉钉/企业微信 Webhook,用于发送实盘交易信号、报错报警和状态监控,让您随时掌握系统动态。
5
+ """
6
+
7
+ import base64
8
+ import hashlib
9
+ import os.path
10
+ import requests
11
+ import json
12
+ import traceback
13
+ from datetime import datetime
14
+ from config import proxy
15
+
16
+
17
+ # 企业微信通知
18
+ def send_wechat_work_msg(content, url):
19
+ if not url:
20
+ print('未配置wechat_webhook_url,不发送信息')
21
+ return
22
+ try:
23
+ data = {
24
+ "msgtype": "text",
25
+ "text": {
26
+ "content": content + '\n' + datetime.now().strftime("%Y-%m-%d %H:%M:%S")
27
+ }
28
+ }
29
+ r = requests.post(url, data=json.dumps(data), timeout=10, proxies=proxy)
30
+ print(f'调用企业微信接口返回: {r.text}')
31
+ print('成功发送企业微信')
32
+ except Exception as e:
33
+ print(f"发送企业微信失败:{e}")
34
+ print(traceback.format_exc())
35
+
36
+
37
+ # 上传图片,解析bytes
38
+ class MyEncoder(json.JSONEncoder):
39
+
40
+ def default(self, obj):
41
+ """
42
+ 只要检查到了是bytes类型的数据就把它转为str类型
43
+ :param obj:
44
+ :return:
45
+ """
46
+ if isinstance(obj, bytes):
47
+ return str(obj, encoding='utf-8')
48
+ return json.JSONEncoder.default(self, obj)
49
+
50
+
51
+ # 企业微信发送图片
52
+ def send_wechat_work_img(file_path, url):
53
+ if not os.path.exists(file_path):
54
+ print('找不到图片')
55
+ return
56
+ if not url:
57
+ print('未配置wechat_webhook_url,不发送信息')
58
+ return
59
+ try:
60
+ with open(file_path, 'rb') as f:
61
+ image_content = f.read()
62
+ image_base64 = base64.b64encode(image_content).decode('utf-8')
63
+ md5 = hashlib.md5()
64
+ md5.update(image_content)
65
+ image_md5 = md5.hexdigest()
66
+ data = {
67
+ 'msgtype': 'image',
68
+ 'image': {
69
+ 'base64': image_base64,
70
+ 'md5': image_md5
71
+ }
72
+ }
73
+ # 服务器上传bytes图片的时候,json.dumps解析会出错,需要自己手动去转一下
74
+ r = requests.post(url, data=json.dumps(data, cls=MyEncoder, indent=4), timeout=10, proxies=proxy)
75
+ print(f'调用企业微信接口返回: {r.text}')
76
+ print('成功发送企业微信')
77
+ except Exception as e:
78
+ print(f"发送企业微信失败:{e}")
79
+ print(traceback.format_exc())
80
+ finally:
81
+ if os.path.exists(file_path):
82
+ os.remove(file_path)
83
+
84
+
85
+ def send_msg_for_order(order_param, order_res, url):
86
+ """
87
+ 发送下单信息,只有出问题才会推送,正常下单不在推送信息
88
+ """
89
+ if not url:
90
+ print('未配置wechat_webhook_url,不发送信息')
91
+ return
92
+ msg = ''
93
+ try:
94
+ for _ in range(len(order_param)):
95
+ if 'msg' in order_res[_].keys():
96
+ msg += f'币种:{order_param[_]["symbol"]}\n'
97
+ msg += f'方向:{"做多" if order_param[_]["side"] == "BUY" else "做空"}\n'
98
+ msg += f'价格:{order_param[_]["price"]}\n'
99
+ msg += f'数量:{order_param[_]["quantity"]}\n'
100
+ msg += f'下单结果:{order_res[_]["msg"]}'
101
+ msg += '\n' * 2
102
+ except BaseException as e:
103
+ print('send_msg_for_order ERROR', e)
104
+ print(traceback.format_exc())
105
+
106
+ if msg:
107
+ send_wechat_work_msg(msg, url)
基础库/common_core/utils/factor_hub.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 选币框架
3
+ """
4
+ import importlib
5
+
6
+ import pandas as pd
7
+
8
+
9
+ class DummyFactor:
10
+ """
11
+ !!!!抽象因子对象,仅用于代码提示!!!!
12
+ """
13
+
14
+ def signal(self, *args) -> pd.DataFrame:
15
+ raise NotImplementedError
16
+
17
+ def signal_multi_params(self, df, param_list: list | set | tuple) -> dict:
18
+ raise NotImplementedError
19
+
20
+
21
+ class FactorHub:
22
+ _factor_cache = {}
23
+
24
+ # noinspection PyTypeChecker
25
+ @staticmethod
26
+ def get_by_name(factor_name) -> DummyFactor:
27
+ if factor_name in FactorHub._factor_cache:
28
+ return FactorHub._factor_cache[factor_name]
29
+
30
+ try:
31
+ # 构造模块名
32
+ module_name = f"factors.{factor_name}"
33
+
34
+ # 动态导入模块
35
+ factor_module = importlib.import_module(module_name)
36
+
37
+ # 创建一个包含模块变量和函数的字典
38
+ factor_content = {
39
+ name: getattr(factor_module, name) for name in dir(factor_module)
40
+ if not name.startswith("__")
41
+ }
42
+
43
+ # 创建一个包含这些变量和函数的对象
44
+ factor_instance = type(factor_name, (), factor_content)
45
+
46
+ # 缓存策略对象
47
+ FactorHub._factor_cache[factor_name] = factor_instance
48
+
49
+ return factor_instance
50
+ except ModuleNotFoundError:
51
+ raise ValueError(f"Factor {factor_name} not found.")
52
+ except AttributeError:
53
+ raise ValueError(f"Error accessing factor content in module {factor_name}.")
54
+
55
+
56
+ # 使用示例
57
+ if __name__ == "__main__":
58
+ factor = FactorHub.get_by_name("PctChange")
基础库/common_core/utils/functions.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 选币框架
3
+ """
4
+ import warnings
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from config import stable_symbol
12
+
13
+ warnings.filterwarnings('ignore')
14
+
15
+
16
+ # =====策略相关函数
17
+ def del_insufficient_data(symbol_candle_data) -> Dict[str, pd.DataFrame]:
18
+ """
19
+ 删除数据长度不足的币种信息
20
+
21
+ :param symbol_candle_data:
22
+ :return
23
+ """
24
+ # ===删除成交量为0的线数据、k线数不足的币种
25
+ symbol_list = list(symbol_candle_data.keys())
26
+ for symbol in symbol_list:
27
+ # 删除空的数据
28
+ if symbol_candle_data[symbol] is None or symbol_candle_data[symbol].empty:
29
+ del symbol_candle_data[symbol]
30
+ continue
31
+ # 删除该币种成交量=0的k线
32
+ # symbol_candle_data[symbol] = symbol_candle_data[symbol][symbol_candle_data[symbol]['volume'] > 0]
33
+
34
+ return symbol_candle_data
35
+
36
+
37
+ def ignore_error(anything):
38
+ return anything
39
+
40
+
41
+ def load_min_qty(file_path: Path) -> (int, Dict[str, int]):
42
+ # 读取min_qty文件并转为dict格式
43
+ min_qty_df = pd.read_csv(file_path, encoding='utf-8-sig')
44
+ min_qty_df['最小下单量'] = -np.log10(min_qty_df['最小下单量']).round().astype(int)
45
+ default_min_qty = min_qty_df['最小下单量'].max()
46
+ min_qty_df.set_index('币种', inplace=True)
47
+ min_qty_dict = min_qty_df['最小下单量'].to_dict()
48
+
49
+ return default_min_qty, min_qty_dict
50
+
51
+
52
+ def is_trade_symbol(symbol, black_list=()) -> bool:
53
+ """
54
+ 过滤掉不能用于交易的币种,比如稳定币、非USDT交易对,以及一些杠杆币
55
+ :param symbol: 交易对
56
+ :param black_list: 黑名单
57
+ :return: 是否可以进入交易,True可以参与选币,False不参与
58
+ """
59
+ # 如果symbol为空
60
+ # 或者是.开头的隐藏文件
61
+ # 或者不是USDT结尾的币种
62
+ # 或者在黑名单里
63
+ if not symbol or symbol.startswith('.') or not symbol.endswith('USDT') or symbol in black_list:
64
+ return False
65
+
66
+ # 筛选杠杆币
67
+ base_symbol = symbol.upper().replace('-USDT', 'USDT')[:-4]
68
+ if base_symbol.endswith(('UP', 'DOWN', 'BEAR', 'BULL')) and base_symbol != 'JUP' or base_symbol in stable_symbol:
69
+ return False
70
+ else:
71
+ return True
基础库/common_core/utils/orderbook_replay.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ L2 订单簿重放引擎
6
+ 功能:根据增量更新(Delta)维护内存中的盘口状态,并支持快照提取。
7
+ 支持价格和数量的整数存储以提高性能。
8
+ """
9
+
10
+ from sortedcontainers import SortedDict
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class OrderBook:
16
+ def __init__(self, symbol: str):
17
+ self.symbol = symbol
18
+ # bids 降序排列 (最高价在最前)
19
+ self.bids = SortedDict(lambda x: -x)
20
+ # asks 升序排列 (最低价在最前)
21
+ self.asks = SortedDict()
22
+ self.last_update_id = 0
23
+ self.timestamp = None
24
+
25
+ def apply_delta(self, side: str, price_int: int, amount_int: int):
26
+ """
27
+ 应用单条增量更新
28
+ :param side: 'buy' 或 'sell' (Tardis 格式) 或 'bid'/'ask'
29
+ :param price_int: 整数价格
30
+ :param amount_int: 整数数量 (为 0 表示删除该档位)
31
+ """
32
+ target_dict = self.bids if side in ['buy', 'bid'] else self.asks
33
+
34
+ if amount_int <= 0:
35
+ target_dict.pop(price_int, None)
36
+ else:
37
+ target_dict[price_int] = amount_int
38
+
39
+ def reset(self):
40
+ """重置盘口"""
41
+ self.bids.clear()
42
+ self.asks.clear()
43
+
44
+ def get_snapshot(self, depth: int = 50) -> dict:
45
+ """
46
+ 获取当前盘口的快照
47
+ :param depth: 档位深度
48
+ :return: 包含 bids 和 asks 列表的字典
49
+ """
50
+ bid_keys = list(self.bids.keys())[:depth]
51
+ bid_list = [(p, self.bids[p]) for p in bid_keys]
52
+
53
+ ask_keys = list(self.asks.keys())[:depth]
54
+ ask_list = [(p, self.asks[p]) for p in ask_keys]
55
+
56
+ return {
57
+ "symbol": self.symbol,
58
+ "bids": bid_list,
59
+ "asks": ask_list
60
+ }
61
+
62
+ def get_flat_snapshot(self, depth: int = 50) -> dict:
63
+ """
64
+ 获取打平的快照格式,方便直接喂给模型 (例如 bid1_p, bid1_q ...)
65
+ """
66
+ result = {}
67
+
68
+ bid_keys = list(self.bids.keys())
69
+ # 处理 Bids
70
+ for i in range(depth):
71
+ if i < len(bid_keys):
72
+ price = bid_keys[i]
73
+ amount = self.bids[price]
74
+ result[f"bid{i+1}_p"] = price
75
+ result[f"bid{i+1}_q"] = amount
76
+ else:
77
+ result[f"bid{i+1}_p"] = 0
78
+ result[f"bid{i+1}_q"] = 0
79
+
80
+ ask_keys = list(self.asks.keys())
81
+ # 处理 Asks
82
+ for i in range(depth):
83
+ if i < len(ask_keys):
84
+ price = ask_keys[i]
85
+ amount = self.asks[price]
86
+ result[f"ask{i+1}_p"] = price
87
+ result[f"ask{i+1}_q"] = amount
88
+ else:
89
+ result[f"ask{i+1}_p"] = 0
90
+ result[f"ask{i+1}_q"] = 0
91
+
92
+ return result
基础库/common_core/utils/path_kit.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quant Unified 量化交易系统
3
+ [路径管理工具]
4
+ 功能:自动识别操作系统,统一管理数据、配置、日志等文件夹路径,解决硬编码路径带来的跨平台问题。
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+
10
+ # 通过当前文件的位置,获取项目根目录
11
+ PROJECT_ROOT = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir, os.path.pardir, os.path.pardir))
12
+
13
+
14
+ # ====================================================================================================
15
+ # ** 功能函数 **
16
+ # - get_folder_by_root: 获取基于某一个地址的绝对路径
17
+ # - get_folder_path: 获取相对于项目根目录的,文件夹的绝对路径
18
+ # - get_file_path: 获取相对于项目根目录的,文件的绝对路径
19
+ # ====================================================================================================
20
+ def get_folder_by_root(root, *paths, auto_create=True) -> str:
21
+ """
22
+ 获取基于某一个地址的绝对路径
23
+ :param root: 相对的地址,默认为运行脚本同目录
24
+ :param paths: 路径
25
+ :param auto_create: 是否自动创建需要的文件夹们
26
+ :return: 绝对路径
27
+ """
28
+ _full_path = os.path.join(root, *paths)
29
+ if auto_create and (not os.path.exists(_full_path)): # 判断文件夹是否存在
30
+ try:
31
+ os.makedirs(_full_path) # 不存在则创建
32
+ except FileExistsError:
33
+ pass # 并行过程中,可能造成冲突
34
+ return str(_full_path)
35
+
36
+
37
+ def get_folder_path(*paths, auto_create=True, as_path_type=False) -> str | Path:
38
+ """
39
+ 获取相对于项目根目录的,文件夹的绝对路径
40
+ :param paths: 文件夹路径
41
+ :param auto_create: 是否自动创建
42
+ :param as_path_type: 是否返回Path对象
43
+ :return: 文件夹绝对路径
44
+ """
45
+ _p = get_folder_by_root(PROJECT_ROOT, *paths, auto_create=auto_create)
46
+ if as_path_type:
47
+ return Path(_p)
48
+ return _p
49
+
50
+
51
+ def get_file_path(*paths, auto_create=True, as_path_type=False) -> str | Path:
52
+ """
53
+ 获取相对于项目根目录的,文件的绝对路径
54
+ :param paths: 文件路径
55
+ :param auto_create: 是否自动创建
56
+ :param as_path_type: 是否返回Path对象
57
+ :return: 文件绝对路径
58
+ """
59
+ parent = get_folder_path(*paths[:-1], auto_create=auto_create, as_path_type=True)
60
+ _p = parent / paths[-1]
61
+ if as_path_type:
62
+ return _p
63
+ return str(_p)
64
+
65
+
66
+ if __name__ == '__main__':
67
+ """
68
+ DEMO
69
+ """
70
+ print(get_file_path('data', 'xxx.pkl'))
71
+ print(get_folder_path('系统日志'))
72
+ print(get_folder_by_root('data', 'center', 'yyds', auto_create=False))
基础库/通用选币回测框架/因子库/9_SharpeMomentum.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 多头动量因子9 | 夏普动量因子
4
+ author: D Luck
5
+ """
6
+
7
+ import pandas as pd
8
+ import numpy as np
9
+
10
+ def signal(*args):
11
+ df, n, factor_name = args
12
+
13
+ ret = df['close'].pct_change()
14
+ mean_ = ret.rolling(n, min_periods=1).mean()
15
+ std_ = ret.rolling(n, min_periods=1).std()
16
+
17
+ df[factor_name] = mean_ / (std_ + 1e-12)
18
+ return df
19
+
20
+
21
+ def signal_multi_params(df, param_list) -> dict:
22
+ ret_dic = {}
23
+ ret = df['close'].pct_change()
24
+
25
+ for param in param_list:
26
+ n = int(param)
27
+
28
+ mean_ = ret.rolling(n).mean()
29
+ std_ = ret.rolling(n).std()
30
+
31
+ ret_dic[str(param)] = mean_ / (std_ + 1e-12)
32
+
33
+ return ret_dic
基础库/通用选币回测框架/因子库/ADX.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def signal(*args):
6
+ df = args[0].copy() # 避免修改原数据
7
+ N = args[1]
8
+ factor_name = args[2]
9
+
10
+ # 1. 计算 True Range (TR)
11
+ prev_close = df['close'].shift(1)
12
+ tr1 = df['high'] - df['low']
13
+ tr2 = (df['high'] - prev_close).abs()
14
+ tr3 = (df['low'] - prev_close).abs()
15
+ df['tr'] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
16
+
17
+ # 2. 计算 +DM 和 -DM
18
+ up_move = df['high'].diff()
19
+ down_move = (-df['low'].diff())
20
+
21
+ df['plus_dm'] = up_move.where((up_move > down_move) & (up_move > 0), 0)
22
+ df['minus_dm'] = down_move.where((down_move > up_move) & (down_move > 0), 0)
23
+
24
+ # 3. Wilders 平滑函数(关键!)
25
+ def wilders_smooth(series, n):
26
+ return series.ewm(alpha=1 / n, adjust=False).mean()
27
+
28
+ df['tr_smooth'] = wilders_smooth(df['tr'], N)
29
+ df['plus_dm_smooth'] = wilders_smooth(df['plus_dm'], N)
30
+ df['minus_dm_smooth'] = wilders_smooth(df['minus_dm'], N)
31
+
32
+ # 4. 计算 +DI 和 -DI
33
+ df['plus_di'] = (df['plus_dm_smooth'] / df['tr_smooth']) * 100
34
+ df['minus_di'] = (df['minus_dm_smooth'] / df['tr_smooth']) * 100
35
+
36
+ # 5. 计算 DX
37
+ di_sum = df['plus_di'] + df['minus_di']
38
+ di_diff = (df['plus_di'] - df['minus_di']).abs()
39
+
40
+ # 防止除零
41
+ df['dx'] = np.where(di_sum > 0, (di_diff / di_sum) * 100, 0)
42
+
43
+ # 6. ADX = DX 的 Wilders 平滑
44
+ df['adx'] = wilders_smooth(df['dx'], N)
45
+
46
+ # 7. 输出到指定列名
47
+ df[factor_name] = df['adx']
48
+
49
+ return df
基础库/通用选币回测框架/因子库/AO.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AO 因子(Awesome Oscillator,动量震荡)— 量纲归一版本
3
+
4
+ 定义
5
+ - 使用中位价 `mid=(high+low)/2` 的短/长均线差作为动量源
6
+ - 先对均线差做比例归一:`(SMA_s - SMA_l) / SMA_l`
7
+ - 再对结果以 `ATR(l)` 做风险归一,得到跨币种可比的无量纲指标
8
+
9
+ 用途
10
+ - 作为前置过滤(方向确认):AO > 0 表示短期动量强于长期动量
11
+ - 也可作为选币因子,但推荐保留主因子(如 VWapBias)主导排名
12
+ """
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+
17
+
18
+ def _parse_param(param):
19
+ """解析参数,支持 (s,l)、"s,l" 两种写法;默认 (5,34)"""
20
+ if isinstance(param, (tuple, list)) and len(param) >= 2:
21
+ return int(param[0]), int(param[1])
22
+ if isinstance(param, str) and "," in param:
23
+ a, b = param.split(",")
24
+ return int(a), int(b)
25
+ return 5, 34
26
+
27
+
28
+ def _atr(df, n):
29
+ """计算 ATR(n):真实波动范围的均值,用于风险归一"""
30
+ prev_close = df["close"].shift(1).fillna(df["open"]) # 前收盘价(首根用开盘补齐)
31
+ # 真实波动 TR = max(高-低, |高-前收|, |低-前收|)
32
+ tr = np.maximum(
33
+ df["high"] - df["low"],
34
+ np.maximum(np.abs(df["high"] - prev_close), np.abs(df["low"] - prev_close)),
35
+ )
36
+ return pd.Series(tr).rolling(n, min_periods=1).mean() # 滚动均值作为 ATR
37
+
38
+
39
+ def signal(*args):
40
+ """计算单参数 AO 因子并写入列名 `factor_name`"""
41
+ df = args[0] # K线数据
42
+ param = args[1] # 参数(支持 "s,l")
43
+ factor_name = args[2] # 因子列名
44
+ s, l = _parse_param(param) # 解析短/长窗口
45
+ eps = 1e-12 # 防除零微量
46
+ mid = (df["high"] + df["low"]) / 2.0 # 中位价
47
+ sma_s = mid.rolling(s, min_periods=1).mean() # 短均线
48
+ sma_l = mid.rolling(l, min_periods=1).mean() # 长均线
49
+ atr_l = _atr(df, l) # 长窗 ATR
50
+ # 比例/ATR 归一:跨币种可比、抗量纲
51
+ ao = ((sma_s - sma_l) / (sma_l + eps)) / (atr_l + eps)
52
+ df[factor_name] = ao.replace([np.inf, -np.inf], 0).fillna(0) # 安全处理
53
+ return df
基础库/通用选币回测框架/因子库/ATR.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ATR 波动率因子(时间序列)
4
+ 基于真实波幅的均值衡量价格波动率
5
+ author: 邢不行框架适配
6
+ """
7
+ def signal(*args):
8
+ df = args[0]
9
+ n = args[1] # ATR计算窗口
10
+ factor_name = args[2]
11
+
12
+ # 1) 计算 True Range (TR)
13
+ df['pre_close'] = df['close'].shift(1) # 前一周期收盘价
14
+ df['tr1'] = df['high'] - df['low']
15
+ df['tr2'] = (df['high'] - df['pre_close']).abs()
16
+ df['tr3'] = (df['low'] - df['pre_close']).abs()
17
+ df['TR'] = df[['tr1', 'tr2', 'tr3']].max(axis=1) # 三者取最大:contentReference[oaicite:1]{index=1}
18
+
19
+ # 2) 计算 ATR:TR 的 n 期滚动均值作为波动率指标
20
+ df[factor_name] = df['TR'].rolling(window=n, min_periods=1).mean()
21
+
22
+ # 3) 清理临时字段
23
+ del df['pre_close'], df['tr1'], df['tr2'], df['tr3'], df['TR']
24
+
25
+ return df
基础库/通用选币回测框架/因子库/ActiveBuyRatio.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ def signal(*args):
3
+ df = args[0]
4
+ n = args[1]
5
+ factor_name = args[2]
6
+
7
+ # 计算主动买入占比因子
8
+ # taker_buy_base_asset_volume: 主动买入的基础资产数量
9
+ # volume: 总交易量(基础资产)
10
+ # 主动买入占比 = 主动买入量 / 总交易量
11
+ df['active_buy_ratio'] = df['taker_buy_base_asset_volume'] / (df['volume'] + 1e-9)
12
+
13
+ # 对占比进行滚动窗口处理(可选)
14
+ # 计算n周期内的平均主动买入占比
15
+ df[factor_name] = df['active_buy_ratio'].rolling(
16
+ window=n,
17
+ min_periods=1
18
+ ).mean()
19
+
20
+ # 清理临时列
21
+ df.drop('active_buy_ratio', axis=1, inplace=True, errors='ignore')
22
+
23
+ return df
基础库/通用选币回测框架/因子库/AdxMinus.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """邢不行™️选币框架
2
+ Python数字货币量化投资课程
3
+
4
+ 版权所有 ©️ 邢不行
5
+ 微信: xbx8662
6
+
7
+ 未经授权,不得复制、修改、或使用本代码的全部或部分内容。仅限个人学习用途,禁止商业用途。
8
+
9
+ Author: 邢不行
10
+ --------------------------------------------------------------------------------
11
+
12
+ # ** 因子文件功能说明 **
13
+ 1. 因子库中的每个 Python 文件需实现 `signal` 函数,用于计算因子值。
14
+ 2. 除 `signal` 外,可根据需求添加辅助函数,不影响因子计算逻辑。
15
+
16
+ # ** signal 函数参数与返回值说明 **
17
+ 1. `signal` 函数的第一个参数为 `candle_df`,用于接收单个币种的 K 线数据。
18
+ 2. `signal` 函数的第二个参数用于因子计算的主要参数,具体用法见函数实现。
19
+ 3. `signal` 函数可以接收其他可选参数,按实际因子计算逻辑使用。
20
+ 4. `signal` 函数的返回值应为包含因子数据的 K 线数据。
21
+
22
+ # ** candle_df 示例 **
23
+ candle_begin_time symbol open high low close volume quote_volume trade_num taker_buy_base_asset_volume taker_buy_quote_asset_volume funding_fee first_candle_time 是否交易
24
+ 0 2023-11-22 1000BONK-USDT 0.004780 0.004825 0.004076 0.004531 12700997933 5.636783e+07 320715 6184933232 2.746734e+07 0.001012 2023-11-22 14:00:00 1
25
+ 1 2023-11-23 1000BONK-USDT 0.004531 0.004858 0.003930 0.004267 18971334686 8.158966e+07 573386 8898242083 3.831782e+07 0.001634 2023-11-22 14:00:00 1
26
+ 2 2023-11-24 1000BONK-USDT 0.004267 0.004335 0.003835 0.004140 17168511399 6.992947e+07 475254 7940993618 3.239266e+07 0.005917 2023-11-22 14:00:00 1
27
+
28
+ # ** signal 参数示例 **
29
+ - 如果策略配置中 `factor_list` 包含 ('AdxMinus', True, 14, 1),则 `param` 为 14,`args[0]` 为 'AdxMinus_14'。
30
+ - 如果策略配置中 `filter_list` 包含 ('AdxMinus', 14, 'pct:<0.8'),则 `param` 为 14,`args[0]` 为 'AdxMinus_14'。
31
+ """
32
+
33
+
34
+ """ADX- (DI-) 下跌趋势强度指标,用于衡量下跌动能的强度"""
35
+ import pandas as pd
36
+ import numpy as np
37
+
38
+
39
+ def signal(candle_df, param, *args):
40
+ """
41
+ 计算ADX- (DI-) 下跌趋势强度指标
42
+ DI-反映价格下跌动能的强度,数值越高表示下跌趋势越强
43
+
44
+ 计算原理:
45
+ 1. 计算真实波幅TR
46
+ 2. 计算下跌方向移动DM-
47
+ 3. 对TR和DM-进行平滑处理
48
+ 4. DI- = (平滑DM- / 平滑TR) * 100
49
+
50
+ :param candle_df: 单个币种的K线数据
51
+ :param param: 计算周期参数,通常为14
52
+ :param args: 其他可选参数,args[0]为因子名称
53
+ :return: 包含因子数据的 K 线数据
54
+ """
55
+ factor_name = args[0] # 从额外参数中获取因子名称
56
+ n = param # 计算周期
57
+
58
+ # 步骤1: 计算真实波幅TR (True Range)
59
+ # TR衡量价格的真实波动幅度,考虑跳空因素
60
+ tr1 = candle_df['high'] - candle_df['low'] # 当日最高价-最低价
61
+ tr2 = abs(candle_df['high'] - candle_df['close'].shift(1)) # 当日最高价-前日收盘价的绝对值
62
+ tr3 = abs(candle_df['low'] - candle_df['close'].shift(1)) # 当日最低价-前日收盘价的绝对值
63
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
64
+
65
+ # 步骤2: 计算下跌方向移动DM- (Directional Movement Minus)
66
+ # DM-衡量价格向下突破的动能
67
+ high_diff = candle_df['high'] - candle_df['high'].shift(1) # 最高价变化
68
+ low_diff = candle_df['low'].shift(1) - candle_df['low'] # 最低价变化(前日-当日,正值表示下跌)
69
+
70
+ # 只有当下跌幅度大于上涨幅度且确实下跌时,才记录为负向动能
71
+ dm_minus = np.where((low_diff > high_diff) & (low_diff > 0), low_diff, 0)
72
+
73
+ # 步骤3: 使用Wilder's平滑方法计算平滑TR和DM-
74
+ # Wilder's平滑:类似指数移动平均,但平滑系数为1/n
75
+ tr_smooth = tr.ewm(alpha=1/n, adjust=False).mean()
76
+ dm_minus_smooth = pd.Series(dm_minus).ewm(alpha=1/n, adjust=False).mean()
77
+
78
+ # 步骤4: 计算DI- (Directional Indicator Minus)
79
+ # DI-表示下跌趋势的相对强度,范围0-100
80
+ di_minus = (dm_minus_smooth / tr_smooth) * 100
81
+
82
+ # 将计算结果赋值给因子列
83
+ candle_df[factor_name] = di_minus
84
+
85
+ return candle_df
86
+
87
+
88
+ # 使用说明:
89
+ # 1. DI-数值含义:
90
+ # - DI- > 25: 下跌趋势较强
91
+ # - DI- > 40: 强下跌趋势
92
+ # - DI- < 20: 下跌动能较弱
93
+ #
94
+ # 2. 交易信号参考:
95
+ # - DI-持续上升:下跌趋势加强,可考虑做空或避险
96
+ # - DI-开始下降:下跌动能减弱,可能见底反弹
97
+ # - DI-与DI+交叉:趋势可能转换
98
+ #
99
+ # 3. 风险管理应用:
100
+ # - DI-快速上升:及时止损,避免深度套牢
101
+ # - DI-高位钝化:下跌动能衰竭,关注反弹机会
102
+ # - DI-与价格背离:可能出现趋���反转信号
103
+ #
104
+ # 4. 最佳实践:
105
+ # - 结合ADX使用:ADX>25时DI-信号更可靠
106
+ # - 结合成交量:放量下跌时DI-信号更强
107
+ # - 关注支撑位:在重要支撑位DI-减弱可能是买入机会
108
+ #
109
+ # 5. 在config.py中的配置示例:
110
+ # factor_list = [
111
+ # ('AdxMinus', True, 14, 1), # 14周期DI-
112
+ # ('AdxMinus', True, 21, 1), # 21周期DI-(更平滑)
113
+ # ]
114
+ #
115
+ # filter_list = [
116
+ # ('AdxMinus', 14, 'pct:<0.3'), # 过滤掉DI-排名前30%的币种(避开强下跌趋势)
117
+ # ]
118
+ #
119
+ # 6. 与其他指标组合:
120
+ # - DI- + RSI: DI-高位+RSI超卖可能是反弹信号
121
+ # - DI- + 布林带: DI-上升+价格触及下轨可能是超跌
122
+ # - DI- + MACD: DI-与MACD背离可能预示趋势转换
基础库/通用选币回测框架/因子库/AdxPlus.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """邢不行™️选币框架
2
+ Python数字货币量化投资课程
3
+
4
+ 版权所有 ©️ 邢不行
5
+ 微信: xbx8662
6
+
7
+ 未经授权,不得复制、修改、或使用本代码的全部或部分内容。仅限个人学习用途,禁止商业用途。
8
+
9
+ Author: 邢不行
10
+ --------------------------------------------------------------------------------
11
+
12
+ # ** 因子文件功能说明 **
13
+ 1. 因子库中的每个 Python 文件需实现 `signal` 函数,用于计算因子值。
14
+ 2. 除 `signal` 外,可根据需求添加辅助函数,不影响因子计算逻辑。
15
+
16
+ # ** signal 函数参数与返回值说明 **
17
+ 1. `signal` 函数的第一个参数为 `candle_df`,用于接收单个币种的 K 线数据。
18
+ 2. `signal` 函数的第二个参数用于因子计算的主要参数,具体用法见函数实现。
19
+ 3. `signal` 函数可以接收其他可选参数,按实际因子计算逻辑使用。
20
+ 4. `signal` 函数的返回值应为包含因子数据的 K 线数据。
21
+
22
+ # ** candle_df 示例 **
23
+ candle_begin_time symbol open high low close volume quote_volume trade_num taker_buy_base_asset_volume taker_buy_quote_asset_volume funding_fee first_candle_time 是否交易
24
+ 0 2023-11-22 1000BONK-USDT 0.004780 0.004825 0.004076 0.004531 12700997933 5.636783e+07 320715 6184933232 2.746734e+07 0.001012 2023-11-22 14:00:00 1
25
+ 1 2023-11-23 1000BONK-USDT 0.004531 0.004858 0.003930 0.004267 18971334686 8.158966e+07 573386 8898242083 3.831782e+07 0.001634 2023-11-22 14:00:00 1
26
+ 2 2023-11-24 1000BONK-USDT 0.004267 0.004335 0.003835 0.004140 17168511399 6.992947e+07 475254 7940993618 3.239266e+07 0.005917 2023-11-22 14:00:00 1
27
+
28
+ # ** signal 参数示例 **
29
+ - 如果策略配置中 `factor_list` 包含 ('AdxPlus', True, 14, 1),则 `param` 为 14,`args[0]` 为 'AdxPlus_14'。
30
+ - 如果策略配置中 `filter_list` 包含 ('AdxPlus', 14, 'pct:<0.8'),则 `param` 为 14,`args[0]` 为 'AdxPlus_14'。
31
+ """
32
+
33
+
34
+ """ADX+ (DI+) 上涨趋势强度指标,用于衡量上涨动能的强度"""
35
+ import pandas as pd
36
+ import numpy as np
37
+
38
+
39
+ def signal(candle_df, param, *args):
40
+ """
41
+ 计算ADX+ (DI+) 上涨趋势强度指标
42
+ DI+反映价格上涨动能的强度,数值越高表示上涨趋势越强
43
+
44
+ 计算原理:
45
+ 1. 计算真实波幅TR
46
+ 2. 计算上涨方向移动DM+
47
+ 3. 对TR和DM+进行平滑处理
48
+ 4. DI+ = (平滑DM+ / 平滑TR) * 100
49
+
50
+ :param candle_df: 单个币种的K线数据
51
+ :param param: 计算周期参数,通常为14
52
+ :param args: 其他可选参数,args[0]为因子名称
53
+ :return: 包含因子数据的 K 线数据
54
+ """
55
+ factor_name = args[0] # 从额外参数中获取因子名称
56
+ n = param # 计算周期
57
+
58
+ # 步骤1: 计算真实波幅TR (True Range)
59
+ # TR衡量价格的真实波动幅度,考虑跳空因素
60
+ tr1 = candle_df['high'] - candle_df['low'] # 当日最高价-最低价
61
+ tr2 = abs(candle_df['high'] - candle_df['close'].shift(1)) # 当日最高价-前日收盘价的绝对值
62
+ tr3 = abs(candle_df['low'] - candle_df['close'].shift(1)) # 当日最低价-前日收盘价的绝对值
63
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
64
+
65
+ # 步骤2: 计算上涨方向移动DM+ (Directional Movement Plus)
66
+ # DM+衡量价格向上突破的动能
67
+ high_diff = candle_df['high'] - candle_df['high'].shift(1) # 最高价变化
68
+ low_diff = candle_df['low'].shift(1) - candle_df['low'] # 最低价变化(前日-当日)
69
+
70
+ # 只有当上涨幅度大于下跌幅度且确实上涨时,才记录为正向动能
71
+ dm_plus = np.where((high_diff > low_diff) & (high_diff > 0), high_diff, 0)
72
+
73
+ # 步骤3: 使用Wilder's平滑方法计算平滑TR和DM+
74
+ # Wilder's平滑:类似指数移动平均,但平滑系数为1/n
75
+ tr_smooth = tr.ewm(alpha=1/n, adjust=False).mean()
76
+ dm_plus_smooth = pd.Series(dm_plus).ewm(alpha=1/n, adjust=False).mean()
77
+
78
+ # 步骤4: 计算DI+ (Directional Indicator Plus)
79
+ # DI+表示上涨趋势的相对强度,范围0-100
80
+ di_plus = (dm_plus_smooth / tr_smooth) * 100
81
+
82
+ # 将计算结果赋值给因子列
83
+ candle_df[factor_name] = di_plus
84
+
85
+ return candle_df
86
+
87
+
88
+ # 使用说明:
89
+ # 1. DI+数值含义:
90
+ # - DI+ > 25: 上涨趋势较强
91
+ # - DI+ > 40: 强上涨趋势
92
+ # - DI+ < 20: 上涨动能较弱
93
+ #
94
+ # 2. 交易信号参考:
95
+ # - DI+持续上升:上涨趋势加强,可考虑做多
96
+ # - DI+开始下降:上涨动能减弱,注意风险
97
+ # - DI+与DI-交叉:趋势可能转换
98
+ #
99
+ # 3. 最佳实践:
100
+ # - 结合ADX使用:ADX>25时DI+信号更可靠
101
+ # - 结合价格形态:在支撑位附近DI+上升信号更强
102
+ # - 避免震荡市:横盘整理时DI+信号容易失效
103
+ #
104
+ # 4. 在config.py中的配置示���:
105
+ # factor_list = [
106
+ # ('AdxPlus', True, 14, 1), # 14周期DI+
107
+ # ('AdxPlus', True, 21, 1), # 21周期DI+(更平滑)
108
+ # ]
109
+ #
110
+ # filter_list = [
111
+ # ('AdxPlus', 14, 'pct:>0.7'), # 选择DI+排名前30%的币种
112
+ # ]
基础库/通用选币回测框架/因子库/AdxVolWaves.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from .TMV import calculate_adx
4
+
5
+
6
+ def signal(candle_df, param, *args):
7
+ factor_name = args[0] if args else "AdxVolWaves"
8
+
9
+ if isinstance(param, tuple):
10
+ bb_length = param[0] if len(param) > 0 else 20
11
+ bb_mult = param[1] if len(param) > 1 else 1.5
12
+ adx_length = param[2] if len(param) > 2 else 14
13
+ adx_influence = param[3] if len(param) > 3 else 0.8
14
+ zone_offset = param[4] if len(param) > 4 else 1.0
15
+ zone_expansion = param[5] if len(param) > 5 else 1.0
16
+ smooth_length = param[6] if len(param) > 6 else 50
17
+ signal_cooldown = param[7] if len(param) > 7 else 20
18
+ else:
19
+ bb_length = int(param)
20
+ bb_mult = 1.5
21
+ adx_length = 14
22
+ adx_influence = 0.8
23
+ zone_offset = 1.0
24
+ zone_expansion = 1.0
25
+ smooth_length = 50
26
+ signal_cooldown = 20
27
+
28
+ close = candle_df["close"]
29
+ high = candle_df["high"]
30
+ low = candle_df["low"]
31
+
32
+ adx_df = calculate_adx(candle_df, adx_length)
33
+ adx = adx_df["adx"]
34
+ di_plus = adx_df["di_plus"]
35
+ di_minus = adx_df["di_minus"]
36
+ adx_normalized = adx / 100.0
37
+
38
+ bb_basis = close.rolling(window=bb_length, min_periods=1).mean()
39
+ bb_dev = close.rolling(window=bb_length, min_periods=1).std()
40
+
41
+ adx_multiplier = 1.0 + adx_normalized * adx_influence
42
+ bb_dev_adjusted = bb_mult * bb_dev * adx_multiplier
43
+
44
+ bb_upper = bb_basis + bb_dev_adjusted
45
+ bb_lower = bb_basis - bb_dev_adjusted
46
+
47
+ bb_basis_safe = bb_basis.replace(0, np.nan)
48
+ bb_width = (bb_upper - bb_lower) / bb_basis_safe * 100.0
49
+ bb_width = bb_width.replace([np.inf, -np.inf], np.nan).fillna(0.0)
50
+
51
+ bb_upper_smooth = bb_upper.rolling(window=smooth_length, min_periods=1).mean()
52
+ bb_lower_smooth = bb_lower.rolling(window=smooth_length, min_periods=1).mean()
53
+ bb_range_smooth = bb_upper_smooth - bb_lower_smooth
54
+
55
+ offset_distance = bb_range_smooth * zone_offset
56
+
57
+ top_zone_bottom = bb_upper_smooth + offset_distance
58
+ top_zone_top = top_zone_bottom + bb_range_smooth * zone_expansion
59
+
60
+ bottom_zone_top = bb_lower_smooth - offset_distance
61
+ bottom_zone_bottom = bottom_zone_top - bb_range_smooth * zone_expansion
62
+
63
+ price_in_top_zone = close > top_zone_bottom
64
+ price_in_bottom_zone = close < bottom_zone_top
65
+
66
+ bb_width_ma = bb_width.rolling(window=50, min_periods=1).mean()
67
+ is_squeeze = bb_width < bb_width_ma
68
+
69
+ price_in_top_zone_prev = price_in_top_zone.shift(1).fillna(False)
70
+ price_in_bottom_zone_prev = price_in_bottom_zone.shift(1).fillna(False)
71
+
72
+ n = len(candle_df)
73
+ enter_top = np.zeros(n, dtype=bool)
74
+ enter_bottom = np.zeros(n, dtype=bool)
75
+
76
+ top_vals = price_in_top_zone.to_numpy()
77
+ bottom_vals = price_in_bottom_zone.to_numpy()
78
+ top_prev_vals = price_in_top_zone_prev.to_numpy()
79
+ bottom_prev_vals = price_in_bottom_zone_prev.to_numpy()
80
+
81
+ last_buy_bar = -10**9
82
+ last_sell_bar = -10**9
83
+
84
+ for i in range(n):
85
+ if top_vals[i] and (not top_prev_vals[i]) and (i - last_sell_bar >= signal_cooldown):
86
+ enter_top[i] = True
87
+ last_sell_bar = i
88
+ if bottom_vals[i] and (not bottom_prev_vals[i]) and (i - last_buy_bar >= signal_cooldown):
89
+ enter_bottom[i] = True
90
+ last_buy_bar = i
91
+
92
+ factor_main = np.where(enter_bottom, 1.0, 0.0)
93
+ factor_main = np.where(enter_top, -1.0, factor_main)
94
+
95
+ candle_df[factor_name] = factor_main
96
+ candle_df[f"{factor_name}_ADX"] = adx
97
+ candle_df[f"{factor_name}_DI_Plus"] = di_plus
98
+ candle_df[f"{factor_name}_DI_Minus"] = di_minus
99
+ candle_df[f"{factor_name}_BB_Upper"] = bb_upper
100
+ candle_df[f"{factor_name}_BB_Lower"] = bb_lower
101
+ candle_df[f"{factor_name}_TopZoneBottom"] = top_zone_bottom
102
+ candle_df[f"{factor_name}_BottomZoneTop"] = bottom_zone_top
103
+ candle_df[f"{factor_name}_Squeeze"] = is_squeeze.astype(int)
104
+ candle_df[f"{factor_name}_EnterTop"] = enter_top.astype(int)
105
+ candle_df[f"{factor_name}_EnterBottom"] = enter_bottom.astype(int)
106
+
107
+ return candle_df
108
+