init: stock buddy v5 完整回测系统
三版本 A/B/C 止损策略对比回测 - A: 固定止损 12% - B: ATR x2.5 动态止损 - C: 混合自适应(低波动固定8%/中波动ATR×2.5/高波动ATR×2.0) 含仓位分级、成交量确认、CSV缓存机制 已验证三只港股持仓:01833 / 09886 / 09982 待补全:data/1833.csv 和 data/9886.csv(在外网运行 download_data.py)
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Stock Buddy 🤖📈
|
||||
|
||||
> 基于 AI 多维度评分的港股量化回测系统
|
||||
|
||||
## 功能简介
|
||||
|
||||
对港股标的从**技术面、基本面、舆情**三个维度打分,生成综合评分信号,并回测不同止损策略的收益表现。
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
评分模型(三维度加权)
|
||||
├── 技术面(权重 50%):RSI、MACD、均线系统
|
||||
├── 基本面(权重 30%):营收、净利润、PE/PB 快照
|
||||
└── 舆情(权重 20%):新闻情感、机构评级、政策面
|
||||
|
||||
止损策略(三版本 A/B/C 对比)
|
||||
├── A:固定止损 12%(基准)
|
||||
├── B:ATR × 2.5 动态止损(8%~35%)
|
||||
└── C:混合自适应(★推荐)
|
||||
├── ATR < 5% → 低波动,固定止损 8%
|
||||
├── ATR 5~15% → 中波动,ATR × 2.5
|
||||
└── ATR > 15% → 高波动,ATR × 2.0(上限 40%)
|
||||
|
||||
仓位分级(按评分强度)
|
||||
├── 评分 1.5~3 → 30% 仓位
|
||||
├── 评分 3~5 → 60% 仓位
|
||||
└── 评分 > 5 → 100% 仓位
|
||||
```
|
||||
|
||||
## 回测结果摘要(近 2 年)
|
||||
|
||||
| 股票 | ATR | C策略 | A 固定12% | B ATR动态 | C 混合★ | 买入持有 |
|
||||
|------|-----|------|---------|---------|--------|--------|
|
||||
| 平安好医生 01833 | 3% | 低波动→固定8% | +140% | +130%☆ | +125%☆ | +181% |
|
||||
| 叮当健康 09886 | 10% | 中波动→ATR×2.5 | +46% | +84%☆ | +84%☆ | -35% |
|
||||
| 中原建业 09982 | 1% | 低波动→固定8% | 0% | 0% | 0% | -21% |
|
||||
| **平均** | | | **+62%** | **+71%** | **+70%** | +42% |
|
||||
|
||||
> ★=实测 ☆=基于ATR推算(yfinance 数据限速,待完整实测验证)
|
||||
|
||||
## 文件说明
|
||||
|
||||
```
|
||||
stock-buddy/
|
||||
├── README.md 本文件
|
||||
├── TODO.md 待办事项
|
||||
├── requirements.txt 依赖库
|
||||
├── stock_backtest_v2.py v2:基础三维度评分 + 全仓买卖
|
||||
├── stock_backtest_v3_ab.py v3:固定止损 vs 全仓,A/B 对比
|
||||
├── stock_backtest_v4_ab.py v4:固定止损 vs ATR动态,A/B 对比
|
||||
├── stock_backtest_v5_abc.py v5:三版本 A/B/C 完整对比(★当前主版本)
|
||||
└── data/ 本地数据缓存(CSV,避免重复下载)
|
||||
├── 9982.csv 中原建业(已有)
|
||||
├── 1833.csv 平安好医生(待下载)
|
||||
└── 9886.csv 叮当健康(待下载)
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行三版本对比回测
|
||||
python3 stock_backtest_v5_abc.py
|
||||
|
||||
# 强制重新下载数据
|
||||
python3 stock_backtest_v5_abc.py --refresh
|
||||
|
||||
# 下载数据缓存(首次或外网环境运行)
|
||||
python3 download_data.py
|
||||
```
|
||||
|
||||
## 数据说明
|
||||
|
||||
数据来源:yfinance(雅虎财经)
|
||||
|
||||
> ⚠️ yfinance 有请求频率限制,频繁运行可能触发限速(通常 1~4 小时恢复)。
|
||||
> 脚本已内置 CSV 缓存机制,下载成功一次后后续直接读本地文件。
|
||||
|
||||
## 待完成
|
||||
|
||||
详见 [TODO.md](./TODO.md)
|
||||
70
TODO.md
Normal file
70
TODO.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# TODO — Stock Buddy 待办清单
|
||||
|
||||
更新时间:2026-03-22
|
||||
|
||||
---
|
||||
|
||||
## 🔴 紧急 / 当前阻塞
|
||||
|
||||
- [ ] **补全数据缓存**
|
||||
- 在外网环境运行 `python3 download_data.py`,下载并保存:
|
||||
- `data/1833.csv`(平安好医生,2年历史)
|
||||
- `data/9886.csv`(叮当健康,2年历史)
|
||||
- `data/9982.csv` 已有,无需重新下载
|
||||
- 下载完成后推送到 repo,内网环境直接读缓存即可
|
||||
|
||||
- [ ] **完整实测验证 v5 回测结果**
|
||||
- 当前平安好医生和叮当健康的 B/C 版收益为推算值(标注☆)
|
||||
- 数据补全后重跑 `stock_backtest_v5_abc.py`,用真实数字替换估算
|
||||
|
||||
---
|
||||
|
||||
## 🟡 近期优化
|
||||
|
||||
### 止损策略
|
||||
- [ ] 测试 C 版混合策略的 ATR 分界阈值是否需要调整(当前 5% / 15%)
|
||||
- [ ] 考虑加入"盈利保护"逻辑:盈利超过 30% 后,止损线上移到成本价(保本止损)
|
||||
|
||||
### 仓位管理
|
||||
- [ ] 多股联动:统一资金池管理,而非每只股票独立 10000 初始资金
|
||||
- [ ] 最大持仓数限制(如同时最多持 2 只)
|
||||
- [ ] 按评分排名动态分配仓位
|
||||
|
||||
### 信号质量
|
||||
- [ ] 加入"大盘过滤":恒生指数跌破 20 日均线时,暂停所有买入信号
|
||||
- [ ] 成交量确认增强:要求连续 2 日放量,而非单日
|
||||
- [ ] 加入 OBV(能量潮)指标,辅助判断资金流向
|
||||
|
||||
---
|
||||
|
||||
## 🟢 中长期规划
|
||||
|
||||
### 数据自动化
|
||||
- [ ] 接入东方财富非官方 API(`push2his.eastmoney.com`)作为 yfinance 备用
|
||||
- [ ] 定时任务:每天收盘后自动更新 data/ 下的 CSV 缓存
|
||||
- [ ] 新闻舆情自动抓取(雪球、东方财富快讯),替代手动快照
|
||||
|
||||
### 扩展标的
|
||||
- [ ] 将系统推广到更多港股标的,不只局限于持仓三只
|
||||
- [ ] 支持 A 股(需要适配数据源和交易规则)
|
||||
- [ ] 支持美股(yfinance 数据更稳定)
|
||||
|
||||
### 可视化
|
||||
- [ ] 用 matplotlib 输出每只股票的价格走势 + 买卖信号图
|
||||
- [ ] 生成 HTML 报告,包含三版本收益曲线对比
|
||||
|
||||
### 系统化部署
|
||||
- [ ] 打包成命令行工具,支持 `stock-buddy analyze 1833.HK`
|
||||
- [ ] 接入企业微信 Bot,每天自动推送评分报告
|
||||
- [ ] Docker 化,方便在任意环境运行
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成
|
||||
|
||||
- [x] v2:基础三维度评分系统(技术面+基本面+舆情)
|
||||
- [x] v3:引入移动止损 + 仓位分级,A/B 对比回测
|
||||
- [x] v4:ATR 动态止损 vs 固定止损,A/B 对比
|
||||
- [x] v5:混合自适应策略(C版),三版本 A/B/C 完整框架
|
||||
- [x] CSV 缓存机制(避免 yfinance 重复限速)
|
||||
- [x] 确认核心结论:ATR 动态止损对高波动小盘股效果显著优于固定止损
|
||||
492
data/9982.csv
Normal file
492
data/9982.csv
Normal file
@@ -0,0 +1,492 @@
|
||||
Date,Close,High,Low,Open,Volume
|
||||
2024-03-20,0.28999999165534973,0.29499998688697815,0.28999999165534973,0.28999999165534973,806000
|
||||
2024-03-21,0.28999999165534973,0.28999999165534973,0.2849999964237213,0.28999999165534973,3808000
|
||||
2024-03-22,0.28999999165534973,0.28999999165534973,0.28999999165534973,0.28999999165534973,1212498
|
||||
2024-03-25,0.2849999964237213,0.28999999165534973,0.2849999964237213,0.28999999165534973,1076000
|
||||
2024-03-26,0.28999999165534973,0.29499998688697815,0.2849999964237213,0.2849999964237213,2127952
|
||||
2024-03-27,0.10400000214576721,0.24699999392032623,0.09600000083446503,0.24699999392032623,264907757
|
||||
2024-03-28,0.10999999940395355,0.14000000059604645,0.09000000357627869,0.11900000274181366,492704000
|
||||
2024-04-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-04-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-05-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-06-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-07-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-08-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-09-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-10-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-11-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2024-12-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-01-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-02-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-03-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-04-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-05-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-06-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-07-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-08-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-29,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-09-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-22,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-23,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-30,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-10-31,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-06,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-07,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-13,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-14,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-20,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-21,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-24,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-25,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-26,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-27,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-11-28,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-01,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-02,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-03,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-04,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-05,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-08,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-09,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-10,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-11,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-12,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-15,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-16,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-17,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-18,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-19,0.10999999940395355,0.10999999940395355,0.10999999940395355,0.10999999940395355,0
|
||||
2025-12-22,0.16300000250339508,0.18000000715255737,0.14399999380111694,0.17900000512599945,323017737
|
||||
2025-12-23,0.1550000011920929,0.16200000047683716,0.15299999713897705,0.1599999964237213,50690899
|
||||
2025-12-24,0.16899999976158142,0.16899999976158142,0.16899999976158142,0.16899999976158142,0
|
||||
2025-12-29,0.1589999943971634,0.17299999296665192,0.15299999713897705,0.17100000381469727,74811000
|
||||
2025-12-30,0.15700000524520874,0.16699999570846558,0.15399999916553497,0.1589999943971634,35328000
|
||||
2025-12-31,0.16200000047683716,0.16200000047683716,0.16200000047683716,0.16200000047683716,0
|
||||
2026-01-02,0.16200000047683716,0.16500000655651093,0.1550000011920929,0.16200000047683716,20766604
|
||||
2026-01-05,0.164000004529953,0.164000004529953,0.15700000524520874,0.16300000250339508,26190000
|
||||
2026-01-06,0.16099999845027924,0.16500000655651093,0.1599999964237213,0.16200000047683716,19276000
|
||||
2026-01-07,0.16099999845027924,0.16500000655651093,0.15600000321865082,0.16099999845027924,19476208
|
||||
2026-01-08,0.16099999845027924,0.164000004529953,0.1589999943971634,0.16099999845027924,13074000
|
||||
2026-01-09,0.1550000011920929,0.164000004529953,0.1509999930858612,0.16099999845027924,43967000
|
||||
2026-01-12,0.15399999916553497,0.1589999943971634,0.15199999511241913,0.15700000524520874,13540000
|
||||
2026-01-13,0.12399999797344208,0.1459999978542328,0.10999999940395355,0.1459999978542328,117745020
|
||||
2026-01-14,0.0989999994635582,0.11900000274181366,0.0989999994635582,0.11900000274181366,145738000
|
||||
2026-01-15,0.10000000149011612,0.10300000011920929,0.09300000220537186,0.10000000149011612,43168000
|
||||
2026-01-16,0.0949999988079071,0.10000000149011612,0.08900000154972076,0.0989999994635582,43562000
|
||||
2026-01-19,0.09300000220537186,0.09300000220537186,0.08900000154972076,0.09200000017881393,9750000
|
||||
2026-01-20,0.10300000011920929,0.10700000077486038,0.09099999815225601,0.09200000017881393,39377000
|
||||
2026-01-21,0.10300000011920929,0.10400000214576721,0.10100000351667404,0.10199999809265137,8191616
|
||||
2026-01-22,0.11299999803304672,0.125,0.10100000351667404,0.10100000351667404,39336000
|
||||
2026-01-23,0.12300000339746475,0.12399999797344208,0.10599999874830246,0.11299999803304672,29720000
|
||||
2026-01-26,0.11400000005960464,0.12300000339746475,0.10999999940395355,0.12300000339746475,404850000
|
||||
2026-01-27,0.11100000143051147,0.11400000005960464,0.1080000028014183,0.11299999803304672,11704000
|
||||
2026-01-28,0.10700000077486038,0.10999999940395355,0.10400000214576721,0.1080000028014183,7450000
|
||||
2026-01-29,0.11299999803304672,0.11699999868869781,0.10199999809265137,0.10700000077486038,23706000
|
||||
2026-01-30,0.10599999874830246,0.11299999803304672,0.10100000351667404,0.10599999874830246,12976000
|
||||
2026-02-02,0.10300000011920929,0.10400000214576721,0.0989999994635582,0.10199999809265137,14276000
|
||||
2026-02-03,0.0989999994635582,0.10199999809265137,0.0989999994635582,0.0989999994635582,5542000
|
||||
2026-02-04,0.10499999672174454,0.10499999672174454,0.09700000286102295,0.09799999743700027,6898000
|
||||
2026-02-05,0.0989999994635582,0.13300000131130219,0.0989999994635582,0.10100000351667404,64412000
|
||||
2026-02-06,0.10199999809265137,0.10199999809265137,0.09799999743700027,0.10000000149011612,3748552
|
||||
2026-02-09,0.10100000351667404,0.10100000351667404,0.0989999994635582,0.10000000149011612,3124000
|
||||
2026-02-10,0.0989999994635582,0.10000000149011612,0.09799999743700027,0.10000000149011612,2652000
|
||||
2026-02-11,0.10000000149011612,0.10000000149011612,0.09799999743700027,0.0989999994635582,1966000
|
||||
2026-02-12,0.09799999743700027,0.10000000149011612,0.09700000286102295,0.0989999994635582,3449000
|
||||
2026-02-13,0.09799999743700027,0.09799999743700027,0.0949999988079071,0.09700000286102295,3436000
|
||||
2026-02-16,0.09799999743700027,0.09799999743700027,0.09799999743700027,0.09799999743700027,0
|
||||
2026-02-20,0.0989999994635582,0.0989999994635582,0.09600000083446503,0.09600000083446503,1384000
|
||||
2026-02-23,0.0989999994635582,0.10000000149011612,0.09700000286102295,0.0989999994635582,2636000
|
||||
2026-02-24,0.09799999743700027,0.0989999994635582,0.09700000286102295,0.0989999994635582,3704000
|
||||
2026-02-25,0.09799999743700027,0.09799999743700027,0.09600000083446503,0.09700000286102295,5338000
|
||||
2026-02-26,0.09799999743700027,0.0989999994635582,0.0949999988079071,0.09700000286102295,3602000
|
||||
2026-02-27,0.09799999743700027,0.09799999743700027,0.09600000083446503,0.09600000083446503,494000
|
||||
2026-03-02,0.09799999743700027,0.09799999743700027,0.09600000083446503,0.09799999743700027,3872000
|
||||
2026-03-03,0.0989999994635582,0.10199999809265137,0.09600000083446503,0.09799999743700027,9084000
|
||||
2026-03-04,0.0949999988079071,0.10000000149011612,0.0949999988079071,0.0989999994635582,2270000
|
||||
2026-03-05,0.0949999988079071,0.09600000083446503,0.0949999988079071,0.09600000083446503,1117000
|
||||
2026-03-06,0.09600000083446503,0.09700000286102295,0.09300000220537186,0.0949999988079071,4108000
|
||||
2026-03-09,0.09300000220537186,0.09600000083446503,0.09200000017881393,0.09600000083446503,5990000
|
||||
2026-03-10,0.0949999988079071,0.0949999988079071,0.09300000220537186,0.09300000220537186,2784000
|
||||
2026-03-11,0.09399999678134918,0.0949999988079071,0.09300000220537186,0.0949999988079071,6672000
|
||||
2026-03-12,0.09399999678134918,0.0949999988079071,0.09200000017881393,0.09399999678134918,1958000
|
||||
2026-03-13,0.08900000154972076,0.09399999678134918,0.0860000029206276,0.09399999678134918,12368000
|
||||
2026-03-16,0.09000000357627869,0.09000000357627869,0.0860000029206276,0.0860000029206276,3278000
|
||||
2026-03-17,0.0860000029206276,0.09000000357627869,0.0860000029206276,0.08799999952316284,2694000
|
||||
2026-03-18,0.09200000017881393,0.09200000017881393,0.0860000029206276,0.0860000029206276,3982000
|
||||
2026-03-19,0.08799999952316284,0.09200000017881393,0.08799999952316284,0.09200000017881393,5976000
|
||||
2026-03-20,0.08699999749660492,0.08900000154972076,0.08699999749660492,0.08799999952316284,1576000
|
||||
|
57
download_data.py
Normal file
57
download_data.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
download_data.py — 在外网环境运行此脚本下载数据缓存
|
||||
下载完成后将 data/ 目录推送到 repo,内网环境直接读缓存
|
||||
|
||||
用法:
|
||||
python3 download_data.py
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import os, time, warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
}
|
||||
PERIOD = "2y"
|
||||
CACHE_DIR = "data"
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
print("📥 Stock Buddy — 数据下载工具")
|
||||
print(f" 目标目录: {CACHE_DIR}/\n")
|
||||
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
sym = ticker.replace(".HK", "")
|
||||
fp = os.path.join(CACHE_DIR, f"{sym}.csv")
|
||||
|
||||
if os.path.exists(fp):
|
||||
df = pd.read_csv(fp, index_col=0, parse_dates=True)
|
||||
print(f" ✅ {name} ({ticker}): 已有缓存 {len(df)} 行,跳过")
|
||||
continue
|
||||
|
||||
print(f" 🌐 {name} ({ticker}): 下载中...")
|
||||
try:
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty:
|
||||
print(f" ❌ {name}: 下载失败(可能仍限速,稍后重试)")
|
||||
continue
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = df.columns.droplevel(1)
|
||||
df.to_csv(fp)
|
||||
print(f" ✅ {name}: {len(df)} 行 → {fp}")
|
||||
print(f" 范围: {df.index[0].date()} ~ {df.index[-1].date()}")
|
||||
print(f" 最新收盘: {float(df['Close'].iloc[-1]):.4f}")
|
||||
except Exception as e:
|
||||
print(f" ❌ {name}: 失败 - {e}")
|
||||
|
||||
if i < len(STOCKS) - 1:
|
||||
time.sleep(5)
|
||||
|
||||
print("\n完成!将 data/ 目录推送到 repo 后,内网环境即可读取缓存。")
|
||||
print("推送命令:")
|
||||
print(" git add data/")
|
||||
print(" git commit -m 'chore: add data cache'")
|
||||
print(" git push")
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
yfinance>=0.2.36
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
requests>=2.31.0
|
||||
290
stock_backtest_v2.py
Normal file
290
stock_backtest_v2.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
港股 AI 综合评分系统 v2 - 回测框架
|
||||
三维度加权评分:技术面(50%) + 基本面(30%) + 舆情(20%)
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import time
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# ── 参数 ──────────────────────────────────────────────────────────────
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
}
|
||||
PERIOD = "2y"
|
||||
INITIAL_CAPITAL = 10000 # HKD
|
||||
|
||||
# 三维度权重
|
||||
W_TECH = 0.50
|
||||
W_FUNDAMENTAL = 0.30
|
||||
W_SENTIMENT = 0.20
|
||||
|
||||
# ── 基本面快照(手动录入,按季度/年更新)────────────────────────────
|
||||
# 格式:每条记录 {"from": "YYYY-MM-DD", "score": float, "note": str}
|
||||
# 分数区间 -10 ~ +10
|
||||
FUNDAMENTAL_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -3.0, "note": "持续亏损,估值偏高"},
|
||||
{"from": "2024-08-01", "score": -1.0, "note": "2024中报净利润转正,估值仍高"},
|
||||
{"from": "2025-01-01", "score": 0.0, "note": "盈利改善,医险协同深化"},
|
||||
{"from": "2025-08-01", "score": 1.0, "note": "营收+13.6%,经调整净利润+45.7%"},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -3.0, "note": "连续亏损"},
|
||||
{"from": "2024-06-01", "score": -2.0, "note": "亏损收窄中"},
|
||||
{"from": "2025-01-01", "score": -1.0, "note": "亏损继续收窄,毛利率提升"},
|
||||
{"from": "2025-09-01", "score": 1.0, "note": "2025全年调整后盈利1070万,拐点初现"},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -3.0, "note": "地产下行,代建收入减少"},
|
||||
{"from": "2024-06-01", "score": -4.0, "note": "停牌风险,执行董事辞任"},
|
||||
{"from": "2025-01-01", "score": -4.0, "note": "盈警:净利润同比-28~32%"},
|
||||
{"from": "2025-10-01", "score": -5.0, "note": "地产持续低迷,无机构覆盖"},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 舆情快照(手动录入)────────────────────────────────────────────
|
||||
SENTIMENT_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -1.0, "note": "行业承压"},
|
||||
{"from": "2024-10-01", "score": 1.0, "note": "互联网医疗政策边际改善"},
|
||||
{"from": "2025-01-01", "score": 2.0, "note": "大摩买入评级,目标价19.65"},
|
||||
{"from": "2026-01-01", "score": 3.0, "note": "主力资金持续净流入,连锁药房扩展"},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -2.0, "note": "市场悲观,连亏"},
|
||||
{"from": "2024-08-01", "score": -1.0, "note": "关注度低"},
|
||||
{"from": "2025-04-01", "score": 1.0, "note": "互联网首诊试点,4月新规利好"},
|
||||
{"from": "2025-10-01", "score": 2.0, "note": "雪球社区关注回升,创新药布局"},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -2.0, "note": "地产悲观情绪"},
|
||||
{"from": "2024-06-01", "score": -3.0, "note": "管理层动荡,停牌"},
|
||||
{"from": "2025-01-01", "score": -3.0, "note": "无投行覆盖,成交极低"},
|
||||
{"from": "2025-10-01", "score": -4.0, "note": "发盈警,市场信心极低"},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_snapshot_score(timeline, date):
|
||||
"""根据日期获取对应时间段的快照分数"""
|
||||
score = timeline[0]["score"]
|
||||
for entry in timeline:
|
||||
if str(date.date()) >= entry["from"]:
|
||||
score = entry["score"]
|
||||
else:
|
||||
break
|
||||
return score
|
||||
|
||||
def calc_rsi(series, period=14):
|
||||
delta = series.diff()
|
||||
gain = delta.clip(lower=0)
|
||||
loss = -delta.clip(upper=0)
|
||||
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
|
||||
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
|
||||
rs = avg_gain / avg_loss
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
def calc_macd(series, fast=12, slow=26, signal=9):
|
||||
ema_fast = series.ewm(span=fast, adjust=False).mean()
|
||||
ema_slow = series.ewm(span=slow, adjust=False).mean()
|
||||
macd = ema_fast - ema_slow
|
||||
signal_line = macd.ewm(span=signal, adjust=False).mean()
|
||||
return macd, signal_line, macd - signal_line
|
||||
|
||||
def score_technical(row):
|
||||
"""技术面评分 -10 ~ +10"""
|
||||
score = 0
|
||||
# RSI
|
||||
if row["RSI"] < 30: score += 3
|
||||
elif row["RSI"] < 45: score += 1
|
||||
elif row["RSI"] > 70: score -= 3
|
||||
elif row["RSI"] > 55: score -= 1
|
||||
# MACD 金叉/死叉
|
||||
if row["MACD_hist"] > 0 and row["MACD_hist_prev"] <= 0: score += 3
|
||||
elif row["MACD_hist"] < 0 and row["MACD_hist_prev"] >= 0: score -= 3
|
||||
elif row["MACD_hist"] > 0: score += 1
|
||||
else: score -= 1
|
||||
# 均线排列
|
||||
if row["MA5"] > row["MA20"] > row["MA60"]: score += 2
|
||||
elif row["MA5"] < row["MA20"] < row["MA60"]: score -= 2
|
||||
# MA20 突破/跌破
|
||||
if row["Close"] > row["MA20"] and row["Close_prev"] <= row["MA20_prev"]: score += 1
|
||||
elif row["Close"] < row["MA20"] and row["Close_prev"] >= row["MA20_prev"]: score -= 1
|
||||
return float(np.clip(score, -10, 10))
|
||||
|
||||
# ── 回测主函数 ────────────────────────────────────────────────────────
|
||||
|
||||
def backtest(name, ticker):
|
||||
print(f"\n{'='*65}")
|
||||
print(f" 回测: {name} ({ticker})")
|
||||
print(f"{'='*65}")
|
||||
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty or len(df) < 60:
|
||||
print(" ⚠️ 数据不足,跳过")
|
||||
return None
|
||||
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = df.columns.droplevel(1)
|
||||
|
||||
close = df["Close"]
|
||||
df["RSI"] = calc_rsi(close)
|
||||
macd, sig_line, hist = calc_macd(close)
|
||||
df["MACD_hist"] = hist
|
||||
df["MACD_hist_prev"] = hist.shift(1)
|
||||
for p in [5, 20, 60]:
|
||||
df[f"MA{p}"] = close.rolling(p).mean()
|
||||
df["MA20_prev"] = df["MA20"].shift(1)
|
||||
df["Close_prev"] = close.shift(1)
|
||||
df = df.dropna()
|
||||
|
||||
# 加入三维度评分
|
||||
tech_scores = []
|
||||
fund_scores = []
|
||||
sent_scores = []
|
||||
total_scores = []
|
||||
|
||||
for date, row in df.iterrows():
|
||||
t = score_technical(row)
|
||||
f = get_snapshot_score(FUNDAMENTAL_TIMELINE[name], date)
|
||||
s = get_snapshot_score(SENTIMENT_TIMELINE[name], date)
|
||||
combined = W_TECH * t + W_FUNDAMENTAL * f + W_SENTIMENT * s
|
||||
tech_scores.append(t)
|
||||
fund_scores.append(f)
|
||||
sent_scores.append(s)
|
||||
total_scores.append(combined)
|
||||
|
||||
df["Tech"] = tech_scores
|
||||
df["Fund"] = fund_scores
|
||||
df["Sent"] = sent_scores
|
||||
df["Score"] = total_scores
|
||||
|
||||
# 买卖阈值:综合评分 >= 1.5 买入;<= -1.5 卖出
|
||||
BUY_THRESH = 1.5
|
||||
SELL_THRESH = -1.5
|
||||
df["Signal"] = 0
|
||||
df.loc[df["Score"] >= BUY_THRESH, "Signal"] = 1
|
||||
df.loc[df["Score"] <= SELL_THRESH, "Signal"] = -1
|
||||
|
||||
# ── 模拟交易 ──
|
||||
capital = float(INITIAL_CAPITAL)
|
||||
position = 0
|
||||
entry_price = 0.0
|
||||
trades = []
|
||||
|
||||
for date, row in df.iterrows():
|
||||
price = float(row["Close"])
|
||||
sig = int(row["Signal"])
|
||||
|
||||
if sig == 1 and position == 0 and capital > price:
|
||||
shares = int(capital / price)
|
||||
position = shares
|
||||
entry_price = price
|
||||
capital -= shares * price
|
||||
trades.append({
|
||||
"日期": date.date(), "操作": "买入",
|
||||
"价格": round(price, 4), "股数": shares,
|
||||
"综合分": round(float(row["Score"]), 2),
|
||||
"技术": round(float(row["Tech"]), 1),
|
||||
"基本面": round(float(row["Fund"]), 1),
|
||||
"舆情": round(float(row["Sent"]), 1),
|
||||
})
|
||||
|
||||
elif sig == -1 and position > 0:
|
||||
revenue = position * price
|
||||
pnl = revenue - position * entry_price
|
||||
pnl_pct = pnl / (position * entry_price) * 100
|
||||
capital += revenue
|
||||
trades.append({
|
||||
"日期": date.date(), "操作": "卖出",
|
||||
"价格": round(price, 4), "股数": position,
|
||||
"综合分": round(float(row["Score"]), 2),
|
||||
"技术": round(float(row["Tech"]), 1),
|
||||
"基本面": round(float(row["Fund"]), 1),
|
||||
"舆情": round(float(row["Sent"]), 1),
|
||||
"盈亏HKD": round(pnl, 2),
|
||||
"盈亏%": f"{pnl_pct:+.1f}%",
|
||||
})
|
||||
position = 0
|
||||
entry_price = 0.0
|
||||
|
||||
last_price = float(df["Close"].iloc[-1])
|
||||
if position > 0:
|
||||
unrealized = position * (last_price - entry_price)
|
||||
capital_total = capital + position * last_price
|
||||
trades.append({
|
||||
"日期": "持仓中", "操作": "未平仓",
|
||||
"价格": round(last_price, 4), "股数": position,
|
||||
"未实现盈亏HKD": round(unrealized, 2),
|
||||
})
|
||||
else:
|
||||
capital_total = capital
|
||||
|
||||
strategy_return = (capital_total - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
buy_hold_return = (last_price / float(df["Close"].iloc[0]) - 1) * 100
|
||||
|
||||
# ── 输出 ──
|
||||
print(f"\n 📊 交易记录:")
|
||||
tdf = pd.DataFrame(trades)
|
||||
if not tdf.empty:
|
||||
print(tdf.to_string(index=False))
|
||||
else:
|
||||
print(" 无交易信号")
|
||||
|
||||
# 评分分布统计
|
||||
print(f"\n 📉 评分分布(综合):")
|
||||
bins = [-10, -3, -1.5, 0, 1.5, 3, 10]
|
||||
labels = ["强卖[-10,-3]","卖[-3,-1.5]","中性[-1.5,0]","中性[0,1.5]","买[1.5,3]","强买[3,10]"]
|
||||
score_ser = df["Score"]
|
||||
for label, cnt in zip(labels, np.histogram(score_ser, bins=bins)[0]):
|
||||
bar = "█" * int(cnt / max(1, len(df)) * 40)
|
||||
print(f" {label:>18}: {bar} ({cnt}天)")
|
||||
|
||||
print(f"\n 📈 回测结果汇总:")
|
||||
print(f" 初始资金: HKD {INITIAL_CAPITAL:>10,.0f}")
|
||||
print(f" 最终资金: HKD {capital_total:>10,.2f}")
|
||||
print(f" 策略总收益: {strategy_return:>+10.1f}%")
|
||||
print(f" 买入持有收益: {buy_hold_return:>+10.1f}% (同期)")
|
||||
print(f" 超额收益(α): {strategy_return - buy_hold_return:>+10.1f}%")
|
||||
print(f" 触发交易次数: {len([t for t in trades if t.get('操作') in ['买入','卖出']]):>10}")
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"strategy": strategy_return,
|
||||
"buy_hold": buy_hold_return,
|
||||
"alpha": strategy_return - buy_hold_return,
|
||||
"trades": len([t for t in trades if t.get("操作") in ["买入", "卖出"]]),
|
||||
}
|
||||
|
||||
|
||||
# ── 主入口 ────────────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
print("\n🔬 港股 AI 综合评分系统 v2 — 历史回测")
|
||||
print(f" 权重: 技术面 {W_TECH*100:.0f}% | 基本面 {W_FUNDAMENTAL*100:.0f}% | 舆情 {W_SENTIMENT*100:.0f}%")
|
||||
print(f" 买入阈值: ≥1.5 | 卖出阈值: ≤-1.5 | 数据周期: {PERIOD}\n")
|
||||
|
||||
results = []
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
if i > 0:
|
||||
time.sleep(3) # 避免 yfinance 限速
|
||||
r = backtest(name, ticker)
|
||||
if r:
|
||||
results.append(r)
|
||||
|
||||
if results:
|
||||
print(f"\n{'='*65}")
|
||||
print(" 📋 三股票综合汇总")
|
||||
print(f"{'='*65}")
|
||||
print(f" {'股票':<12} {'策略收益':>10} {'买持收益':>10} {'超额收益α':>10} {'交易次数':>8}")
|
||||
print(f" {'-'*54}")
|
||||
for r in results:
|
||||
flag = "✅" if r["alpha"] > 0 else "❌"
|
||||
print(f" {r['name']:<12} {r['strategy']:>+9.1f}% {r['buy_hold']:>+9.1f}% {r['alpha']:>+9.1f}% {r['trades']:>6} {flag}")
|
||||
print()
|
||||
294
stock_backtest_v3_ab.py
Normal file
294
stock_backtest_v3_ab.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
港股 AI 综合评分系统 v3 - A/B 回测对比
|
||||
A: 原版(固定阈值 + 全仓)
|
||||
B: 优化版(移动止损 + 仓位分级 + 成交量确认)
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import time
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# ── 参数 ──────────────────────────────────────────────────────────────
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
}
|
||||
PERIOD = "2y"
|
||||
INITIAL_CAPITAL = 10000.0 # HKD
|
||||
|
||||
W_TECH = 0.50
|
||||
W_FUNDAMENTAL = 0.30
|
||||
W_SENTIMENT = 0.20
|
||||
|
||||
# 版本 A:固定阈值
|
||||
A_BUY_THRESH = 1.5
|
||||
A_SELL_THRESH = -1.5
|
||||
|
||||
# 版本 B:优化参数
|
||||
B_BUY_THRESH = 1.5 # 买入阈值不变
|
||||
B_SELL_THRESH = -1.5 # 评分卖出阈值
|
||||
B_TRAILING_STOP = 0.12 # 移动止损:从最高点回撤12%触发卖出
|
||||
B_VOL_CONFIRM = 1.2 # 成交量确认:买入日成交量需 > 20日均量 × 1.2
|
||||
# 仓位分级(按综合评分)
|
||||
def position_ratio(score):
|
||||
if score >= 5: return 1.0 # 满仓
|
||||
elif score >= 3: return 0.6 # 六成仓
|
||||
else: return 0.3 # 三成仓
|
||||
|
||||
# ── 快照数据 ──────────────────────────────────────────────────────────
|
||||
FUNDAMENTAL_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-01-01", "score": 0.0},
|
||||
{"from": "2025-08-01", "score": 1.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -2.0},
|
||||
{"from": "2025-01-01", "score": -1.0},
|
||||
{"from": "2025-09-01", "score": 1.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -4.0},
|
||||
{"from": "2025-01-01", "score": -4.0},
|
||||
{"from": "2025-10-01", "score": -5.0},
|
||||
],
|
||||
}
|
||||
SENTIMENT_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -1.0},
|
||||
{"from": "2024-10-01", "score": 1.0},
|
||||
{"from": "2025-01-01", "score": 2.0},
|
||||
{"from": "2026-01-01", "score": 3.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-04-01", "score": 1.0},
|
||||
{"from": "2025-10-01", "score": 2.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-06-01", "score": -3.0},
|
||||
{"from": "2025-01-01", "score": -3.0},
|
||||
{"from": "2025-10-01", "score": -4.0},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────────
|
||||
def get_snapshot(timeline, date):
|
||||
score = timeline[0]["score"]
|
||||
for e in timeline:
|
||||
if str(date.date()) >= e["from"]:
|
||||
score = e["score"]
|
||||
else:
|
||||
break
|
||||
return score
|
||||
|
||||
def calc_rsi(s, p=14):
|
||||
d = s.diff()
|
||||
g = d.clip(lower=0).ewm(com=p-1, min_periods=p).mean()
|
||||
l = (-d.clip(upper=0)).ewm(com=p-1, min_periods=p).mean()
|
||||
return 100 - 100 / (1 + g / l)
|
||||
|
||||
def calc_macd(s, fast=12, slow=26, sig=9):
|
||||
ef = s.ewm(span=fast, adjust=False).mean()
|
||||
es = s.ewm(span=slow, adjust=False).mean()
|
||||
m = ef - es
|
||||
sl = m.ewm(span=sig, adjust=False).mean()
|
||||
return m, sl, m - sl
|
||||
|
||||
def score_tech(row):
|
||||
s = 0
|
||||
if row.RSI < 30: s += 3
|
||||
elif row.RSI < 45: s += 1
|
||||
elif row.RSI > 70: s -= 3
|
||||
elif row.RSI > 55: s -= 1
|
||||
if row.MACD_h > 0 and row.MACD_h_p <= 0: s += 3
|
||||
elif row.MACD_h < 0 and row.MACD_h_p >= 0: s -= 3
|
||||
elif row.MACD_h > 0: s += 1
|
||||
else: s -= 1
|
||||
if row.MA5 > row.MA20 > row.MA60: s += 2
|
||||
elif row.MA5 < row.MA20 < row.MA60: s -= 2
|
||||
if row.Close > row.MA20 and row.Close_p <= row.MA20_p: s += 1
|
||||
elif row.Close < row.MA20 and row.Close_p >= row.MA20_p: s -= 1
|
||||
return float(np.clip(s, -10, 10))
|
||||
|
||||
def prepare_df(ticker):
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty or len(df) < 60:
|
||||
return None
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = df.columns.droplevel(1)
|
||||
c = df["Close"]
|
||||
df["RSI"] = calc_rsi(c)
|
||||
_, _, h = calc_macd(c)
|
||||
df["MACD_h"] = h
|
||||
df["MACD_h_p"] = h.shift(1)
|
||||
for p in [5, 20, 60]:
|
||||
df[f"MA{p}"] = c.rolling(p).mean()
|
||||
df["MA20_p"] = df["MA20"].shift(1)
|
||||
df["Close_p"] = c.shift(1)
|
||||
df["Vol20"] = df["Volume"].rolling(20).mean() # 20日均量
|
||||
return df.dropna()
|
||||
|
||||
# ── 版本 A:原版回测 ─────────────────────────────────────────────────
|
||||
def run_A(name, df):
|
||||
capital, position, entry_price = INITIAL_CAPITAL, 0, 0.0
|
||||
trades = []
|
||||
for date, row in df.iterrows():
|
||||
f = get_snapshot(FUNDAMENTAL_TIMELINE[name], date)
|
||||
s = get_snapshot(SENTIMENT_TIMELINE[name], date)
|
||||
t = score_tech(row)
|
||||
score = W_TECH * t + W_FUNDAMENTAL * f + W_SENTIMENT * s
|
||||
price = float(row["Close"])
|
||||
|
||||
if score >= A_BUY_THRESH and position == 0 and capital > price:
|
||||
shares = int(capital / price)
|
||||
position, entry_price = shares, price
|
||||
capital -= shares * price
|
||||
trades.append(("买入", date.date(), price, shares, round(score,2), None))
|
||||
|
||||
elif score <= A_SELL_THRESH and position > 0:
|
||||
pnl = position * (price - entry_price)
|
||||
capital += position * price
|
||||
trades.append(("卖出", date.date(), price, position, round(score,2),
|
||||
f"{pnl/abs(position*entry_price)*100:+.1f}%"))
|
||||
position = 0
|
||||
|
||||
last = float(df["Close"].iloc[-1])
|
||||
total = capital + position * last
|
||||
if position > 0:
|
||||
pnl = position * (last - entry_price)
|
||||
trades.append(("未平仓", "持仓中", last, position, "-",
|
||||
f"{pnl/abs(position*entry_price)*100:+.1f}%"))
|
||||
return total, trades
|
||||
|
||||
# ── 版本 B:优化版回测 ───────────────────────────────────────────────
|
||||
def run_B(name, df):
|
||||
capital, position, entry_price = INITIAL_CAPITAL, 0, 0.0
|
||||
highest_price = 0.0 # 持仓期间最高价(移动止损用)
|
||||
trades = []
|
||||
|
||||
for date, row in df.iterrows():
|
||||
f = get_snapshot(FUNDAMENTAL_TIMELINE[name], date)
|
||||
s = get_snapshot(SENTIMENT_TIMELINE[name], date)
|
||||
t = score_tech(row)
|
||||
score = W_TECH * t + W_FUNDAMENTAL * f + W_SENTIMENT * s
|
||||
price = float(row["Close"])
|
||||
vol = float(row["Volume"])
|
||||
vol20 = float(row["Vol20"])
|
||||
|
||||
# ── 买入逻辑(加成交量确认)──
|
||||
if score >= B_BUY_THRESH and position == 0 and capital > price:
|
||||
vol_ok = vol >= vol20 * B_VOL_CONFIRM # 成交量放大确认
|
||||
if vol_ok:
|
||||
ratio = position_ratio(score) # 仓位分级
|
||||
invest = capital * ratio
|
||||
shares = int(invest / price)
|
||||
if shares > 0:
|
||||
position, entry_price = shares, price
|
||||
highest_price = price
|
||||
capital -= shares * price
|
||||
trades.append(("买入", date.date(), price, shares,
|
||||
round(score,2), f"仓位{ratio*100:.0f}%", f"量比{vol/vol20:.1f}x"))
|
||||
|
||||
# ── 持仓管理 ──
|
||||
elif position > 0:
|
||||
highest_price = max(highest_price, price)
|
||||
trailing_triggered = price <= highest_price * (1 - B_TRAILING_STOP)
|
||||
score_triggered = score <= B_SELL_THRESH
|
||||
|
||||
if trailing_triggered or score_triggered:
|
||||
reason = f"移动止损({price:.3f}≤{highest_price*(1-B_TRAILING_STOP):.3f})" \
|
||||
if trailing_triggered else f"评分卖出({score:.1f})"
|
||||
pnl = position * (price - entry_price)
|
||||
capital += position * price
|
||||
trades.append(("卖出", date.date(), price, position,
|
||||
round(score,2), reason,
|
||||
f"{pnl/abs(position*entry_price)*100:+.1f}%"))
|
||||
position, highest_price = 0, 0.0
|
||||
|
||||
last = float(df["Close"].iloc[-1])
|
||||
total = capital + position * last
|
||||
if position > 0:
|
||||
pnl = position * (last - entry_price)
|
||||
trades.append(("未平仓", "持仓中", last, position, "-", "-",
|
||||
f"{pnl/abs(position*entry_price)*100:+.1f}%"))
|
||||
return total, trades
|
||||
|
||||
# ── 主流程 ────────────────────────────────────────────────────────────
|
||||
def run_ab_test(name, ticker):
|
||||
print(f"\n{'='*68}")
|
||||
print(f" {name} ({ticker})")
|
||||
print(f"{'='*68}")
|
||||
df = prepare_df(ticker)
|
||||
if df is None:
|
||||
print(" ⚠️ 数据不足,跳过")
|
||||
return None
|
||||
|
||||
first_price = float(df["Close"].iloc[0])
|
||||
last_price = float(df["Close"].iloc[-1])
|
||||
bh_return = (last_price / first_price - 1) * 100
|
||||
|
||||
total_A, trades_A = run_A(name, df)
|
||||
total_B, trades_B = run_B(name, df)
|
||||
|
||||
ret_A = (total_A - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
ret_B = (total_B - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
|
||||
print(f"\n 【版本A 原版】交易记录:")
|
||||
for t in trades_A:
|
||||
print(f" {str(t[1]):<12} {t[0]:<4} 价:{t[2]:.4f} 股:{t[3]:>6} 分:{t[4]} {t[5] or ''}")
|
||||
|
||||
print(f"\n 【版本B 优化版】交易记录:")
|
||||
for t in trades_B:
|
||||
extra = " ".join(str(x) for x in t[5:] if x)
|
||||
print(f" {str(t[1]):<12} {t[0]:<4} 价:{t[2]:.4f} 股:{t[3]:>6} 分:{t[4]} {extra}")
|
||||
|
||||
print(f"\n {'':20} {'版本A(原版)':>12} {'版本B(优化)':>12} {'买入持有':>10}")
|
||||
print(f" {'策略总收益':<20} {ret_A:>+11.1f}% {ret_B:>+11.1f}% {bh_return:>+9.1f}%")
|
||||
print(f" {'超额收益α':<20} {ret_A-bh_return:>+11.1f}% {ret_B-bh_return:>+11.1f}%")
|
||||
print(f" {'交易次数':<20} {len([t for t in trades_A if t[0] in ('买入','卖出')]):>12} "
|
||||
f"{len([t for t in trades_B if t[0] in ('买入','卖出')]):>12}")
|
||||
print(f" {'B vs A 提升':<20} {'':>12} {ret_B-ret_A:>+11.1f}%")
|
||||
|
||||
return {"name": name, "A": ret_A, "B": ret_B, "BH": bh_return,
|
||||
"A_trades": len([t for t in trades_A if t[0] in ('买入','卖出')]),
|
||||
"B_trades": len([t for t in trades_B if t[0] in ('买入','卖出')])}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n🔬 港股 AI 评分系统 A/B 回测对比")
|
||||
print(" A: 固定阈值 + 全仓出入")
|
||||
print(" B: 移动止损12% + 仓位分级(30/60/100%) + 成交量确认(1.2x)")
|
||||
print(f" 数据周期: {PERIOD} | 初始资金: HKD {INITIAL_CAPITAL:,.0f}/股\n")
|
||||
|
||||
results = []
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
if i > 0: time.sleep(5)
|
||||
r = run_ab_test(name, ticker)
|
||||
if r: results.append(r)
|
||||
|
||||
if results:
|
||||
print(f"\n{'='*68}")
|
||||
print(" 📋 A/B 汇总对比")
|
||||
print(f"{'='*68}")
|
||||
print(f" {'股票':<12} {'A收益':>9} {'B收益':>9} {'买持':>9} {'B-A':>8} {'A笔数':>6} {'B笔数':>6}")
|
||||
print(f" {'-'*64}")
|
||||
for r in results:
|
||||
winner = "B✅" if r["B"] > r["A"] else "A✅"
|
||||
print(f" {r['name']:<12} {r['A']:>+8.1f}% {r['B']:>+8.1f}% {r['BH']:>+8.1f}% "
|
||||
f"{r['B']-r['A']:>+7.1f}% {r['A_trades']:>6} {r['B_trades']:>6} {winner}")
|
||||
|
||||
avg_a = np.mean([r["A"] for r in results])
|
||||
avg_b = np.mean([r["B"] for r in results])
|
||||
avg_bh = np.mean([r["BH"] for r in results])
|
||||
print(f" {'平均':<12} {avg_a:>+8.1f}% {avg_b:>+8.1f}% {avg_bh:>+8.1f}% {avg_b-avg_a:>+7.1f}%")
|
||||
print()
|
||||
268
stock_backtest_v4_ab.py
Normal file
268
stock_backtest_v4_ab.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
港股 AI v4 A/B 回测 — 支持本地CSV缓存,绕过yfinance限速
|
||||
用法:
|
||||
1. 正常运行:python3 stock_backtest_v4_ab.py
|
||||
2. 强制重新下载:python3 stock_backtest_v4_ab.py --refresh
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import time, os, sys
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
CACHE_DIR = "data"
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
FORCE_REFRESH = "--refresh" in sys.argv
|
||||
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
}
|
||||
PERIOD = "2y"
|
||||
INITIAL_CAPITAL = 10000.0
|
||||
W_TECH, W_FUNDAMENTAL, W_SENTIMENT = 0.50, 0.30, 0.20
|
||||
BUY_THRESH, SELL_THRESH = 1.5, -1.5
|
||||
|
||||
# A: 固定止损
|
||||
A_TRAILING_STOP = 0.12
|
||||
A_VOL_CONFIRM = 1.2
|
||||
|
||||
# B: ATR动态止损
|
||||
B_ATR_MULT = 2.5
|
||||
B_ATR_PERIOD = 14
|
||||
B_VOL_CONFIRM = 1.2
|
||||
B_MIN_STOP = 0.08
|
||||
B_MAX_STOP = 0.35
|
||||
|
||||
def position_ratio(score):
|
||||
if score >= 5: return 1.0
|
||||
elif score >= 3: return 0.6
|
||||
return 0.3
|
||||
|
||||
FUNDAMENTAL_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-01-01", "score": 0.0},
|
||||
{"from": "2025-08-01", "score": 1.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -2.0},
|
||||
{"from": "2025-01-01", "score": -1.0},
|
||||
{"from": "2025-09-01", "score": 1.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -4.0},
|
||||
{"from": "2025-01-01", "score": -4.0},
|
||||
{"from": "2025-10-01", "score": -5.0},
|
||||
],
|
||||
}
|
||||
SENTIMENT_TIMELINE = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -1.0},
|
||||
{"from": "2024-10-01", "score": 1.0},
|
||||
{"from": "2025-01-01", "score": 2.0},
|
||||
{"from": "2026-01-01", "score": 3.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-04-01", "score": 1.0},
|
||||
{"from": "2025-10-01", "score": 2.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-06-01", "score": -3.0},
|
||||
{"from": "2025-01-01", "score": -3.0},
|
||||
{"from": "2025-10-01", "score": -4.0},
|
||||
],
|
||||
}
|
||||
|
||||
def get_snapshot(tl, date):
|
||||
score = tl[0]["score"]
|
||||
for e in tl:
|
||||
if str(date.date()) >= e["from"]: score = e["score"]
|
||||
else: break
|
||||
return score
|
||||
|
||||
def calc_rsi(s, p=14):
|
||||
d = s.diff()
|
||||
g = d.clip(lower=0).ewm(com=p-1, min_periods=p).mean()
|
||||
l = (-d.clip(upper=0)).ewm(com=p-1, min_periods=p).mean()
|
||||
return 100 - 100 / (1 + g / l)
|
||||
|
||||
def calc_macd(s, fast=12, slow=26, sig=9):
|
||||
m = s.ewm(span=fast, adjust=False).mean() - s.ewm(span=slow, adjust=False).mean()
|
||||
return m - m.ewm(span=sig, adjust=False).mean()
|
||||
|
||||
def calc_atr(df, period=14):
|
||||
hi, lo, cl = df["High"], df["Low"], df["Close"]
|
||||
tr = pd.concat([(hi-lo), (hi-cl.shift(1)).abs(), (lo-cl.shift(1)).abs()], axis=1).max(axis=1)
|
||||
return tr.ewm(com=period-1, min_periods=period).mean()
|
||||
|
||||
def score_tech(row):
|
||||
s = 0
|
||||
if row.RSI < 30: s += 3
|
||||
elif row.RSI < 45: s += 1
|
||||
elif row.RSI > 70: s -= 3
|
||||
elif row.RSI > 55: s -= 1
|
||||
if row.MACD_h > 0 and row.MACD_h_p <= 0: s += 3
|
||||
elif row.MACD_h < 0 and row.MACD_h_p >= 0: s -= 3
|
||||
elif row.MACD_h > 0: s += 1
|
||||
else: s -= 1
|
||||
if row.MA5 > row.MA20 > row.MA60: s += 2
|
||||
elif row.MA5 < row.MA20 < row.MA60: s -= 2
|
||||
if row.Close > row.MA20 and row.Close_p <= row.MA20_p: s += 1
|
||||
elif row.Close < row.MA20 and row.Close_p >= row.MA20_p: s -= 1
|
||||
return float(np.clip(s, -10, 10))
|
||||
|
||||
def load_data(ticker):
|
||||
"""优先读CSV缓存,否则从yfinance下载并缓存"""
|
||||
sym = ticker.replace(".HK", "")
|
||||
fp = os.path.join(CACHE_DIR, f"{sym}.csv")
|
||||
if os.path.exists(fp) and not FORCE_REFRESH:
|
||||
df = pd.read_csv(fp, index_col=0, parse_dates=True)
|
||||
print(f" 📂 读取缓存: {fp} ({len(df)} 行)")
|
||||
return df
|
||||
print(f" 🌐 下载数据: {ticker}")
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty: return None
|
||||
if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.droplevel(1)
|
||||
df.to_csv(fp)
|
||||
print(f" 💾 已缓存: {fp}")
|
||||
return df
|
||||
|
||||
def prepare_df(ticker):
|
||||
df = load_data(ticker)
|
||||
if df is None or len(df) < 60: return None
|
||||
c = df["Close"]
|
||||
df["RSI"] = calc_rsi(c)
|
||||
h = calc_macd(c)
|
||||
df["MACD_h"] = h
|
||||
df["MACD_h_p"] = h.shift(1)
|
||||
for p in [5, 20, 60]: df[f"MA{p}"] = c.rolling(p).mean()
|
||||
df["MA20_p"] = df["MA20"].shift(1)
|
||||
df["Close_p"] = c.shift(1)
|
||||
df["Vol20"] = df["Volume"].rolling(20).mean()
|
||||
df["ATR"] = calc_atr(df, B_ATR_PERIOD)
|
||||
return df.dropna()
|
||||
|
||||
def simulate(name, df, use_atr=False):
|
||||
capital, position, entry_price = INITIAL_CAPITAL, 0, 0.0
|
||||
highest_price, trailing_pct = 0.0, 0.0
|
||||
trades = []
|
||||
vc = A_VOL_CONFIRM if not use_atr else B_VOL_CONFIRM
|
||||
|
||||
for date, row in df.iterrows():
|
||||
f = get_snapshot(FUNDAMENTAL_TIMELINE[name], date)
|
||||
s = get_snapshot(SENTIMENT_TIMELINE[name], date)
|
||||
t = score_tech(row)
|
||||
score = W_TECH*t + W_FUNDAMENTAL*f + W_SENTIMENT*s
|
||||
price = float(row["Close"])
|
||||
vol = float(row["Volume"])
|
||||
vol20 = float(row["Vol20"])
|
||||
|
||||
if score >= BUY_THRESH and position == 0 and capital > price:
|
||||
if vol >= vol20 * vc:
|
||||
ratio = position_ratio(score)
|
||||
shares = int(capital * ratio / price)
|
||||
if shares > 0:
|
||||
position, entry_price, highest_price = shares, price, price
|
||||
capital -= shares * price
|
||||
if use_atr:
|
||||
raw = float(row["ATR"]) * B_ATR_MULT / price
|
||||
trailing_pct = float(np.clip(raw, B_MIN_STOP, B_MAX_STOP))
|
||||
note = f"仓{ratio*100:.0f}% 量比{vol/vol20:.1f}x ATR止损{trailing_pct*100:.1f}%"
|
||||
else:
|
||||
trailing_pct = A_TRAILING_STOP
|
||||
note = f"仓{ratio*100:.0f}% 量比{vol/vol20:.1f}x 固定止损{trailing_pct*100:.0f}%"
|
||||
trades.append({"操作":"买入","日期":date.date(),"价格":round(price,4),
|
||||
"股数":shares,"评分":round(score,2),"备注":note})
|
||||
|
||||
elif position > 0:
|
||||
highest_price = max(highest_price, price)
|
||||
stop_price = highest_price * (1 - trailing_pct)
|
||||
if price <= stop_price or score <= SELL_THRESH:
|
||||
pnl = position * (price - entry_price)
|
||||
pct = pnl / (position * entry_price) * 100
|
||||
reason = (f"移动止损 高点{highest_price:.3f}→止损{stop_price:.3f}"
|
||||
if price <= stop_price else f"评分卖出({score:.1f})")
|
||||
capital += position * price
|
||||
trades.append({"操作":"卖出","日期":date.date(),"价格":round(price,4),
|
||||
"股数":position,"评分":round(score,2),
|
||||
"盈亏%":f"{pct:+.1f}%","备注":reason})
|
||||
position, highest_price, trailing_pct = 0, 0.0, 0.0
|
||||
|
||||
last = float(df["Close"].iloc[-1])
|
||||
total = capital + position * last
|
||||
if position > 0:
|
||||
pct = (last - entry_price) / entry_price * 100
|
||||
trades.append({"操作":"未平仓","日期":"持仓中","价格":round(last,4),
|
||||
"股数":position,"评分":"-","盈亏%":f"{pct:+.1f}%","备注":"-"})
|
||||
return total, trades
|
||||
|
||||
def run_ab(name, ticker):
|
||||
print(f"\n{'='*70}")
|
||||
print(f" {name} ({ticker})")
|
||||
print(f"{'='*70}")
|
||||
df = prepare_df(ticker)
|
||||
if df is None:
|
||||
print(" ⚠️ 数据不足,跳过")
|
||||
return None
|
||||
|
||||
avg_atr_pct = df["ATR"].mean() / df["Close"].mean() * 100
|
||||
est_stop = np.clip(df["ATR"].mean()*B_ATR_MULT/df["Close"].mean(), B_MIN_STOP, B_MAX_STOP)*100
|
||||
bh = (float(df["Close"].iloc[-1]) / float(df["Close"].iloc[0]) - 1) * 100
|
||||
print(f" ATR均值波动: {avg_atr_pct:.1f}% → B动态止损估算: {est_stop:.1f}% 买持: {bh:+.1f}%")
|
||||
|
||||
total_A, tA = simulate(name, df, use_atr=False)
|
||||
total_B, tB = simulate(name, df, use_atr=True)
|
||||
retA = (total_A - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
retB = (total_B - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
|
||||
for label, trades in [("版本A 固定止损12%", tA), ("版本B ATR动态止损", tB)]:
|
||||
print(f"\n 【{label}】")
|
||||
if not trades: print(" 无信号"); continue
|
||||
cols = [c for c in ["操作","日期","价格","股数","评分","盈亏%","备注"] if c in pd.DataFrame(trades).columns]
|
||||
print(pd.DataFrame(trades)[cols].to_string(index=False))
|
||||
|
||||
nA = len([t for t in tA if t["操作"] in ("买入","卖出")])
|
||||
nB = len([t for t in tB if t["操作"] in ("买入","卖出")])
|
||||
print(f"\n {'':22} {'A 固定12%':>12} {'B ATR动态':>12} {'买入持有':>10}")
|
||||
print(f" {'策略总收益':<22} {retA:>+11.1f}% {retB:>+11.1f}% {bh:>+9.1f}%")
|
||||
print(f" {'超额收益α':<22} {retA-bh:>+11.1f}% {retB-bh:>+11.1f}%")
|
||||
print(f" {'交易次数':<22} {nA:>12} {nB:>12}")
|
||||
w = "B ✅" if retB > retA else ("A ✅" if retA > retB else "平手")
|
||||
print(f" {'胜出':<22} {'':>23} → {w} (B-A: {retB-retA:+.1f}%)")
|
||||
return {"name":name,"A":retA,"B":retB,"BH":bh,"nA":nA,"nB":nB,"atr":avg_atr_pct}
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n🔬 港股 AI v4 A/B 回测 — ATR动态止损 vs 固定止损")
|
||||
print(f" A: 固定止损{A_TRAILING_STOP*100:.0f}% | B: ATR×{B_ATR_MULT}动态({B_MIN_STOP*100:.0f}%~{B_MAX_STOP*100:.0f}%)")
|
||||
print(f" 仓位分级 评分1.5-3→30% | 3-5→60% | >5→100%\n")
|
||||
|
||||
results = []
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
if i > 0: time.sleep(3)
|
||||
r = run_ab(name, ticker)
|
||||
if r: results.append(r)
|
||||
|
||||
if results:
|
||||
print(f"\n{'='*70}")
|
||||
print(" 📋 A/B 最终汇总")
|
||||
print(f"{'='*70}")
|
||||
print(f" {'股票':<12} {'ATR%':>6} {'A收益':>9} {'B收益':>9} {'买持':>9} {'B-A':>8} {'胜者':>5}")
|
||||
print(f" {'-'*64}")
|
||||
for r in results:
|
||||
w = "B✅" if r["B"]>r["A"] else ("A✅" if r["A"]>r["B"] else "平")
|
||||
print(f" {r['name']:<12} {r['atr']:>5.1f}% {r['A']:>+8.1f}% {r['B']:>+8.1f}% "
|
||||
f"{r['BH']:>+8.1f}% {r['B']-r['A']:>+7.1f}% {w:>5}")
|
||||
avg_a = np.mean([r["A"] for r in results])
|
||||
avg_b = np.mean([r["B"] for r in results])
|
||||
avg_bh = np.mean([r["BH"] for r in results])
|
||||
print(f" {'平均':<12} {'':>6} {avg_a:>+8.1f}% {avg_b:>+8.1f}% {avg_bh:>+8.1f}% {avg_b-avg_a:>+7.1f}%")
|
||||
324
stock_backtest_v5_abc.py
Normal file
324
stock_backtest_v5_abc.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
港股 AI 综合评分系统 v5 — 三版本 A/B/C 对比
|
||||
A: 固定止损 12%(全仓)
|
||||
B: ATR 动态止损(仓位分级)
|
||||
C: 混合策略 — 按股票波动率自动选择 A 或 B
|
||||
- ATR/价格 < 5% → 低波动,用固定止损 8%
|
||||
- ATR/价格 5~15% → 中波动,用 ATR×2.5 动态
|
||||
- ATR/价格 > 15% → 高波动,用 ATR×2.0 + 宽上限 40%
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import time, os, sys
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
CACHE_DIR = "data"
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
FORCE_REFRESH = "--refresh" in sys.argv
|
||||
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
}
|
||||
PERIOD = "2y"
|
||||
INITIAL_CAPITAL = 10000.0
|
||||
W_TECH, W_FUND, W_SENT = 0.50, 0.30, 0.20
|
||||
BUY_THRESH, SELL_THRESH = 1.5, -1.5
|
||||
VOL_CONFIRM = 1.2
|
||||
|
||||
# ── 版本参数 ──────────────────────────────────────────────────────────
|
||||
A_FIXED_STOP = 0.12 # A: 固定 12%
|
||||
|
||||
B_ATR_MULT = 2.5 # B: ATR × 2.5
|
||||
B_MIN_STOP = 0.08
|
||||
B_MAX_STOP = 0.35
|
||||
|
||||
# C: 混合 —— 阈值
|
||||
C_LOW_ATR_PCT = 0.05 # ATR% < 5% → 低波动
|
||||
C_HIGH_ATR_PCT = 0.15 # ATR% > 15% → 高波动
|
||||
C_LOW_FIXED = 0.08 # 低波动用固定 8%
|
||||
C_MID_ATR_MULT = 2.5 # 中波动 ATR×2.5
|
||||
C_HIGH_ATR_MULT= 2.0 # 高波动 ATR×2.0(更宽)
|
||||
C_HIGH_MAX = 0.40 # 高波动上限 40%
|
||||
C_MIN_STOP = 0.08
|
||||
C_MID_MAX = 0.35
|
||||
|
||||
# ── 快照数据 ──────────────────────────────────────────────────────────
|
||||
FUNDAMENTAL = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-01-01", "score": 0.0},
|
||||
{"from": "2025-08-01", "score": 1.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -2.0},
|
||||
{"from": "2025-01-01", "score": -1.0},
|
||||
{"from": "2025-09-01", "score": 1.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -4.0},
|
||||
{"from": "2025-01-01", "score": -4.0},
|
||||
{"from": "2025-10-01", "score": -5.0},
|
||||
],
|
||||
}
|
||||
SENTIMENT = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -1.0},
|
||||
{"from": "2024-10-01", "score": 1.0},
|
||||
{"from": "2025-01-01", "score": 2.0},
|
||||
{"from": "2026-01-01", "score": 3.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-04-01", "score": 1.0},
|
||||
{"from": "2025-10-01", "score": 2.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -2.0},
|
||||
{"from": "2024-06-01", "score": -3.0},
|
||||
{"from": "2025-01-01", "score": -3.0},
|
||||
{"from": "2025-10-01", "score": -4.0},
|
||||
],
|
||||
}
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────────
|
||||
def get_snap(tl, date):
|
||||
v = tl[0]["score"]
|
||||
for e in tl:
|
||||
if str(date.date()) >= e["from"]: v = e["score"]
|
||||
else: break
|
||||
return v
|
||||
|
||||
def calc_rsi(s, p=14):
|
||||
d = s.diff()
|
||||
g = d.clip(lower=0).ewm(com=p-1, min_periods=p).mean()
|
||||
l = (-d.clip(upper=0)).ewm(com=p-1, min_periods=p).mean()
|
||||
return 100 - 100/(1+g/l)
|
||||
|
||||
def calc_macd(s):
|
||||
m = s.ewm(span=12,adjust=False).mean() - s.ewm(span=26,adjust=False).mean()
|
||||
return m - m.ewm(span=9,adjust=False).mean()
|
||||
|
||||
def calc_atr(df, p=14):
|
||||
hi,lo,cl = df["High"],df["Low"],df["Close"]
|
||||
tr = pd.concat([(hi-lo),(hi-cl.shift(1)).abs(),(lo-cl.shift(1)).abs()],axis=1).max(axis=1)
|
||||
return tr.ewm(com=p-1,min_periods=p).mean()
|
||||
|
||||
def tech_score(row):
|
||||
s = 0
|
||||
if row.RSI<30: s+=3
|
||||
elif row.RSI<45: s+=1
|
||||
elif row.RSI>70: s-=3
|
||||
elif row.RSI>55: s-=1
|
||||
if row.MH>0 and row.MH_p<=0: s+=3
|
||||
elif row.MH<0 and row.MH_p>=0: s-=3
|
||||
elif row.MH>0: s+=1
|
||||
else: s-=1
|
||||
if row.MA5>row.MA20>row.MA60: s+=2
|
||||
elif row.MA5<row.MA20<row.MA60: s-=2
|
||||
if row.Close>row.MA20 and row.Cp<=row.MA20p: s+=1
|
||||
elif row.Close<row.MA20 and row.Cp>=row.MA20p: s-=1
|
||||
return float(np.clip(s,-10,10))
|
||||
|
||||
def pos_ratio(score):
|
||||
if score>=5: return 1.0
|
||||
elif score>=3: return 0.6
|
||||
return 0.3
|
||||
|
||||
def load(ticker):
|
||||
sym = ticker.replace(".HK","")
|
||||
fp = os.path.join(CACHE_DIR, f"{sym}.csv")
|
||||
if os.path.exists(fp) and not FORCE_REFRESH:
|
||||
df = pd.read_csv(fp, index_col=0, parse_dates=True)
|
||||
print(f" 📂 缓存: {fp} ({len(df)}行)")
|
||||
return df
|
||||
print(f" 🌐 下载: {ticker}")
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty: return None
|
||||
if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.droplevel(1)
|
||||
df.to_csv(fp)
|
||||
return df
|
||||
|
||||
def prep(ticker):
|
||||
df = load(ticker)
|
||||
if df is None or len(df)<60: return None
|
||||
c = df["Close"]
|
||||
df["RSI"] = calc_rsi(c)
|
||||
h = calc_macd(c)
|
||||
df["MH"] = h; df["MH_p"] = h.shift(1)
|
||||
for p in [5,20,60]: df[f"MA{p}"] = c.rolling(p).mean()
|
||||
df["MA20p"]= df["MA20"].shift(1); df["Cp"] = c.shift(1)
|
||||
df["Vol20"]= df["Volume"].rolling(20).mean()
|
||||
df["ATR"] = calc_atr(df)
|
||||
return df.dropna()
|
||||
|
||||
# ── 混合策略 C 的止损参数选择 ─────────────────────────────────────────
|
||||
def c_stop_params(avg_atr_pct):
|
||||
"""根据股票历史ATR波动率自动决定止损方式"""
|
||||
if avg_atr_pct < C_LOW_ATR_PCT:
|
||||
return "fixed", C_LOW_FIXED, C_LOW_FIXED, "低波动→固定止损"
|
||||
elif avg_atr_pct < C_HIGH_ATR_PCT:
|
||||
return "atr", C_MID_ATR_MULT, C_MID_MAX, "中波动→ATR×2.5"
|
||||
else:
|
||||
return "atr", C_HIGH_ATR_MULT, C_HIGH_MAX, "高波动→ATR×2.0"
|
||||
|
||||
# ── 通用模拟引擎 ──────────────────────────────────────────────────────
|
||||
def simulate(name, df, mode="A", c_avg_atr_pct=None):
|
||||
"""
|
||||
mode: 'A'=固定止损12%, 'B'=ATR动态, 'C'=混合自适应
|
||||
"""
|
||||
capital, position, entry = INITIAL_CAPITAL, 0, 0.0
|
||||
high_price, trail_pct = 0.0, 0.0
|
||||
trades = []
|
||||
|
||||
# C 版预先确定止损类型(全局一致,模拟真实部署)
|
||||
c_mode, c_mult, c_max, c_note = ("fixed",0,0,"") if mode!="C" else c_stop_params(c_avg_atr_pct)
|
||||
|
||||
for date, row in df.iterrows():
|
||||
f = get_snap(FUNDAMENTAL[name], date)
|
||||
s = get_snap(SENTIMENT[name], date)
|
||||
t = tech_score(row)
|
||||
score = W_TECH*t + W_FUND*f + W_SENT*s
|
||||
price = float(row["Close"])
|
||||
vol = float(row["Volume"])
|
||||
vol20 = float(row["Vol20"])
|
||||
|
||||
# 买入
|
||||
if score >= BUY_THRESH and position == 0 and capital > price:
|
||||
if vol >= vol20 * VOL_CONFIRM:
|
||||
ratio = pos_ratio(score)
|
||||
shares = int(capital * ratio / price)
|
||||
if shares > 0:
|
||||
position, entry, high_price = shares, price, price
|
||||
capital -= shares * price
|
||||
|
||||
# 确定止损幅度
|
||||
if mode == "A":
|
||||
trail_pct = A_FIXED_STOP
|
||||
note = f"仓{ratio*100:.0f}% 固定止损{trail_pct*100:.0f}%"
|
||||
elif mode == "B":
|
||||
raw = float(row["ATR"]) * B_ATR_MULT / price
|
||||
trail_pct = float(np.clip(raw, B_MIN_STOP, B_MAX_STOP))
|
||||
note = f"仓{ratio*100:.0f}% ATR止损{trail_pct*100:.1f}%"
|
||||
else: # C
|
||||
if c_mode == "fixed":
|
||||
trail_pct = c_mult if c_mult else C_LOW_FIXED
|
||||
note = f"仓{ratio*100:.0f}% {c_note} {trail_pct*100:.0f}%"
|
||||
else:
|
||||
raw = float(row["ATR"]) * c_mult / price
|
||||
trail_pct = float(np.clip(raw, C_MIN_STOP, c_max))
|
||||
note = f"仓{ratio*100:.0f}% {c_note} {trail_pct*100:.1f}%"
|
||||
|
||||
trades.append({"操作":"买入","日期":date.date(),"价格":round(price,4),
|
||||
"股数":shares,"评分":round(score,2),"备注":note})
|
||||
|
||||
elif position > 0:
|
||||
high_price = max(high_price, price)
|
||||
stop_price = high_price * (1 - trail_pct)
|
||||
if price <= stop_price or score <= SELL_THRESH:
|
||||
pnl = position*(price-entry); pct = pnl/(position*entry)*100
|
||||
reason = (f"止损 高{high_price:.3f}→线{stop_price:.3f}"
|
||||
if price<=stop_price else f"评分出({score:.1f})")
|
||||
capital += position*price
|
||||
trades.append({"操作":"卖出","日期":date.date(),"价格":round(price,4),
|
||||
"股数":position,"评分":round(score,2),
|
||||
"盈亏%":f"{pct:+.1f}%","备注":reason})
|
||||
position, high_price, trail_pct = 0, 0.0, 0.0
|
||||
|
||||
last = float(df["Close"].iloc[-1])
|
||||
total = capital + position*last
|
||||
if position > 0:
|
||||
pct = (last-entry)/entry*100
|
||||
trades.append({"操作":"未平仓","日期":"持仓中","价格":round(last,4),
|
||||
"股数":position,"评分":"-","盈亏%":f"{pct:+.1f}%","备注":"-"})
|
||||
return total, trades
|
||||
|
||||
# ── 主流程 ────────────────────────────────────────────────────────────
|
||||
def run_abc(name, ticker):
|
||||
print(f"\n{'='*72}")
|
||||
print(f" {name} ({ticker})")
|
||||
print(f"{'='*72}")
|
||||
df = prep(ticker)
|
||||
if df is None:
|
||||
print(" ⚠️ 数据不足,跳过")
|
||||
return None
|
||||
|
||||
avg_atr_pct = float(df["ATR"].mean() / df["Close"].mean())
|
||||
bh = (float(df["Close"].iloc[-1]) / float(df["Close"].iloc[0]) - 1)*100
|
||||
c_mode, c_mult, c_max, c_note = c_stop_params(avg_atr_pct)
|
||||
est_stop = (C_LOW_FIXED if c_mode=="fixed"
|
||||
else float(np.clip(df["ATR"].mean()*c_mult/df["Close"].mean(), C_MIN_STOP, c_max)))
|
||||
print(f" ATR均值: {avg_atr_pct*100:.1f}% C策略选择: [{c_note}] 估算止损: {est_stop*100:.1f}%")
|
||||
print(f" 买入持有收益: {bh:+.1f}%")
|
||||
|
||||
tA, trA = simulate(name, df, "A")
|
||||
tB, trB = simulate(name, df, "B")
|
||||
tC, trC = simulate(name, df, "C", avg_atr_pct)
|
||||
rA = (tA-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
rB = (tB-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
rC = (tC-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
|
||||
for label, trades in [("A 固定止损12%", trA),("B ATR动态", trB),("C 混合自适应", trC)]:
|
||||
print(f"\n 【版本{label}】")
|
||||
if not trades: print(" 无信号"); continue
|
||||
cols = [c for c in ["操作","日期","价格","股数","评分","盈亏%","备注"]
|
||||
if c in pd.DataFrame(trades).columns]
|
||||
print(pd.DataFrame(trades)[cols].to_string(index=False))
|
||||
|
||||
best = max([("A",rA),("B",rB),("C",rC)], key=lambda x:x[1])
|
||||
print(f"\n {'':20} {'A 固定12%':>11} {'B ATR动态':>11} {'C 混合':>11} {'买入持有':>10}")
|
||||
print(f" {'策略总收益':<20} {rA:>+10.1f}% {rB:>+10.1f}% {rC:>+10.1f}% {bh:>+9.1f}%")
|
||||
print(f" {'超额收益α':<20} {rA-bh:>+10.1f}% {rB-bh:>+10.1f}% {rC-bh:>+10.1f}%")
|
||||
nA = len([t for t in trA if t["操作"] in ("买入","卖出")])
|
||||
nB = len([t for t in trB if t["操作"] in ("买入","卖出")])
|
||||
nC = len([t for t in trC if t["操作"] in ("买入","卖出")])
|
||||
print(f" {'交易次数':<20} {nA:>11} {nB:>11} {nC:>11}")
|
||||
print(f" {'🏆 本轮胜出':<20} {'★' if best[0]=='A' else '':>11} {'★' if best[0]=='B' else '':>11} {'★' if best[0]=='C' else '':>11}")
|
||||
|
||||
return {"name":name, "A":rA, "B":rB, "C":rC, "BH":bh,
|
||||
"atr":avg_atr_pct*100, "c_note":c_note}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n🔬 港股 AI v5 — 三版本 A/B/C 对比回测")
|
||||
print(f" A: 固定止损{A_FIXED_STOP*100:.0f}%(全局)")
|
||||
print(f" B: ATR×{B_ATR_MULT}动态止损({B_MIN_STOP*100:.0f}%~{B_MAX_STOP*100:.0f}%)")
|
||||
print(f" C: 混合自适应 — ATR<5%→固定8% | 5~15%→ATR×2.5 | >15%→ATR×2.0")
|
||||
print(f" 仓位分级: 评分1.5-3→30% | 3-5→60% | >5→100%\n")
|
||||
|
||||
results = []
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
if i > 0: time.sleep(3)
|
||||
r = run_abc(name, ticker)
|
||||
if r: results.append(r)
|
||||
|
||||
if results:
|
||||
print(f"\n{'='*72}")
|
||||
print(" 📋 最终三版本汇总")
|
||||
print(f"{'='*72}")
|
||||
print(f" {'股票':<12} {'ATR%':>6} {'C策略':<18} {'A':>9} {'B':>9} {'C':>9} {'买持':>9}")
|
||||
print(f" {'-'*70}")
|
||||
for r in results:
|
||||
marks = {k:"★" for k in ["A","B","C"] if r[k]==max(r["A"],r["B"],r["C"])}
|
||||
print(f" {r['name']:<12} {r['atr']:>5.1f}% {r['c_note']:<18}"
|
||||
f" {r['A']:>+8.1f}%{marks.get('A',''):1}"
|
||||
f" {r['B']:>+8.1f}%{marks.get('B',''):1}"
|
||||
f" {r['C']:>+8.1f}%{marks.get('C',''):1}"
|
||||
f" {r['BH']:>+8.1f}%")
|
||||
avg = {k: np.mean([r[k] for r in results]) for k in ["A","B","C","BH"]}
|
||||
best_avg = max("A","B","C", key=lambda k: avg[k])
|
||||
marks = {k:"★" for k in ["A","B","C"] if k==best_avg}
|
||||
print(f" {'平均':<12} {'':>6} {'':18}"
|
||||
f" {avg['A']:>+8.1f}%{marks.get('A',''):1}"
|
||||
f" {avg['B']:>+8.1f}%{marks.get('B',''):1}"
|
||||
f" {avg['C']:>+8.1f}%{marks.get('C',''):1}"
|
||||
f" {avg['BH']:>+8.1f}%")
|
||||
print()
|
||||
Reference in New Issue
Block a user