
Dex Quant Skill
- 4 installs
- Updated April 27, 2026
- miyaosk/dex-quant-skill
Helps with ai & agent building tasks.
About
dex-quant-skill is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- dex-quant-skill
- AI & Agent Building
- AI-coding skill
Dex Quant Skill by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,372 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miyaosk/dex-quant-skill --skill dex-quant-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| Last updated | April 27, 2026 |
| Repository | miyaosk/dex-quant-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Backtester — 回测
目标
接收 strategy-maker 生成的策略脚本,本地运行脚本产出信号列表, 然后将信号发送到 dex-quant-server 后端执行回测。
Server 负责拉取 K 线数据(带 MySQL 缓存,同币同周期不重复下载), 用信号驱动回测引擎模拟真实交易,返回绩效报告和上线建议。
核心公式:
策略脚本 → 本地跑出信号 → 调 Server API(发信号)→ Server 拉 K 线 + 回测 → 返回结果---
触发条件
当用户表达以下意图时激活本 Skill:
- "帮我回测一下这个策略"
- "用 2024 年的数据跑一下看看"
- "这个策略去年能赚多少?"
- "回测结果怎么样?能上线吗?"
- "帮我测试 5 倍杠杆的效果"
- 任何涉及策略回测、历史验证、绩效分析的请求
---
输入
| 输入项 | 是否必须 | 说明 |
|---|---|---|
| 策略脚本 | 必须 | strategy-maker 生成的 .py 文件 |
| 回测时间范围 | 必须 | 起止日期(如 2024-01-01 ~ 2024-12-31) |
| K 线周期 | 可选 | 15m / 1h / 2h / 1d(默认 1h) |
| 初始资金 | 可选 | 默认 $100,000 |
| 杠杆 | 可选 | 默认 1x |
| 手续费率 | 可选 | 默认 Taker 0.05% |
| 滑点 | 可选 | 默认 5 bps |
---
输出
| 输出项 | 说明 |
|---|---|
| 绩效指标 | 收益率、夏普、Sortino、最大回撤、胜率、盈亏比、Calmar |
| 交易记录 | 每笔交易的开仓/平仓详情、盈亏、原因 |
| 权益曲线 | 账户净值随时间的变化 |
| 评估结论 | 通过(approved) / 先模拟(paper_trade_first) / 驳回(rejected) |
| 信号统计 | 总信号数、已执行信号数 |
---
工作流程
第 1 步:运行策略脚本(本地)
1. 确认脚本文件存在且实现了 generate_signals() 接口 2. 以 backtest 模式运行:
python my_strategy.py backtest 2024-01-01 2024-12-313. 收集脚本输出的信号列表
第 2 步:调 Server 回测接口
将信号列表 + 配置发送到 dex-quant-server:
from backtester.scripts.api_client import QuantAPIClient
client = QuantAPIClient("http://server-address:8000")
result = client.run_backtest(
strategy_name="BTC MACD 策略",
symbol="BTCUSDT",
timeframe="1h",
start_date="2024-01-01",
end_date="2024-12-31",
signals=signals, # 策略脚本产出的信号列表
leverage=3,
initial_capital=100000,
)第 3 步:Server 处理(自动)
Server 端自动完成:
1. 拉 K 线数据 — 支持 15m/1h/2h/1d,MySQL 缓存,同币同周期不重复下载 2. 信号驱动回测 — 逐根 K 线回放信号,模拟交易 3. 交易模拟 — 杠杆、保证金、手续费、滑点、资金费率、止损止盈、强平 4. 绩效计算 — Sharpe、Sortino、最大回撤、胜率、Calmar 等 5. 评估结论 — 综合指标给出三选一结论
第 4 步:展示结果
client.print_metrics(result) # 绩效报告
client.print_trades(result) # 交易明细
client.print_conclusion(result) # 结论和建议第 5 步:迭代优化
用户根据结果调整策略 → 重新生成信号 → 再次回测 → 循环
---
Server API(dex-quant-server)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/backtest/run | 核心:接收信号 + 配置,执行回测 |
| GET | /api/v1/backtest/{id} | 查询已保存的回测结果 |
| GET | /api/v1/backtest/{id}/trades | 交易记录 |
| GET | /api/v1/backtest/{id}/equity | 权益曲线 |
| POST | /api/v1/data/klines | K 线数据(带缓存) |
| GET | /api/v1/data/symbols | 交易对列表 |
| POST | /api/v1/strategies | 保存策略 |
| GET | /api/v1/strategies | 策略列表 |
| POST | /api/v1/signals/batch | 批量保存信号 |
| POST | /api/v1/signals/query | 查询信号 |
回测请求格式
{
"strategy_name": "BTC MACD 策略",
"strategy_id": "strat_abc123",
"symbol": "BTCUSDT",
"timeframe": "1h",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"signals": [
{
"timestamp": "2024-01-15 08:00:00+00:00",
"symbol": "BTCUSDT",
"action": "buy",
"direction": "long",
"confidence": 0.85,
"reason": "MACD 金叉 + RSI < 40",
"price_at_signal": 42350.0,
"suggested_stop_loss": 41500.0,
"suggested_take_profit": 45000.0
}
],
"initial_capital": 100000,
"leverage": 3,
"fee_rate": 0.0005,
"slippage_bps": 5.0,
"margin_mode": "isolated",
"direction": "long_short"
}回测响应格式
{
"backtest_id": "bt_abc123def456",
"strategy_id": "strat_abc123",
"strategy_name": "BTC MACD 策略",
"status": "completed",
"conclusion": "approved",
"metrics": {
"total_return_pct": 0.156,
"annual_return_pct": 0.178,
"sharpe_ratio": 1.85,
"max_drawdown_pct": -0.082,
"win_rate": 0.55,
"total_trades": 48,
"total_signals": 52,
"signals_executed": 48
},
"trades": [...],
"equity_curve": [...]
}---
K 线数据缓存
Server 端实现了 MySQL 缓存机制:
| 特性 | 说明 |
|---|---|
| 支持周期 | 15m / 1h / 2h / 1d |
| 缓存策略 | 同币同周期同时间范围命中缓存则不重复下载 |
| 缓存有效期 | 加密货币 1h,股票/商品 6h |
| 数据源 | Binance 永续/现货 |
---
回测引擎能力
| 能力 | 说明 |
|---|---|
| 多空双向 | 做多做空均支持 |
| 杠杆 1x-125x | 模拟真实杠杆 |
| 逐仓保证金 | 按仓位隔离 |
| 资金费率 | 8h 结算(永续合约) |
| 止损/止盈 | 按 bar 内高低价判断 |
| 强制平仓 | 保证金不足时爆仓 |
| 手续费 | Taker 0.05% / Maker 0.02% |
| 滑点 | 可配置 bps |
---
评估结论标准
| 结论 | 条件 |
|---|---|
| 通过 | 收益 > 10%,夏普 > 1.5,回撤 < 10%,胜率 > 40%,交易 ≥ 30 笔 |
| 先模拟 | 部分指标达标,但不够全面 |
| 驳回 | 亏损 > 10%,或有爆仓,或交易 < 5 笔 |
---
可用工具
API 客户端 — api_client.py
位置:backtester/scripts/api_client.py
| 方法 | 说明 |
|---|---|
run_backtest() | 提交信号驱动回测 |
get_backtest() | 查询已保存结果 |
get_trades() | 获取交易明细 |
get_equity() | 获取权益曲线 |
get_klines() | 获取 K 线数据 |
list_symbols() | 交易对列表 |
save_strategy() | 保存策略 |
save_signals() | 批量保存信号 |
print_metrics() | 打印绩效报告 |
print_trades() | 打印交易记录 |
print_conclusion() | 打印结论建议 |
本地数据获取 — data_client.py
位置:backtester/scripts/data_client.py
策略脚本生成信号时可能需要本地拉数据(用于计算指标)。
技术指标库 — indicators.py
位置:backtester/scripts/indicators.py
支持:SMA, EMA, RSI, MACD, Bollinger Bands, ATR, KDJ, crossover/crossunder 等 12 种。
---
禁止事项
| 禁止行为 | 原因 |
|---|---|
| ❌ 修改用户的策略脚本 | 修改策略是 strategy-maker 的职责 |
| ❌ 仅因回测盈利就通过 | 还需检查稳健性、过拟合、成本影响 |
| ❌ 用未来数据回测 | 严格按时间顺序,不可前视偏差 |
| ❌ 部署监控或执行交易 | 这是 monitor-executor 的职责 |
*.pyc
__pycache__/
.env
*.egg-info/
dist/
build/
.history/
__pycache__/
display_name: "DEX Quant Skill"
short_description: "AI 量化交易平台 — 策略设计→回测→评审→信号→执行"
default_prompt: "帮我设计一个加密货币量化策略"
{
"review_id": "",
"strategy_id": "",
"backtest_id": "",
"version": "",
"reviewer": "backtest-reviewer",
"review_time": "",
"decision": "pending",
"summary": "",
"strengths": [],
"risks": [],
"metrics_summary": {
"sharpe_ratio": {
"value": null,
"rating": "",
"comment": ""
},
"max_drawdown": {
"value": null,
"rating": "",
"comment": ""
},
"win_rate": {
"value": null,
"rating": "",
"comment": ""
},
"profit_factor": {
"value": null,
"rating": "",
"comment": ""
},
"calmar_ratio": {
"value": null,
"rating": "",
"comment": ""
},
"sortino_ratio": {
"value": null,
"rating": "",
"comment": ""
},
"total_return": {
"value": null,
"rating": "",
"comment": ""
},
"annual_return": {
"value": null,
"rating": "",
"comment": ""
},
"total_trades": {
"value": null,
"rating": "",
"comment": ""
}
},
"overfitting_assessment": {
"risk_level": "",
"freedom_ratio": null,
"oos_decay": null,
"parameter_sensitivity": "",
"detail": ""
},
"cost_impact": {
"pre_cost_return": null,
"post_cost_return": null,
"erosion_ratio": null,
"double_cost_still_profitable": null,
"detail": ""
},
"revenue_source_analysis": {
"top5_concentration": null,
"direction_balance": "",
"regime_dependency": "",
"detail": ""
},
"market_regime_analysis": {
"trend_performance": "",
"ranging_performance": "",
"crash_performance": "",
"detail": ""
},
"tradability_assessment": {
"liquidity_ok": null,
"latency_sensitive": null,
"funding_rate_impact": "",
"detail": ""
},
"score": null,
"score_breakdown": {
"sharpe_score": null,
"drawdown_score": null,
"expectancy_score": null,
"calmar_score": null,
"sortino_score": null,
"cost_score": null,
"liquidation_score": null
},
"required_actions": [],
"recommended_next_step": "",
"metadata": {
"review_checklist_version": "v1.0",
"metrics_guide_version": "v1.0",
"notes": ""
}
}
成本模型参考
目录
---
手续费模型
中心化交易所(CEX)
Binance 永续合约
| VIP 等级 | Maker | Taker | 30 天交易量要求 |
|---|---|---|---|
| VIP 0 (默认) | 0.0200% | 0.0500% | < $5M |
| VIP 1 | 0.0160% | 0.0400% | ≥ $5M |
| VIP 2 | 0.0140% | 0.0350% | ≥ $25M |
| VIP 3 | 0.0120% | 0.0320% | ≥ $100M |
| VIP 4 | 0.0100% | 0.0300% | ≥ $200M |
回测默认使用 VIP 0: Maker 0.02%, Taker 0.05%
Binance 现货
| 等级 | Maker/Taker |
|---|---|
| 默认 | 0.1% / 0.1% |
| BNB 折扣 | 0.075% / 0.075% |
OKX 永续合约
| 等级 | Maker | Taker |
|---|---|---|
| Lv 1 (默认) | 0.0200% | 0.0500% |
| Lv 2 | 0.0160% | 0.0400% |
| Lv 3 | 0.0140% | 0.0350% |
Bybit 永续合约
| 等级 | Maker | Taker |
|---|---|---|
| Regular | 0.0200% | 0.0550% |
| VIP 1 | 0.0180% | 0.0400% |
手续费在回测中的影响
示例: $100,000 仓位, Taker 开仓 + Taker 平仓
单次来回手续费 = $100,000 × 0.05% × 2 = $100
一年 200 次交易 = $100 × 200 = $20,000 (初始资金 20%)
→ 高频策略必须精确建模手续费,否则回测会严重高估收益---
滑点模型
固定滑点默认值
| 资产类型 | 建议滑点 (bps) | 说明 |
|---|---|---|
| BTC 永续 | 2 | 流动性极好,深度大 |
| ETH 永续 | 2 | 流动性极好 |
| 主流币永续 (SOL/BNB/XRP/DOGE) | 5 | 流动性好 |
| 中等市值币永续 | 10 | 流动性一般 |
| 小市值币永续 | 15-20 | 流动性差,价格冲击大 |
| BTC/ETH 现货 | 3 | 比永续略差 |
| 其他现货 | 5-10 | 取决于交易对深度 |
滑点计算
买入: 实际成交价 = 信号价 × (1 + slippage_bps / 10000)
卖出: 实际成交价 = 信号价 × (1 - slippage_bps / 10000)
示例: BTC $60,000, 滑点 2 bps
买入成交价 = $60,000 × 1.0002 = $60,012
卖出成交价 = $60,000 × 0.9998 = $59,988
单次滑点成本 = $12 (每 BTC)大额订单滑点调整
当单笔订单量较大时,建议增加滑点:
| 订单占 bar 成交量比例 | 滑点调整 |
|---|---|
| < 1% | 基础滑点 |
| 1% - 5% | 基础滑点 × 1.5 |
| 5% - 10% | 基础滑点 × 2 |
| > 10% | 基础滑点 × 3(或拆单) |
---
资金费率成本
典型资金费率范围
| 市场环境 | 资金费率范围 | 说明 |
|---|---|---|
| 中性市场 | -0.005% ~ +0.01% | 大部分时间 |
| 牛市 | +0.01% ~ +0.1% | 多头支付空头 |
| 极端牛市 | +0.1% ~ +0.3% | 多头成本极高 |
| 熊市 | -0.01% ~ -0.05% | 空头支付多头 |
| 极端恐慌 | -0.1% ~ -0.5% | 空头成本极高 |
年化资金费率成本
假设平均资金费率 0.01%:
每 8 小时一次: 每日 0.03%
年化: 0.03% × 365 = 10.95%
→ 10x 杠杆做多一年,资金费率成本约 10.95%
→ 必须在回测中体现,否则高杠杆做多策略的收益会被严重高估资金费率套利收益估算
策略: 做空永续 + 做多现货 (delta 中性)
假设平均资金费率 0.01%:
永续空头收到: 每日 0.03%
年化: ~11%
扣除现货交易成本: ~10% 净收益
风险: 负费率期间亏损、价格极端波动导致永续端爆仓---
DeFi Gas 费用
以太坊主网
| 操作 | Gas 消耗 | 费用 (50 Gwei) |
|---|---|---|
| ERC20 Transfer | ~65,000 | ~$5 |
| Uniswap Swap | ~150,000 | ~$12 |
| 复杂合约交互 | ~300,000 | ~$24 |
L2 网络
| 网络 | 典型 Swap 费用 |
|---|---|
| Arbitrum | $0.1 - $0.5 |
| Optimism | $0.1 - $0.5 |
| Base | $0.01 - $0.1 |
| zkSync | $0.05 - $0.3 |
DEX 交易费
| DEX | 常见费率 |
|---|---|
| Uniswap V3 | 0.01% / 0.05% / 0.3% / 1% (按池子) |
| Curve | 0.04% (稳定币) |
| SushiSwap | 0.3% |
| PancakeSwap | 0.25% |
DeFi 策略回测时需同时考虑 Gas 费 + DEX 交易费
---
成本对策略的影响
成本敏感度分析建议
在回测报告中建议包含以下敏感度分析:
| 测试项 | 方法 |
|---|---|
| 手续费敏感度 | 分别用 0.03%, 0.05%, 0.07% 测试 |
| 滑点敏感度 | 分别用 1x, 2x, 3x 默认滑点测试 |
| 资金费率影响 | 对比启用/禁用资金费率的结果 |
不同策略类型的成本占比参考
| 策略类型 | 年换手率 | 手续费占比 | 滑点占比 | 资金费率占比 |
|---|---|---|---|---|
| 低频趋势跟踪 | 10-50 次 | 低 | 低 | 中(长期持仓) |
| 日内动量 | 200-500 次 | 高 | 中 | 低 |
| 资金费率套利 | 2-10 次 | 低 | 低 | 核心收入 |
| 网格交易 | 500-2000 次 | 极高 | 高 | 中 |
| 均值回归 | 50-200 次 | 中 | 中 | 低 |
法则: 如果手续费+滑点成本 > 策略毛利润的 50%,该策略可能不可持续
数据源 API 详细规格
目录
- Binance Futures(永续合约)
- Binance Spot(现货)
- Hyperliquid(永续合约)
- CoinGecko(代币价格)
- Yahoo Finance(美股/大宗商品/贵金属)
- DeFi Llama(协议 TVL/手续费)
- 限流与注意事项
- 代理配置
---
Binance Futures(永续合约)
Base URL: https://fapi.binance.com
全部为公开端点,无需 API Key。
GET /fapi/v1/klines — 永续合约 K 线
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| symbol | string | 是 | Binance 格式,如 BTCUSDT |
| interval | string | 是 | 1m / 5m / 15m / 1h / 4h / 1d |
| startTime | long | 否 | 毫秒时间戳 |
| endTime | long | 否 | 毫秒时间戳 |
| limit | int | 否 | 默认 500,最大 1500 |
返回格式(数组,每条):
[open_time, open, high, low, close, volume, close_time,
quote_volume, trades, taker_buy_volume, taker_buy_quote_volume, ignore]| 属性 | 值 |
|---|---|
| 历史深度 | 无限制(合约上线以来全部数据) |
| 单次最多 | 1500 条 |
| 分页方式 | 设置 startTime = 上一批最后一条的 open_time + 1 |
| 限流 | IP 2400 次/分钟 |
GET /fapi/v1/fundingRate — 资金费率历史
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| symbol | string | 否 | 如 BTCUSDT |
| startTime | long | 否 | 毫秒时间戳 |
| endTime | long | 否 | 毫秒时间戳 |
| limit | int | 否 | 默认 100,最大 1000 |
返回格式:
{
"symbol": "BTCUSDT",
"fundingRate": "0.00010000",
"fundingTime": 1700000000000,
"markPrice": "43250.5"
}| 属性 | 值 |
|---|---|
| 频率 | 每 8 小时一条(00:00, 08:00, 16:00 UTC) |
| 历史深度 | 上线以来全部 |
| 单次最多 | 1000 条 |
| 分页方式 | 设置 startTime = 上一批最后一条的 fundingTime + 1 |
| 限流 | 500 次 / 5 分钟(与其他 funding 端点共享) |
GET /fapi/v1/openInterest — 当前持仓量快照
| 参数 | 类型 | 必填 |
|---|---|---|
| symbol | string | 是 |
| 属性 | 值 |
|---|---|
| 返回 | 仅当前快照,无历史 |
| 限流 | 权重 1 |
GET /futures/data/openInterestHist — 持仓量历史
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pair | string | 是 | 如 BTC(不含 USDT) |
| contractType | string | 是 | PERPETUAL |
| period | string | 是 | 5m / 15m / 30m / 1h / 2h / 4h / 6h / 12h / 1d |
| limit | int | 否 | 默认 30,最大 500 |
| 属性 | 值 |
|---|---|
| ⚠️ 历史深度 | 仅最近 30 天 |
| 返回字段 | sumOpenInterest(张)、sumOpenInterestValue(USDT) |
| 限流 | 1000 次 / 5 分钟 |
GET /futures/data/topLongShortPositionRatio — Top Trader 多空比
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| symbol | string | 是 | 如 BTCUSDT |
| period | string | 是 | 5m / 15m / 30m / 1h / 2h / 4h / 6h / 12h / 1d |
| limit | int | 否 | 默认 30,最大 500 |
| 属性 | 值 |
|---|---|
| ⚠️ 历史深度 | 仅最近 30 天 |
| 样本 | 持仓保证金余额 Top 20% 的用户 |
| 限流 | 1000 次 / 5 分钟 |
GET /fapi/v1/premiumIndex — 标记价格 + 资金费率
| 参数 | 类型 | 必填 |
|---|---|---|
| symbol | string | 否 |
返回: markPrice、indexPrice、lastFundingRate、nextFundingTime
GET /fapi/v1/exchangeInfo — 合约规格
- 无需参数,返回所有合约的完整规格
- 包含: 合约类型、最小下单量、价格精度、维持保证金率
- 解析后字段:
tick_size,min_qty,max_qty,step_size,maintenance_margin_rate
---
Binance Spot(现货)
Base URL: https://api.binance.com
GET /api/v3/klines — 现货 K 线
参数和返回格式与 Futures K 线完全相同。
| 属性 | 值 |
|---|---|
| 历史深度 | 无限制 |
| 限流 | IP 6000 次/分钟 |
---
Hyperliquid(永续合约)
Base URL: https://api.hyperliquid.xyz
全部为公开端点,无需 API Key。所有请求使用 POST `/info`。
POST /info — K 线 (candleSnapshot)
请求体:
{
"type": "candleSnapshot",
"req": {
"coin": "BTC",
"interval": "1h",
"startTime": 1700000000000,
"endTime": 1710000000000
}
}| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| coin | string | 是 | 资产名称,如 BTC、ETH、SOL(不含 USDT) |
| interval | string | 是 | 1m / 3m / 5m / 15m / 30m / 1h / 2h / 4h / 8h / 12h / 1d / 3d / 1w / 1M |
| startTime | long | 是 | 毫秒时间戳(含) |
| endTime | long | 否 | 毫秒时间戳(含),默认当前时间 |
返回格式(数组,每条):
{
"t": 1700000000000,
"T": 1700003600000,
"s": "BTC",
"i": "1h",
"o": "43250.5",
"c": "43300.0",
"h": "43350.0",
"l": "43200.0",
"v": "1234.56",
"n": 5678
}| 属性 | 值 |
|---|---|
| 历史深度 | 最近 5000 根(单次),自动分页可拉更多 |
| 分页方式 | 设置 startTime = 上一批最后一条的 T + 1 |
| coin 格式 | 只需资产名(BTC),不需要 USDT 后缀 |
POST /info — 资金费率 (fundingHistory)
请求体:
{
"type": "fundingHistory",
"coin": "BTC",
"startTime": 1700000000000,
"endTime": 1710000000000
}| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| coin | string | 是 | 资产名称 |
| startTime | long | 是 | 毫秒时间戳 |
| endTime | long | 否 | 默认当前时间 |
返回格式:
{
"time": 1700000000000,
"coin": "BTC",
"fundingRate": "0.00010000",
"premium": "0.00005000"
}| 属性 | 值 |
|---|---|
| 频率 | 每 8 小时一条 |
| 历史深度 | 合约上线以来全部 |
POST /info — 合约列表 (meta)
请求体: {"type": "meta"}
返回 universe 数组,包含所有可交易的永续合约信息(name, szDecimals 等)。
---
CoinGecko(代币价格)
Base URL: https://api.coingecko.com/api/v3
GET /coins/{id}/market_chart — 代币价格历史
| 参数 | 类型 | 说明 |
|---|---|---|
| vs_currency | string | usd |
| days | int | 历史天数 |
| interval | string | daily |
已配置的代币 ID 映射:
| 代币 | CoinGecko ID | 说明 |
|---|---|---|
| PAXG | pax-gold | 黄金锚定代币 |
| XAUT | tether-gold | Tether 黄金代币 |
| OUSG | ondo-us-government-bond-fund | 美债基金代币 |
| OMMF | ondo-us-dollar-yield | 美元收益代币 |
| 属性 | 值 |
|---|---|
| 免费版限流 | 10-30 次/分钟 |
| 免费版日线 | 最多 365 天 |
| 返回字段 | prices、total_volumes、market_caps |
---
Yahoo Finance(美股/大宗商品/贵金属)
数据源: yfinance Python 包。免费,无需 API Key。
美股 K 线 — get_stock_klines()
| 我方 Symbol | yfinance Ticker | 说明 |
|---|---|---|
| RWA:AAPL | AAPL | 苹果 |
| RWA:NVDA | NVDA | 英伟达 |
| RWA:TSLA | TSLA | 特斯拉 |
| RWA:MSFT | MSFT | 微软 |
| RWA:GOOGL | GOOGL | 谷歌 |
| RWA:AMZN | AMZN | 亚马逊 |
| RWA:META | META | Meta |
| RWA:SPY | SPY | 标普 500 ETF |
| RWA:QQQ | QQQ | 纳斯达克 100 ETF |
- 日线历史: 30+ 年
- 包含股息数据
大宗商品期货 — get_commodity_klines()
| 我方 Symbol | yfinance Ticker | 说明 |
|---|---|---|
| COMM:WTI | CL=F | WTI 原油期货 |
| COMM:BRENT | BZ=F | 布伦特原油期货 |
| COMM:NG | NG=F | 天然气期货 |
| COMM:COPPER | HG=F | 铜期货 |
- 历史深度: 10+ 年
贵金属现货 — get_metal_spot_klines()
| 我方 Symbol | yfinance Ticker | 说明 |
|---|---|---|
| METAL:XAU-SPOT | GC=F | 黄金期货(代理现货) |
| METAL:XAG-SPOT | SI=F | 白银期货(代理现货) |
- 历史深度: 10+ 年
- 注意: 实际用期货价格代理现货,与伦敦定盘价有微小差异
---
DeFi Llama(协议 TVL/手续费)
Base URL: https://api.llama.fi
免费端点,无需 API Key。
GET /protocol/{name} — 协议 TVL 历史 + 基本信息
返回从协议上线至今的每日 TVL,以及协议基本信息(类别、支持的链等)。
支持的协议(slug): aave / compound-v3 / lido / curve-dex / uniswap / makerdao / rocket-pool / convex-finance 等
GET /fees/{protocol} — 协议手续费/收入
返回协议的手续费和收入数据(24h / 7d / 30d)。
GET /overview/fees — 所有协议手续费概览
返回所有协议的 24h / 7d / 30d 手续费和收入汇总。
GET /protocols — 协议列表
返回所有 DeFi 协议基本信息 + 当前 TVL。
---
限流与注意事项
| API | 限流 | 建议 |
|---|---|---|
| Binance Futures K 线 | 2400 次/分钟 | 分页拉取时加 100ms 间隔 |
| Binance Futures 资金费率 | 500 次/5 分钟(共享) | 大量拉取时控速 |
| Binance Futures 数据统计 | 1000 次/5 分钟 | openInterestHist / longShortRatio |
| Binance Spot | 6000 次/分钟 | 宽松 |
| Hyperliquid | 无硬限制 | 建议分页间隔 200ms,单次最多 5000 根 |
| CoinGecko 免费 | 10-30 次/分钟 | 严格,建议加缓存 |
| yfinance | 无硬限制 | Yahoo 可能临时封 IP,建议加间隔 |
| DeFi Llama 免费 | 无硬限制 | 偶尔慢,建议缓存 |
分页注意事项
- Binance K 线: 单次最多 1500 条。
data_client.py已实现自动分页,设置startTime = 上一批 open_time + 1。 - Binance 资金费率: 单次最多 1000 条。自动分页,设置
startTime = 上一批 fundingTime + 1。 - Hyperliquid K 线: 单次最多 5000 根。自动分页,设置
startTime = 上一批 T + 1。 - 其他端点: 无需分页或仅返回快照。
---
代理配置
国内访问 Binance API 需配置代理。设置环境变量:
export PROXY_URL="http://127.0.0.1:7890"data_client.py 初始化时自动读取 PROXY_URL 环境变量并应用到所有 HTTP 请求。
回测引擎规格 — 永续合约专项
目录
---
核心计算公式
保证金计算
初始保证金 (Initial Margin) = 仓位名义价值 / 杠杆倍数
维持保证金 (Maintenance Margin) = 仓位名义价值 × 维持保证金率
仓位名义价值 (Notional Value) = 数量 × 标记价格盈亏计算
未实现盈亏(多单) = 数量 × (标记价格 - 开仓均价)
未实现盈亏(空单) = 数量 × (开仓均价 - 标记价格)保证金率
保证金率 = (保证金余额 + 未实现盈亏) / 仓位名义价值强平价格
强平价格(多单/逐仓) = 开仓均价 × (1 - 1/杠杆 + 维持保证金率)
强平价格(空单/逐仓) = 开仓均价 × (1 + 1/杠杆 - 维持保证金率)费用计算
资金费用 = 仓位名义价值 × 资金费率
开仓手续费 = 仓位名义价值 × 手续费率
滑点成本 = 成交价 × 滑点(bps) / 10000 × 数量---
保证金模式
逐仓(Isolated)
- 每个仓位独立分配保证金
- 强平仅影响当前仓位,其他仓位不受波及
- 最大亏损 = 该仓位分配的保证金
- 适用场景: 多币种分散持仓、风险隔离
全仓(Cross)
- 所有仓位共享账户可用余额作为保证金
- 某仓位亏损可由其他仓位的未实现盈利补充
- 强平阈值更低,但一旦触发影响全账户
- 适用场景: 对冲策略、同方向重仓
---
资金费率结算
结算规则
| 项目 | 规则 |
|---|---|
| 结算周期 | 每 8 小时(00:00, 08:00, 16:00 UTC) |
| 方向 | funding_rate > 0:多头支付空头;funding_rate < 0:空头支付多头 |
| 计算 | funding_fee = position_value × funding_rate |
| 对账户影响 | 直接从/加到保证金余额 |
| 数据要求 | 必须使用真实历史资金费率数据,不允许模拟/估算 |
结算逻辑
def settle_funding(position, funding_rate, mark_price):
"""在 00:00/08:00/16:00 UTC 时刻调用"""
position_value = position.quantity * mark_price
funding_fee = position_value * funding_rate
if position.side == "long":
# 正费率:多头支付;负费率:多头收到
position.margin -= funding_fee
account.balance -= funding_fee
else:
# 正费率:空头收到;负费率:空头支付
position.margin += funding_fee
account.balance += funding_fee资金费率成本估算
BTC 多单,仓位价值 $60,000,funding_rate = 0.01%
单次结算: $60,000 × 0.0001 = $6.00(多头支付)
一天 3 次: ≈ $18
一年: ≈ $6,570(不可忽略的成本!)---
强制平仓逻辑
触发条件
保证金率 ≤ 维持保证金率 → 触发强制平仓逐仓模式
1. 每个 bar 检查每个仓位的保证金率 2. 保证金率 ≤ 维持保证金率 → 该仓位被强平 3. 其他仓位不受影响 4. 强平以当前 bar 的标记价格成交 5. 已实现亏损 ≈ 初始保证金
全仓模式
1. 计算账户整体保证金率: (全部余额 + 全部未实现盈亏) / 全部仓位名义价值 2. 整体保证金率不足 → 按亏损最大仓位优先平仓 3. 平仓后重新计算,若仍不足则继续平下一个
---
持仓操作
| 操作 | 引擎方法 | 说明 |
|---|---|---|
| 开多 | engine.open_long() | 买入合约做多 |
| 开空 | engine.open_short() | 卖出合约做空 |
| 平多 | engine.close_long() | 卖出平仓 |
| 平空 | engine.close_short() | 买入平仓 |
| 设置止损 | engine.set_stop_loss() | 设定止损价 |
| 设置止盈 | engine.set_take_profit() | 设定止盈价 |
| 设置杠杆 | engine.set_leverage() | 修改杠杆倍数 |
| 设置保证金模式 | engine.set_margin_mode() | isolated / cross |
---
止损/止盈机制
止损
- 多单: 当 bar 的
low ≤ 止损价时触发,以止损价成交 - 空单: 当 bar 的
high ≥ 止损价时触发,以止损价成交 - 触发后自动平仓,不产生额外手续费(已含在平仓操作中)
止盈
- 多单: 当 bar 的
high ≥ 止盈价时触发,以止盈价成交 - 空单: 当 bar 的
low ≤ 止盈价时触发,以止盈价成交
检查顺序
每个 bar 内按以下顺序执行: 1. 更新未实现盈亏 2. 资金费率结算(如果是结算时刻) 3. 止损/止盈检查 4. 强平检查 5. 记录净值
---
滑点模型
固定滑点(默认)
买入成交价 = 信号价 × (1 + slippage_bps / 10000)
卖出成交价 = 信号价 × (1 - slippage_bps / 10000)| 资产类型 | 建议默认滑点 |
|---|---|
| BTC/ETH 永续 | 2 bps |
| 其他主流币永续 | 5 bps |
| 小币种永续 | 10-20 bps |
---
手续费模型
| 资产类型 | Maker | Taker |
|---|---|---|
| 永续合约 | 0.02% | 0.05% |
| 加密现货 | 0.1% | 0.1% |
回测引擎默认使用 Taker 费率(市价单),Maker 费率用于限价单。
---
绩效指标输出
engine.get_result() 返回以下指标:
| 指标 | 计算公式 |
|---|---|
total_return | (最终权益 / 初始资金) - 1 |
annual_return | (1 + total_return) ^ (365 / 交易天数) - 1 |
sharpe_ratio | 年化收益 / 年化波动率 (>2 优秀, 1-2 良好, <1 需优化) |
sortino_ratio | 年化收益 / 年化下行波动率 |
max_drawdown | max((peak - trough) / peak) |
max_drawdown_duration | 最大回撤持续 bar 数 |
calmar_ratio | `年化收益 / |
volatility | 日收益标准差 × √365 |
win_rate | 盈利交易数 / 总平仓交易数 |
profit_loss_ratio | 平均盈利 / 平均亏损 |
total_trades | 总交易次数 |
total_commission | 累计手续费 |
total_slippage_cost | 累计滑点成本 |
net_funding | 收到资金费 - 支付资金费 |
liquidation_count | 强平次数(>0 建议降杠杆) |
---
数值计算示例
示例 1: BTC 多单开仓 + 强平价格
场景: BTC 多单, 开仓价 $60,000, 杠杆 5x, 数量 1 BTC, 维持保证金率 0.5%
初始保证金 = $60,000 / 5 = $12,000
手续费 = $60,000 × 0.05% = $30 (Taker)
滑点成本 = $60,000 × 2/10000 = $12 (2 bps)
实际成交价 = $60,000 × (1 + 2/10000) = $60,012
强平价格 = $60,000 × (1 - 1/5 + 0.005)
= $60,000 × 0.805
= $48,300示例 2: 价格变动与保证金率
开仓后 BTC 涨到 $65,000:
未实现盈亏 = 1 × ($65,000 - $60,000) = +$5,000
保证金率 = ($12,000 + $5,000) / (1 × $65,000) = 26.2% ✓ 安全
开仓后 BTC 跌到 $49,000:
未实现盈亏 = 1 × ($49,000 - $60,000) = -$11,000
保证金率 = ($12,000 - $11,000) / (1 × $49,000) = 2.0% ✓ 仍安全
开仓后 BTC 跌到 $48,300:
未实现盈亏 = 1 × ($48,300 - $60,000) = -$11,700
保证金率 = ($12,000 - $11,700) / (1 × $48,300) = 0.62% → ≈ 维持保证金率 → 触发强平示例 3: ETH 空单 + 资金费率
场景: ETH 空单, 开仓价 $3,000, 杠杆 10x, 数量 10 ETH
仓位价值 = 10 × $3,000 = $30,000
初始保证金 = $30,000 / 10 = $3,000
手续费 = $30,000 × 0.05% = $15
强平价格(空) = $3,000 × (1 + 1/10 - 0.005) = $3,000 × 1.095 = $3,285
每次资金费率结算 (假设 funding_rate = 0.01%):
费用 = $30,000 × 0.0001 = $3.00
空头收到 $3.00(正费率 → 多付空收)
ETH 跌到 $2,700:
未实现盈亏 = 10 × ($3,000 - $2,700) = +$3,000
收益率 = $3,000 / $3,000 (初始保证金) = 100% (10x 杠杆放大)绩效指标解读指南
本文档为 backtest-reviewer Skill 提供所有关键绩效指标的解读标准、评级阈值和使用注意事项。
---
1. Sharpe Ratio(夏普比率)
定义
衡量单位风险的超额收益。计算方式:(策略年化收益 - 无风险利率) / 策略年化波动率。
评级标准
| 评级 | 范围 | 说明 |
|---|---|---|
| ✅ 优秀 | > 2.0 | 策略风险调整后收益优异,适合部署 |
| ✅ 良好 | 1.5 - 2.0 | 策略表现稳健,可考虑通过 |
| ⚠️ 一般 | 1.0 - 1.5 | 有正收益但风险效率一般,需结合其他指标判断 |
| ❌ 不合格 | < 1.0 | 风险调整后收益不足,不建议通过 |
注意事项
- 加密货币市场无风险利率通常取 USDT 理财利率(约 3%-5%)或取 0
- Sharpe > 3.0 应引起警觉——可能是过拟合或回测偏差
- 短回测周期(< 6 个月)的 Sharpe 统计意义不足
- 年化 Sharpe 与日频/周频 Sharpe 需注意换算(日频 × √252,周频 × √52)
---
2. Max Drawdown(最大回撤)
定义
从历史最高点到最低点的最大跌幅。反映策略最坏情况下的损失。
评级标准
| 评级 | 范围 | 说明 |
|---|---|---|
| ✅ 低风险 | < 10% | 回撤控制优秀,适合低风险偏好 |
| ✅ 可接受 | 10% - 20% | 回撤在合理范围内 |
| ⚠️ 高风险 | 20% - 30% | 回撤较大,需确认用户风险容忍度 |
| ❌ 不可接受 | > 30% | 回撤过大,实盘中可能导致心理崩溃或爆仓 |
注意事项
- 永续合约策略需考虑杠杆放大效应:3 倍杠杆下 10% 的标的回撤 = 30% 账户回撤
- 最大回撤持续时间同样重要——回撤 15% 但 3 天恢复 vs 15% 但 3 个月未恢复,风险等级完全不同
- 实盘中的最大回撤通常比回测大 30%-50%(流动性冲击、延迟等因素)
- 应设置预期:如果回测最大回撤为 15%,实盘应准备承受 20%-25%
---
3. Win Rate(胜率)与 Profit Factor(盈亏比)
定义
- 胜率:盈利交易次数 / 总交易次数
- 盈亏比(Profit Factor):总盈利金额 / 总亏损金额
联合评级
胜率和盈亏比必须联合判断,单独看任何一个都没有意义。
| 胜率 | 盈亏比 | 评级 | 说明 |
|---|---|---|---|
| > 50% | > 1.5 | ✅ 优秀 | 既有较高胜率又有良好的盈亏比 |
| > 50% | 1.0 - 1.5 | ⚠️ 一般 | 胜率高但单笔盈利不够大 |
| 30% - 50% | > 2.0 | ✅ 良好 | 低胜率但高盈亏比(趋势策略典型特征) |
| 30% - 50% | 1.5 - 2.0 | ⚠️ 需观察 | 处于边界,需结合 Sharpe 判断 |
| < 30% | > 3.0 | ⚠️ 特殊 | 极低胜率高盈亏比(需确认交易数量是否足够) |
| < 30% | < 2.0 | ❌ 不合格 | 低胜率低盈亏比,策略无正期望值 |
| > 70% | < 1.2 | ⚠️ 警惕 | 高胜率低盈亏比可能隐藏尾部风险 |
正期望值公式
期望值 = 胜率 × 平均盈利 - (1 - 胜率) × 平均亏损期望值必须为正,否则策略长期必亏。
注意事项
- 胜率 > 80% 应引起警觉——可能是止盈太紧、止损太宽,积累尾部风险
- Profit Factor < 1.0 意味着总亏损 > 总盈利,策略必然亏钱
- 趋势策略的胜率通常在 35%-45%,这是正常的
- 均值回归策略的胜率通常在 55%-65%
---
4. Calmar Ratio(卡尔玛比率)
定义
年化收益率 / 最大回撤。衡量每承受 1% 回撤所获得的年化收益。
评级标准
| 评级 | 范围 | 说明 |
|---|---|---|
| ✅ 优秀 | > 3.0 | 收益远超所承受的回撤风险 |
| ✅ 良好 | 2.0 - 3.0 | 收益与风险比例合理 |
| ⚠️ 一般 | 1.0 - 2.0 | 收益对风险的补偿勉强足够 |
| ❌ 不合格 | < 1.0 | 承受的回撤大于获得的收益,性价比差 |
注意事项
- Calmar 对最大回撤极其敏感——单次黑天鹅就能拉低 Calmar
- 与 Sharpe 互补:Sharpe 看波动率风险,Calmar 看尾部风险
- 建议同时关注"实现 Calmar"(扣除成本后的 Calmar)
---
5. Sortino Ratio(索提诺比率)
定义
类似 Sharpe,但只用下行波动率(亏损的波动率)代替总波动率。对上行波动不做惩罚。
评级标准
| 评级 | 范围 | 说明 |
|---|---|---|
| ✅ 优秀 | > 3.0 | 下行风险控制优秀 |
| ✅ 良好 | 2.0 - 3.0 | 下行风险在可控范围 |
| ⚠️ 一般 | 1.0 - 2.0 | 下行风险一般 |
| ❌ 不合格 | < 1.0 | 下行风险过大 |
与 Sharpe 的关系
- Sortino > Sharpe:说明策略的波动主要来自上行(好事),下行风险控制好
- Sortino ≈ Sharpe:上下行波动对称
- Sortino < Sharpe:说明下行波动大于上行波动,策略有尾部风险
---
6. 资金费率盈亏解读
适用范围
仅适用于永续合约策略。资金费率每 8 小时结算一次(部分交易所为 4 小时)。
解读
| 场景 | 影响 | 说明 |
|---|---|---|
| 做多 + 资金费率为正 | 💸 支出 | 多头付费给空头,增加持仓成本 |
| 做多 + 资金费率为负 | 💰 收入 | 空头付费给多头,降低持仓成本 |
| 做空 + 资金费率为正 | 💰 收入 | 多头付费给空头,获得额外收入 |
| 做空 + 资金费率为负 | 💸 支出 | 空头付费给多头,增加持仓成本 |
评级标准
| 检查项 | pass | warning | fail |
|---|---|---|---|
| 资金费率对总 P&L 的影响占比 | < 10% | 10% - 25% | > 25% |
| 资金费率是收入还是成本 | 净收入 | 净成本但可控 | 净成本且侵蚀收益严重 |
注意事项
- 资金费率套利策略中,资金费率收入是核心收益来源,应单独评估其稳定性
- 高杠杆多头策略在牛市中可能因持续正资金费率而被侵蚀大量利润
- 回测中的资金费率数据应使用历史真实数据,不可用固定值估算
---
7. 手续费/滑点影响比
定义
交易成本(手续费 + 滑点)占总交易收入的比例。
评级标准
| 评级 | 手续费占比 | 滑点占比 | 总成本占比 |
|---|---|---|---|
| ✅ 可忽略 | < 5% | < 3% | < 8% |
| ✅ 可接受 | 5% - 10% | 3% - 5% | 8% - 15% |
| ⚠️ 偏高 | 10% - 20% | 5% - 10% | 15% - 30% |
| ❌ 不可承受 | > 20% | > 10% | > 30% |
计算方法
手续费影响比 = 总手续费支出 / 总盈利(不含手续费)
滑点影响比 = 总滑点成本 / 总盈利(不含滑点)
总成本影响比 = (总手续费 + 总滑点) / 总盈利(不含成本)注意事项
- 高频策略(周期 < 15 分钟)的成本影响通常更大
- 币安永续合约 taker 费率 0.04%,maker 费率 0.02%
- 小币种滑点可能远高于回测假设值
---
8. 爆仓次数影响
定义
回测期间触及或接近强平价格的次数。
评级标准
| 评级 | 爆仓/接近爆仓次数 | 说明 |
|---|---|---|
| ✅ 安全 | 0 次 | 从未接近爆仓线 |
| ⚠️ 需关注 | 1-2 次接近(差 5% 以内) | 极端行情下险些爆仓 |
| ❌ 危险 | 实际爆仓 ≥ 1 次 | 策略存在致命缺陷 |
注意事项
- 逐仓模式下爆仓 = 损失该仓位全部保证金
- 全仓模式下爆仓 = 损失全部账户资金
- 回测中如果出现爆仓,应该直接驳回(rejected),除非用户显式降低杠杆后重新回测
- 即使未爆仓,如果最大浮亏曾接近强平价格(保证金占用率 > 80%),也应给出 warning
---
综合评分模型
权重分配
| 指标 | 权重 | 说明 |
|---|---|---|
| Sharpe Ratio | 25% | 核心风险调整收益指标 |
| Max Drawdown | 20% | 尾部风险指标 |
| 胜率 × 盈亏比 | 15% | 正期望值验证 |
| Calmar Ratio | 10% | 收益/回撤效率 |
| Sortino Ratio | 10% | 下行风险 |
| 成本影响比 | 10% | 真实可行性 |
| 爆仓风险 | 10% | 致命风险一票否决 |
综合评分计算
每个指标按 0-100 分映射,乘以权重加总。
| 总分 | 对应决策 |
|---|---|
| ≥ 75 | approved |
| 55 - 74 | paper_trade_first |
| < 55 | rejected |
一票否决规则
以下任一条件触发即直接 rejected,无论综合评分多高:
- 回测期间发生过爆仓
- OOS Sharpe < 0(样本外亏损)
- Profit Factor < 1.0(总亏损 > 总盈利)
- 成本后总收益为负
回测评审检查清单
本清单是 backtest-reviewer Skill 的核心参考文档,覆盖回测评审的 10 个维度。每个维度包含 pass/warning/fail 判定标准和示例说明。
---
1. 收益来源分析
检查目标:确认收益来自策略逻辑而非运气或极端行情。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 收益集中度 | 计算 Top-5 交易利润占总利润比例 | < 30% | 30%-50% | > 50% |
| 行情依赖 | 剔除最大行情段后是否仍盈利 | 仍盈利 | 盈利大幅下降 | 变亏损 |
| 方向偏差 | 多空收益是否均衡 | 多空均盈利 | 一方显著弱于另一方 | 单方向亏损 |
示例说明
Pass 示例:
总利润 $12,000,Top-5 交易贡献 $2,800(23.3%)。多头盈利 $6,500,空头盈利 $5,500。收益分布均匀,不依赖极端行情。
Warning 示例:
总利润 $10,000,Top-5 交易贡献 $4,200(42%)。收益主要来自 2024-03 的单边上涨行情段。虽仍盈利,但需关注极端行情贡献过高。
Fail 示例:
总利润 $8,000,Top-1 交易贡献 $5,500(68.7%)。剔除该笔交易后总收益仅 $2,500。策略可能只是"碰巧"在某次大行情中站对了方向。
---
2. 成本后表现
检查目标:验证策略在考虑真实交易成本后是否仍可行。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 成本侵蚀比 | 成本前后收益差 / 成本前收益 | < 20% | 20%-40% | > 40% |
| 成本翻倍测试 | 手续费/滑点翻倍后是否盈利 | 仍盈利且 Sharpe > 1 | 勉强盈利 | 变亏损 |
| 单笔成本占比 | 平均单笔手续费 / 平均单笔利润 | < 15% | 15%-30% | > 30% |
示例说明
Pass 示例:
成本前年化 45%,成本后年化 38%。成本侵蚀比 15.6%。手续费翻倍后年化仍有 31%,Sharpe 从 2.1 降至 1.6。策略对成本不敏感。
Warning 示例:
成本前年化 30%,成本后年化 20%。成本侵蚀比 33.3%。策略换手率偏高(年换手 120 次),成本影响显著但尚可接受。
Fail 示例:
成本前年化 18%,成本后年化 3%。成本侵蚀比 83.3%。手续费翻倍后直接亏损。策略盈利几乎全部被交易成本吃掉。
---
3. 参数敏感性
检查目标:确认策略不依赖精心挑选的"最优参数"。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 参数邻域稳定性 | 参数 ±20% 范围内是否仍盈利 | 邻域内 >80% 组合盈利 | 50%-80% 组合盈利 | < 50% 组合盈利 |
| 参数高原 | 是否存在宽阔的盈利参数带 | 宽高原(多组参数表现接近) | 窄高原 | 尖峰(仅极少参数盈利) |
| 参数交互 | 多参数联合变化是否稳定 | 联合变化后表现稳定 | 部分交互导致不稳定 | 参数间强交互,微调即崩溃 |
示例说明
Pass 示例:
SMA 快线 15-25、慢线 50-70 范围内均盈利,Sharpe 在 1.5-2.3 之间波动。参数高原宽阔,策略对参数选择不敏感。
Warning 示例:
快线 18-22 盈利,超出此范围盈利能力显著下降。参数带较窄但仍有一定容错空间。
Fail 示例:
仅 SMA(20, 60) 组合盈利,改为 SMA(19, 60) 或 SMA(21, 60) 即转亏。典型的参数过拟合"尖峰"现象。
---
4. 样本内外一致性
检查目标:验证策略不是对历史数据的"记忆"。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| OOS 收益衰减 | (IS 收益 - OOS 收益) / IS 收益 | < 30% | 30%-50% | > 50% |
| OOS Sharpe | 样本外 Sharpe | > 1.0 | 0.5-1.0 | < 0.5 |
| Walk-forward 一致性 | 各窗口表现的变异系数 | CV < 0.3 | 0.3-0.5 | > 0.5 |
示例说明
Pass 示例:
IS 期间 Sharpe 2.1,OOS 期间 Sharpe 1.7。衰减率 19%。Walk-forward 5 个窗口中 4 个盈利,表现一致性高。
Warning 示例:
IS 期间 Sharpe 2.5,OOS 期间 Sharpe 1.4。衰减率 44%。OOS 仍盈利但表现下降明显,需观察是否有市场结构变化。
Fail 示例:
IS 期间 Sharpe 3.2,OOS 期间 Sharpe 0.3。衰减率 90.6%。Walk-forward 5 个窗口中仅 1 个盈利。严重过拟合。
---
5. 过拟合检测
检查目标:从多个角度评估策略是否过度拟合历史数据。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 自由度比 | 可调参数数 / 独立交易数 | < 0.05 | 0.05-0.1 | > 0.1 |
| 规则复杂度 | 入场+出场条件总数 | ≤ 5 | 6-8 | > 8 |
| 数据长度充分性 | 回测 K 线数 / 参数数 | > 500 | 200-500 | < 200 |
示例说明
Pass 示例:
3 个可调参数(SMA 快线、SMA 慢线、成交量倍数),180 笔交易。自由度比 = 3/180 = 0.017。入场 2 条规则 + 出场 3 条规则 = 5 条。策略简洁,过拟合风险低。
Warning 示例:
6 个可调参数(MACD 3 个 + RSI 1 个 + ADX 1 个 + EMA 1 个),80 笔交易。自由度比 = 6/80 = 0.075。参数较多但尚在可接受范围。
Fail 示例:
12 个可调参数,45 笔交易。自由度比 = 12/45 = 0.267。入场条件含 6 个子条件。严重过参数化,策略很可能是在"记忆"历史数据。
---
6. 市场状态分布
检查目标:确认策略在不同市场环境下的适应性。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 趋势行情表现 | 上涨/下跌趋势段的收益 | 盈利且符合策略逻辑 | 部分趋势段亏损 | 趋势段系统性亏损 |
| 震荡行情表现 | 无明显趋势时的收益 | 小幅盈利或微亏 | 显著亏损但可控 | 大幅亏损 |
| 暴跌行情表现 | 极端下跌期间的表现 | 及时止损或盈利 | 较大回撤但未爆仓 | 接近或触发爆仓 |
| 极端事件适应 | 黑天鹅事件期间 | 有效保护 | 损失可控 | 灾难性损失 |
示例说明
Pass 示例:
趋势行情贡献收益 70%,震荡行情微亏 -2%,暴跌行情(2024-08 闪崩)最大单日亏损 -1.5%。策略在趋势中获利,震荡中守住本金,极端行情有止损保护。
Warning 示例:
趋势行情表现优秀,但 2024-Q3 长达 2 个月的震荡期累计亏损 -12%。策略对震荡行情适应性不足。
Fail 示例:
趋势行情盈利 +30%,但 2024-08-05 ETH 暴跌日单日亏损 -25%(接近爆仓线)。策略缺乏极端行情保护机制。
---
7. 换手与持仓时间
检查目标:检查交易频率是否合理、是否可执行。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 年化换手次数 | 年交易次数 | 与策略周期匹配 | 过高或过低 | 极端值 |
| 平均持仓时间 | 所有交易的平均持仓 K 线数 | ≥ 5 根 K 线 | 3-5 根 | < 3 根 |
| 持仓分布 | 持仓时间的分布形态 | 分布合理 | 极端值过多 | 大量超短持仓 |
适用基准
| 策略周期 | 合理年化换手 | 说明 |
|---|---|---|
| 1m-5m | 500-2000 次 | 高频策略 |
| 15m-1h | 100-500 次 | 日内/短线策略 |
| 4h-1d | 30-150 次 | 波段策略 |
| 1w | 10-50 次 | 趋势策略 |
示例说明
Pass 示例:
1h 周期策略,年交易 156 次,平均持仓 18 根 K 线(约 18 小时)。交易频率与策略周期匹配。
Warning 示例:
4h 周期策略,年交易 400 次,平均持仓 3 根 K 线(12 小时)。换手频率偏高,可能导致成本过高。
Fail 示例:
1h 周期策略,年交易 3000 次,平均持仓 1.2 根 K 线。本质上是在做高频交易但使用了 1h 框架,滑点和手续费将严重侵蚀收益。
---
8. 多品种稳健性
检查目标:检验策略逻辑是否具有普适性(仅对多标的策略适用)。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 跨品种一致性 | 同策略在不同标的上的表现 | ≥ 70% 标的盈利 | 50%-70% 标的盈利 | < 50% 标的盈利 |
| 表现离散度 | 各品种 Sharpe 的标准差 | σ < 0.5 | 0.5-1.0 | > 1.0 |
| 最差品种表现 | 表现最差的标的 | 微亏或持平 | 显著亏损但可控 | 严重亏损 |
示例说明
Pass 示例:
策略在 BTC/ETH/SOL/DOGE/AVAX 5 个标的上测试,4 个盈利(80%)。Sharpe 分别为 1.8/1.5/2.1/0.3/1.2。最差品种 DOGE 仅微亏。
单标的策略说明:
如果策略仅针对单一标的设计,此项标记为"不适用"。但建议用户在相似标的上做交叉验证。
---
9. 真实可交易性
检查目标:确认策略在真实市场环境中可以执行。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 流动性支撑 | 策略交易量 vs 市场交易量 | 占市场量 < 1% | 1%-5% | > 5% |
| 订单类型可行性 | 策略所需订单类型 | 市价/限价单 | 需条件单 | 需复杂订单组合 |
| 延迟敏感性 | 信号到执行的延迟容忍 | > 10 秒 | 1-10 秒 | < 1 秒 |
| 资金费率影响 | 永续合约持仓期间的资金费率成本 | < 年化收益 10% | 10%-25% | > 25% |
示例说明
Pass 示例:
策略以 $50,000 资金交易 BTCUSDT,单笔交易约 $15,000。BTCUSDT 24h 交易量 > $30B,策略影响忽略不计。使用市价单,K 线收盘后执行,延迟不敏感。
Fail 示例:
策略交易某低市值代币,单笔交易 $10,000,该代币 24h 交易量仅 $500,000。策略交易量占市场量 2%,可能造成显著冲击成本。
---
10. 衰减风险
检查目标:评估策略的 alpha 是否具有时效性。
检查项
| 检查项 | 方法 | pass | warning | fail |
|---|---|---|---|---|
| 近期表现趋势 | 滚动 Sharpe 是否下降 | 稳定或上升 | 轻微下降 | 显著下降 |
| 同类策略拥挤度 | 策略逻辑是否已被广泛使用 | 相对独特 | 常见但有差异化 | 极度拥挤(如简单均线交叉) |
| Alpha 半衰期 | 策略有效性的持续时间估计 | > 1 年 | 6-12 个月 | < 6 个月 |
示例说明
Pass 示例:
滚动 3 月 Sharpe 在 1.2-2.5 之间波动,无明显下降趋势。策略利用资金费率异常,市场效率尚未完全消除此类机会。
Warning 示例:
滚动 Sharpe 从回测前期的 2.5 逐步下降到后期的 1.2。可能是市场微观结构变化导致 alpha 衰减。建议缩短策略有效期评估周期。
Fail 示例:
滚动 Sharpe 在回测最后 3 个月持续低于 0.5。策略使用的简单均线交叉信号已被市场充分定价,alpha 可能已基本消失。
---
综合评审矩阵
| 决策 | 核心指标 | 过拟合 | 成本影响 | 收益来源 | OOS 一致性 |
|---|---|---|---|---|---|
| approved | 全 pass 或至多 1 个 warning | low | 侵蚀 < 20% | 分散 | 衰减 < 30% |
| paper_trade_first | 多数 pass + 2-3 个 warning | medium | 侵蚀 20%-40% | 轻微集中 | 衰减 30%-50% |
| rejected | 存在 fail | high | 侵蚀 > 40% | 严重集中 | 衰减 > 50% |
"""
DEX Quant Server API 客户端 — 信号驱动架构(含 Token 认证)
Skill 端调用流程:
1. strategy-maker 生成策略脚本
2. 本地运行脚本,拿到信号列表
3. 调 run_backtest() 把信号发给 Server(自动携带 Token)
4. Server 拉 K 线(带缓存)+ 回测引擎回放信号
5. 返回绩效结果,展示给用户
认证:
- 首次使用自动注册机器码,获取 Token(免费 3 个策略配额)
- Token 缓存在 ~/.dex-quant/config.json
- 所有请求自动携带 X-Token 头
用法:
client = QuantAPIClient("http://your-server:8000")
result = client.run_backtest(
strategy_name="BTC MACD 策略",
symbol="BTCUSDT",
timeframe="1h",
start_date="2024-01-01",
end_date="2024-12-31",
signals=[...],
)
client.print_metrics(result)
"""
from __future__ import annotations
from typing import Optional
import httpx
from loguru import logger
from machine_auth import MachineAuth
DEFAULT_SERVER_URL = "https://quant.qa1.dex.hashkeydev.com"
API_PREFIX = "/api/v1"
class QuantAPIClient:
"""DEX Quant Server HTTP 客户端(自动认证)"""
def __init__(self, server_url: str = DEFAULT_SERVER_URL, timeout: float = 300.0):
self.server_url = server_url
self.base_url = server_url.rstrip("/") + API_PREFIX
self._client = httpx.Client(timeout=timeout)
self._auth = MachineAuth(server_url)
self._token = self._auth.register_or_load()
def _headers(self) -> dict:
return {"X-Token": self._token}
# ═══════════════ 回测 ═══════════════
def run_backtest(
self,
strategy_name: str,
symbol: str,
timeframe: str,
start_date: str,
end_date: str,
signals: list[dict],
strategy_id: str = "",
initial_capital: float = 100_000.0,
leverage: int = 1,
fee_rate: float = 0.0005,
slippage_bps: float = 5.0,
margin_mode: str = "isolated",
direction: str = "long_short",
) -> dict:
"""
提交信号驱动回测。
参数:
strategy_name: 策略名称
symbol: 交易对 (BTCUSDT)
timeframe: K 线周期 (15m / 1h / 2h / 1d)
start_date: 开始日期 "YYYY-MM-DD"
end_date: 结束日期 "YYYY-MM-DD"
signals: 信号列表,每个信号包含:
timestamp, symbol, action (buy/sell/close),
direction (long/short), confidence, reason,
price_at_signal, suggested_stop_loss, suggested_take_profit
返回:
BacktestResponse 字典:
backtest_id, status, metrics, trades, equity_curve, conclusion
"""
payload = {
"strategy_name": strategy_name,
"strategy_id": strategy_id,
"symbol": symbol,
"timeframe": timeframe,
"start_date": start_date,
"end_date": end_date,
"signals": signals,
"initial_capital": initial_capital,
"leverage": leverage,
"fee_rate": fee_rate,
"slippage_bps": slippage_bps,
"margin_mode": margin_mode,
"direction": direction,
}
logger.info(
"提交回测 | {} {} {} | {} → {} | {} 个信号",
strategy_name, symbol, timeframe, start_date, end_date, len(signals),
)
resp = self._client.post(f"{self.base_url}/backtest/run", json=payload, headers=self._headers())
resp.raise_for_status()
result = resp.json()
status = result.get("status", "unknown")
if status == "completed":
metrics = result.get("metrics", {})
logger.info(
"回测完成 | 收益={:.2%} | Sharpe={:.2f} | 回撤={:.2%} | "
"交易={} | 结论={}",
metrics.get("total_return_pct", 0),
metrics.get("sharpe_ratio", 0),
abs(metrics.get("max_drawdown_pct", 0)),
metrics.get("total_trades", 0),
result.get("conclusion", ""),
)
else:
logger.error("回测失败 | {}", result.get("error"))
return result
def get_backtest(self, backtest_id: str) -> dict:
"""查询已保存的回测结果"""
resp = self._client.get(f"{self.base_url}/backtest/{backtest_id}", headers=self._headers())
resp.raise_for_status()
return resp.json()
def get_trades(self, backtest_id: str) -> dict:
"""获取回测交易记录"""
resp = self._client.get(f"{self.base_url}/backtest/{backtest_id}/trades", headers=self._headers())
resp.raise_for_status()
return resp.json()
def get_equity(self, backtest_id: str) -> dict:
"""获取权益曲线"""
resp = self._client.get(f"{self.base_url}/backtest/{backtest_id}/equity", headers=self._headers())
resp.raise_for_status()
return resp.json()
# ═══════════════ 数据 ═══════════════
def get_klines(
self,
symbol: str,
interval: str,
start_date: str,
end_date: str,
exchange: str = "binance",
) -> list[dict]:
"""获取 K 线数据(Server 端带缓存,同币同周期不重复下载)"""
payload = {
"symbol": symbol,
"interval": interval,
"start_date": start_date,
"end_date": end_date,
"exchange": exchange,
}
resp = self._client.post(f"{self.base_url}/data/klines", json=payload, headers=self._headers())
resp.raise_for_status()
result = resp.json()
logger.info("K线 | {} {} | {} 条", symbol, interval, result.get("rows", 0))
return result.get("data", [])
def list_symbols(self, exchange: str = "binance") -> list[str]:
"""列出可用交易对"""
resp = self._client.get(f"{self.base_url}/data/symbols", params={"exchange": exchange}, headers=self._headers())
resp.raise_for_status()
return resp.json().get("symbols", [])
# ═══════════════ 策略 ═══════════════
def save_strategy(
self,
name: str,
script_content: str = "",
description: str = "",
symbol: str = "BTCUSDT",
timeframe: str = "1h",
direction: str = "long_short",
version: str = "v1.0",
tags: list[str] = None,
) -> dict:
"""保存策略到 Server(含脚本源码)"""
payload = {
"name": name,
"description": description,
"script_content": script_content,
"symbol": symbol,
"timeframe": timeframe,
"direction": direction,
"version": version,
"tags": tags or [],
}
resp = self._client.post(f"{self.base_url}/strategies/", json=payload, headers=self._headers())
resp.raise_for_status()
result = resp.json()
logger.info("策略已保存 | {} ({})", name, result.get("strategy_id", ""))
return result
def list_strategies(self) -> list[dict]:
"""列出所有策略"""
resp = self._client.get(f"{self.base_url}/strategies/", headers=self._headers())
resp.raise_for_status()
return resp.json()
def get_strategy(self, strategy_id: str) -> dict:
"""获取策略详情(含脚本源码)"""
resp = self._client.get(f"{self.base_url}/strategies/{strategy_id}", headers=self._headers())
resp.raise_for_status()
return resp.json()
# ═══════════════ 信号 ═══════════════
def save_signals(self, strategy_id: str, signals: list[dict]) -> dict:
"""批量保存信号到 Server"""
resp = self._client.post(
f"{self.base_url}/signals/batch",
json=signals,
params={"strategy_id": strategy_id},
headers=self._headers(),
)
resp.raise_for_status()
return resp.json()
def query_signals(
self,
strategy_id: str = None,
symbol: str = None,
start_date: str = None,
end_date: str = None,
limit: int = 200,
) -> dict:
"""查询信号"""
payload = {"limit": limit}
if strategy_id:
payload["strategy_id"] = strategy_id
if symbol:
payload["symbol"] = symbol
if start_date:
payload["start_date"] = start_date
if end_date:
payload["end_date"] = end_date
resp = self._client.post(f"{self.base_url}/signals/query", json=payload, headers=self._headers())
resp.raise_for_status()
return resp.json()
# ═══════════════ 服务器端执行回测 ═══════════════
def run_server_backtest(
self,
script_content: str,
strategy_name: str,
symbol: str,
timeframe: str,
start_date: str,
end_date: str,
strategy_id: str = "",
initial_capital: float = 100_000.0,
leverage: int = 1,
fee_rate: float = 0.0005,
slippage_bps: float = 5.0,
margin_mode: str = "isolated",
direction: str = "long_short",
) -> dict:
"""
服务器端一站式回测 — 上传脚本,服务器执行+回测。
与 run_backtest() 的区别:
- run_backtest(): 本地跑脚本生成信号,只传信号给服务器
- run_server_backtest(): 把脚本源码传给服务器,服务器执行一切
"""
payload = {
"script_content": script_content,
"strategy_name": strategy_name,
"strategy_id": strategy_id,
"symbol": symbol,
"timeframe": timeframe,
"start_date": start_date,
"end_date": end_date,
"initial_capital": initial_capital,
"leverage": leverage,
"fee_rate": fee_rate,
"slippage_bps": slippage_bps,
"margin_mode": margin_mode,
"direction": direction,
}
logger.info(
"上传脚本到服务器执行 | {} {} {} | {} → {}",
strategy_name, symbol, timeframe, start_date, end_date,
)
resp = self._client.post(
f"{self.base_url}/backtest/run-server",
json=payload,
headers=self._headers(),
)
resp.raise_for_status()
result = resp.json()
status = result.get("status", "unknown")
if status == "completed":
metrics = result.get("metrics", {})
logger.info(
"服务器回测完成 | 收益={:.2%} | Sharpe={:.2f} | 交易={} | 结论={}",
metrics.get("total_return_pct", 0),
metrics.get("sharpe_ratio", 0),
metrics.get("total_trades", 0),
result.get("conclusion", ""),
)
else:
logger.error("服务器回测失败 | {}", result.get("error"))
return result
# ═══════════════ 配额 ═══════════════
def check_quota(self) -> dict:
"""查询当前机器码的策略配额"""
return self._auth.check_quota()
def print_quota(self) -> None:
"""打印配额信息"""
self._auth.print_quota()
# ═══════════════ 展示工具 ═══════════════
@staticmethod
def print_metrics(result: dict) -> None:
"""格式化打印回测结果"""
if result.get("status") != "completed":
print(f"回测失败: {result.get('error', '未知错误')}")
return
m = result.get("metrics", {})
conclusion = result.get("conclusion", "")
conclusion_map = {
"approved": "✅ 通过 — 可以上线",
"paper_trade_first": "⚠️ 先模拟 — 建议观察",
"rejected": "❌ 驳回 — 需要调整",
}
print("\n" + "=" * 55)
print(" 回测绩效报告")
print("=" * 55)
print(f" 策略: {result.get('strategy_name', '')}")
print(f" 结论: {conclusion_map.get(conclusion, conclusion)}")
print("-" * 55)
print(f" 总收益率: {m.get('total_return_pct', 0):>+10.2%}")
print(f" 年化收益: {m.get('annual_return_pct', 0):>+10.2%}")
print(f" Sharpe: {m.get('sharpe_ratio', 0):>10.3f}")
print(f" Sortino: {m.get('sortino_ratio', 0):>10.3f}")
print(f" 最大回撤: {abs(m.get('max_drawdown_pct', 0)):>10.2%}")
print(f" Calmar: {m.get('calmar_ratio', 0):>10.3f}")
print("-" * 55)
print(f" 胜率: {m.get('win_rate', 0):>10.2%}")
print(f" 盈亏比: {m.get('profit_loss_ratio', 0):>10.2f}")
print(f" 总交易数: {m.get('total_trades', 0):>10d}")
print(f" 盈利交易: {m.get('winning_trades', 0):>10d}")
print(f" 亏损交易: {m.get('losing_trades', 0):>10d}")
print(f" 平均持仓: {m.get('avg_holding_bars', 0):>10.1f} bars")
print("-" * 55)
print(f" 总手续费: {m.get('total_commission', 0):>10.2f}")
print(f" 总滑点成本: {m.get('total_slippage_cost', 0):>10.2f}")
print(f" 资金费率净值: {m.get('net_funding', 0):>+10.2f}")
print(f" 爆仓次数: {m.get('liquidation_count', 0):>10d}")
print("-" * 55)
print(f" 总信号数: {m.get('total_signals', 0):>10d}")
print(f" 已执行信号: {m.get('signals_executed', 0):>10d}")
print(f" 最终余额: {m.get('final_balance', 0):>10.2f}")
print("=" * 55)
@staticmethod
def print_trades(result: dict, limit: int = 20) -> None:
"""格式化打印交易记录"""
trades = result.get("trades", [])
if not trades:
print("无交易记录")
return
print(f"\n交易记录(共 {len(trades)} 笔,显示前 {min(limit, len(trades))} 笔)")
print("-" * 100)
print(f"{'#':>4} {'时间':<20} {'动作':<10} {'方向':<6} {'价格':>12} {'数量':>10} {'盈亏':>12} {'余额':>14}")
print("-" * 100)
for t in trades[:limit]:
print(
f"{t.get('trade_id', 0):>4} "
f"{t.get('datetime', ''):<20} "
f"{t.get('action', ''):<10} "
f"{t.get('side', ''):<6} "
f"{t.get('price', 0):>12.2f} "
f"{t.get('quantity', 0):>10.4f} "
f"{t.get('pnl', 0):>+12.2f} "
f"{t.get('balance_after', 0):>14.2f}"
)
if len(trades) > limit:
print(f" ... 还有 {len(trades) - limit} 笔交易")
print("-" * 100)
@staticmethod
def print_conclusion(result: dict) -> None:
"""打印回测结论和建议"""
conclusion = result.get("conclusion", "")
metrics = result.get("metrics", {})
print("\n" + "=" * 50)
if conclusion == "approved":
print(" 结论: 通过")
print(" 建议: 可以进入监控执行阶段")
elif conclusion == "paper_trade_first":
print(" 结论: 先模拟")
print(" 建议: 先跑模拟盘观察 1-2 周")
elif conclusion == "rejected":
print(" 结论: 驳回")
print(" 建议: 调整策略参数后重新回测")
else:
print(f" 结论: {conclusion}")
if metrics:
issues = []
if metrics.get("total_return_pct", 0) < 0:
issues.append("总收益为负")
if abs(metrics.get("max_drawdown_pct", 0)) > 0.2:
issues.append("最大回撤超过 20%")
if metrics.get("sharpe_ratio", 0) < 1.0:
issues.append("夏普比率低于 1.0")
if metrics.get("win_rate", 0) < 0.3:
issues.append("胜率低于 30%")
if metrics.get("liquidation_count", 0) > 0:
issues.append(f"发生 {metrics['liquidation_count']} 次爆仓")
if issues:
print(" 风险点:")
for issue in issues:
print(f" - {issue}")
print("=" * 50)
# ═══════════════ 生命周期 ═══════════════
def close(self):
self._client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
"""
本地回测引擎 — 永续合约专项
支持:
- 多空双向持仓
- 逐仓/全仓保证金
- 杠杆 1x-125x
- 资金费率每 8h 结算(使用真实历史数据)
- 强制平仓
- 止损/止盈
- 固定滑点
- 手续费(Maker/Taker)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
import pandas as pd
from loguru import logger
# ═══════════════════════════════════════════
# 数据结构
# ═══════════════════════════════════════════
@dataclass
class Position:
"""单个持仓。"""
symbol: str
side: str = "none" # "long" / "short" / "none"
quantity: float = 0.0
avg_entry_price: float = 0.0
leverage: int = 1
margin: float = 0.0
margin_mode: str = "isolated"
maintenance_margin_rate: float = 0.005
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
unrealized_pnl: float = 0.0
liquidation_price: float = 0.0
@property
def nominal_value(self) -> float:
return self.quantity * self.avg_entry_price
def calc_unrealized_pnl(self, mark_price: float) -> float:
if self.side == "long":
return self.quantity * (mark_price - self.avg_entry_price)
elif self.side == "short":
return self.quantity * (self.avg_entry_price - mark_price)
return 0.0
def calc_liquidation_price(self) -> float:
"""
逐仓强平价格:
多单: entry × (1 - 1/leverage + mmr)
空单: entry × (1 + 1/leverage - mmr)
"""
if self.quantity == 0 or self.side == "none":
return 0.0
mmr = self.maintenance_margin_rate
if self.side == "long":
return self.avg_entry_price * (1 - 1 / self.leverage + mmr)
else:
return self.avg_entry_price * (1 + 1 / self.leverage - mmr)
def calc_margin_ratio(self, mark_price: float) -> float:
"""保证金率 = (保证金 + 未实现盈亏) / 名义价值"""
nominal = self.quantity * mark_price
if nominal == 0:
return float("inf")
pnl = self.calc_unrealized_pnl(mark_price)
return (self.margin + pnl) / nominal
@dataclass
class TradeRecord:
"""单笔交易记录。"""
datetime: str
symbol: str
side: str
action: str # "open" / "close" / "liquidation"
quantity: float
price: float
mark_price: float
leverage: int
margin_used: float
commission: float
slippage: float
funding_fee: float
realized_pnl: float
@dataclass
class Account:
"""账户状态。"""
initial_capital: float
balance: float = 0.0
positions: dict = field(default_factory=dict)
trade_log: list = field(default_factory=list)
equity_curve: list = field(default_factory=list)
funding_log: list = field(default_factory=list)
liquidation_count: int = 0
total_commission: float = 0.0
total_slippage_cost: float = 0.0
total_funding_paid: float = 0.0
total_funding_received: float = 0.0
def __post_init__(self):
self.balance = self.initial_capital
def get_position(self, symbol: str) -> Position:
if symbol not in self.positions:
self.positions[symbol] = Position(symbol=symbol)
return self.positions[symbol]
@property
def total_unrealized_pnl(self) -> float:
return sum(p.unrealized_pnl for p in self.positions.values())
@property
def equity(self) -> float:
return self.balance + self.total_unrealized_pnl
@property
def used_margin(self) -> float:
return sum(p.margin for p in self.positions.values() if p.side != "none")
@property
def available_balance(self) -> float:
return self.balance - self.used_margin
# ═══════════════════════════════════════════
# 回测配置
# ═══════════════════════════════════════════
@dataclass
class BacktestConfig:
"""回测引擎配置。所有参数均可外部化。"""
initial_capital: float = 100_000.0
default_leverage: int = 1
margin_mode: str = "isolated" # "isolated" / "cross"
slippage_bps: float = 5.0 # 滑点(基点)
taker_fee: float = 0.0005 # Taker 手续费 0.05%
maker_fee: float = 0.0002 # Maker 手续费 0.02%
enable_funding: bool = True # 是否启用资金费率结算
enable_liquidation: bool = True # 是否启用强平检查
maintenance_margin_rate: float = 0.005 # 维持保证金率 0.5%
# ═══════════════════════════════════════════
# 回测引擎
# ═══════════════════════════════════════════
class BacktestEngine:
"""
永续合约回测引擎。
使用方式:
config = BacktestConfig(initial_capital=100000, default_leverage=5)
engine = BacktestEngine(config)
for i, row in df.iterrows():
# 交易逻辑
if signal_long:
engine.open_long(symbol, qty, row["close"], row["close"], dt)
# 每 bar 更新
engine.on_bar(dt, prices, funding_rates)
result = engine.get_result()
"""
def __init__(self, config: BacktestConfig = None):
self.config = config or BacktestConfig()
self.account = Account(initial_capital=self.config.initial_capital)
# ── 交易操作 ──
def open_long(self, symbol: str, qty: float, price: float,
mark_price: float, dt: str, leverage: int = None):
"""开多仓。"""
self._open_position(symbol, "long", qty, price, mark_price, dt, leverage)
def open_short(self, symbol: str, qty: float, price: float,
mark_price: float, dt: str, leverage: int = None):
"""开空仓。"""
self._open_position(symbol, "short", qty, price, mark_price, dt, leverage)
def close_long(self, symbol: str, qty: float, price: float,
mark_price: float, dt: str):
"""平多仓。qty=0 表示全部平仓。"""
self._close_position(symbol, "long", qty, price, mark_price, dt)
def close_short(self, symbol: str, qty: float, price: float,
mark_price: float, dt: str):
"""平空仓。qty=0 表示全部平仓。"""
self._close_position(symbol, "short", qty, price, mark_price, dt)
def set_stop_loss(self, symbol: str, price: float):
"""设置止损价。"""
self.account.get_position(symbol).stop_loss = price
def set_take_profit(self, symbol: str, price: float):
"""设置止盈价。"""
self.account.get_position(symbol).take_profit = price
def set_leverage(self, symbol: str, leverage: int):
"""设置杠杆倍数 (1-125)。"""
pos = self.account.get_position(symbol)
pos.leverage = max(1, min(125, leverage))
def set_margin_mode(self, symbol: str, mode: str):
"""设置保证金模式: 'isolated' 或 'cross'。"""
pos = self.account.get_position(symbol)
pos.margin_mode = mode
def get_position(self, symbol: str) -> dict:
"""获取当前持仓信息。"""
pos = self.account.get_position(symbol)
return {
"side": pos.side,
"quantity": pos.quantity,
"avg_entry_price": pos.avg_entry_price,
"unrealized_pnl": pos.unrealized_pnl,
"liquidation_price": pos.liquidation_price,
"leverage": pos.leverage,
"margin": pos.margin,
"margin_ratio": pos.calc_margin_ratio(pos.avg_entry_price) if pos.side != "none" else 0,
}
# ── 内部: 开仓 ──
def _open_position(self, symbol: str, side: str, qty: float, price: float,
mark_price: float, dt: str, leverage: int = None):
pos = self.account.get_position(symbol)
lev = leverage or pos.leverage or self.config.default_leverage
pos.leverage = lev
pos.margin_mode = pos.margin_mode or self.config.margin_mode
pos.maintenance_margin_rate = self.config.maintenance_margin_rate
slippage = price * self.config.slippage_bps / 10000
fill_price = price + slippage if side == "long" else price - slippage
nominal = qty * fill_price
required_margin = nominal / lev
commission = nominal * self.config.taker_fee
if self.account.available_balance < required_margin + commission:
logger.warning(
f"[{dt}] 余额不足: 需要 {required_margin + commission:.2f}, "
f"可用 {self.account.available_balance:.2f}"
)
return
if pos.side == side and pos.quantity > 0:
total_qty = pos.quantity + qty
pos.avg_entry_price = (
pos.avg_entry_price * pos.quantity + fill_price * qty
) / total_qty
pos.quantity = total_qty
pos.margin += required_margin
else:
pos.side = side
pos.quantity = qty
pos.avg_entry_price = fill_price
pos.margin = required_margin
pos.liquidation_price = pos.calc_liquidation_price()
self.account.balance -= commission
self.account.total_commission += commission
self.account.total_slippage_cost += abs(slippage * qty)
self.account.trade_log.append(TradeRecord(
datetime=dt, symbol=symbol, side=side, action="open",
quantity=qty, price=fill_price, mark_price=mark_price,
leverage=lev, margin_used=required_margin,
commission=commission, slippage=abs(slippage * qty),
funding_fee=0.0, realized_pnl=0.0,
))
# ── 内部: 平仓 ──
def _close_position(self, symbol: str, side: str, qty: float, price: float,
mark_price: float, dt: str, action: str = "close"):
pos = self.account.get_position(symbol)
if pos.side != side or pos.quantity == 0:
return
close_qty = min(qty, pos.quantity) if qty else pos.quantity
slippage = price * self.config.slippage_bps / 10000
fill_price = price - slippage if side == "long" else price + slippage
if side == "long":
realized_pnl = close_qty * (fill_price - pos.avg_entry_price)
else:
realized_pnl = close_qty * (pos.avg_entry_price - fill_price)
nominal = close_qty * fill_price
commission = nominal * self.config.taker_fee
margin_released = pos.margin * (close_qty / pos.quantity)
pos.margin -= margin_released
self.account.balance += margin_released + realized_pnl - commission
self.account.total_commission += commission
self.account.total_slippage_cost += abs(slippage * close_qty)
pos.quantity -= close_qty
if pos.quantity <= 1e-10:
pos.quantity = 0
pos.side = "none"
pos.margin = 0
pos.stop_loss = None
pos.take_profit = None
self.account.trade_log.append(TradeRecord(
datetime=dt, symbol=symbol, side=side, action=action,
quantity=close_qty, price=fill_price, mark_price=mark_price,
leverage=pos.leverage, margin_used=0,
commission=commission, slippage=abs(slippage * close_qty),
funding_fee=0.0, realized_pnl=realized_pnl,
))
# ── 每 bar 检查 ──
def on_bar(self, dt: str, prices: dict[str, dict],
funding_rates: dict[str, float] = None):
"""
每个 bar 调用一次。
执行顺序: 更新盈亏 → 资金费率结算 → 止损止盈 → 强平检查 → 记录净值
参数:
dt: 当前 bar 时间
prices: {symbol: {"close": float, "high": float, "low": float, "mark_price": float}}
funding_rates: {symbol: float} — 仅在 8h 结算时刻传入
"""
for symbol, pos in list(self.account.positions.items()):
if pos.side == "none":
continue
bar = prices.get(symbol, {})
mark = bar.get("mark_price", bar.get("close", pos.avg_entry_price))
pos.unrealized_pnl = pos.calc_unrealized_pnl(mark)
if self.config.enable_funding and funding_rates and symbol in funding_rates:
self._settle_funding(pos, funding_rates[symbol], mark, dt)
if pos.side != "none":
self._check_stop_loss_take_profit(pos, bar, dt)
if self.config.enable_liquidation and pos.side != "none":
self._check_liquidation(pos, mark, dt)
self.account.equity_curve.append({
"datetime": dt,
"equity": self.account.equity,
"balance": self.account.balance,
"unrealized_pnl": self.account.total_unrealized_pnl,
"used_margin": self.account.used_margin,
"drawdown": 0.0,
})
def _settle_funding(self, pos: Position, funding_rate: float,
mark_price: float, dt: str):
"""资金费率结算。"""
nominal = pos.quantity * mark_price
fee = nominal * funding_rate
if pos.side == "long":
pos.margin -= fee
self.account.balance -= fee
if fee > 0:
self.account.total_funding_paid += fee
else:
self.account.total_funding_received += abs(fee)
else:
pos.margin += fee
self.account.balance += fee
if fee > 0:
self.account.total_funding_received += fee
else:
self.account.total_funding_paid += abs(fee)
self.account.funding_log.append({
"datetime": dt,
"symbol": pos.symbol,
"side": pos.side,
"funding_rate": funding_rate,
"position_value": nominal,
"fee": fee,
})
def _check_stop_loss_take_profit(self, pos: Position, bar: dict, dt: str):
"""检查止损/止盈是否触发。"""
high = bar.get("high", bar.get("close", 0))
low = bar.get("low", bar.get("close", 0))
mark = bar.get("mark_price", bar.get("close", 0))
if pos.stop_loss is not None:
triggered = (
(pos.side == "long" and low <= pos.stop_loss)
or (pos.side == "short" and high >= pos.stop_loss)
)
if triggered:
logger.info(f"[{dt}] 止损触发: {pos.symbol} {pos.side} @ {pos.stop_loss}")
self._close_position(
pos.symbol, pos.side, pos.quantity,
pos.stop_loss, mark, dt, "close",
)
return
if pos.take_profit is not None:
triggered = (
(pos.side == "long" and high >= pos.take_profit)
or (pos.side == "short" and low <= pos.take_profit)
)
if triggered:
logger.info(f"[{dt}] 止盈触发: {pos.symbol} {pos.side} @ {pos.take_profit}")
self._close_position(
pos.symbol, pos.side, pos.quantity,
pos.take_profit, mark, dt, "close",
)
def _check_liquidation(self, pos: Position, mark_price: float, dt: str):
"""强平检查。"""
if pos.side == "none" or pos.quantity == 0:
return
margin_ratio = pos.calc_margin_ratio(mark_price)
if margin_ratio <= pos.maintenance_margin_rate:
logger.warning(
f"[{dt}] 强平: {pos.symbol} {pos.side} "
f"保证金率 {margin_ratio:.4f} <= {pos.maintenance_margin_rate}"
)
lost_margin = pos.margin
pos.quantity = 0
pos.side = "none"
pos.margin = 0
pos.unrealized_pnl = 0
pos.stop_loss = None
pos.take_profit = None
self.account.liquidation_count += 1
self.account.trade_log.append(TradeRecord(
datetime=dt, symbol=pos.symbol, side=pos.side, action="liquidation",
quantity=0, price=mark_price, mark_price=mark_price,
leverage=pos.leverage, margin_used=0,
commission=0, slippage=0, funding_fee=0,
realized_pnl=-lost_margin,
))
# ── 结果汇总 ──
def get_result(self) -> dict:
"""获取完整回测结果。"""
eq_df = pd.DataFrame(self.account.equity_curve)
if eq_df.empty:
return {"error": "无回测数据"}
equities = eq_df["equity"].values
peak = np.maximum.accumulate(equities)
drawdowns = (equities - peak) / peak
eq_df["drawdown"] = drawdowns
returns = np.diff(equities) / equities[:-1] if len(equities) > 1 else np.array([0])
total_return = (equities[-1] / equities[0]) - 1
n_days = len(equities)
annual_return = (1 + total_return) ** (365 / max(n_days, 1)) - 1
volatility = float(np.std(returns) * np.sqrt(365)) if len(returns) > 1 else 0
rf = 0.0
sharpe = (annual_return - rf) / volatility if volatility > 0 else 0
downside = returns[returns < 0]
downside_std = float(np.std(downside) * np.sqrt(365)) if len(downside) > 0 else 0
sortino = (annual_return - rf) / downside_std if downside_std > 0 else 0
max_dd = float(np.min(drawdowns))
max_dd_idx = int(np.argmin(drawdowns))
peak_idx = int(np.argmax(equities[:max_dd_idx + 1])) if max_dd_idx > 0 else 0
max_dd_duration = max_dd_idx - peak_idx
calmar = annual_return / abs(max_dd) if max_dd != 0 else 0
trades = self.account.trade_log
close_trades = [
t for t in trades
if t.action in ("close", "liquidation") and t.realized_pnl != 0
]
wins = [t for t in close_trades if t.realized_pnl > 0]
losses = [t for t in close_trades if t.realized_pnl < 0]
win_rate = len(wins) / len(close_trades) if close_trades else 0
avg_win = float(np.mean([t.realized_pnl for t in wins])) if wins else 0
avg_loss = float(abs(np.mean([t.realized_pnl for t in losses]))) if losses else 0
profit_loss_ratio = avg_win / avg_loss if avg_loss > 0 else float("inf")
net_funding = self.account.total_funding_received - self.account.total_funding_paid
return {
"performance": {
"total_return": total_return,
"annual_return": annual_return,
"sharpe_ratio": sharpe,
"sortino_ratio": sortino,
"max_drawdown": max_dd,
"max_drawdown_duration": int(max_dd_duration),
"calmar_ratio": calmar,
"volatility": volatility,
"win_rate": win_rate,
"profit_loss_ratio": profit_loss_ratio,
"total_trades": len(trades),
"total_funding_paid": self.account.total_funding_paid,
"total_funding_received": self.account.total_funding_received,
"net_funding": net_funding,
"total_commission": self.account.total_commission,
"total_slippage_cost": self.account.total_slippage_cost,
"liquidation_count": self.account.liquidation_count,
},
"equity_curve": eq_df.to_dict("records"),
"trade_log": [vars(t) for t in trades],
"funding_log": self.account.funding_log,
}
def get_metrics(self) -> dict:
"""获取绩效指标摘要(不含完整曲线和日志)。"""
result = self.get_result()
return result.get("performance", result)
@staticmethod
def format_summary(result: dict) -> str:
"""格式化输出回测结果摘要。"""
p = result.get("performance", {})
lines = [
"═══ 回测结果摘要 ═══",
f"总收益率: {p.get('total_return', 0):.2%}",
f"年化收益率: {p.get('annual_return', 0):.2%}",
f"夏普比率: {p.get('sharpe_ratio', 0):.3f}",
f"索提诺比率: {p.get('sortino_ratio', 0):.3f}",
f"最大回撤: {p.get('max_drawdown', 0):.2%}",
f"卡尔玛比率: {p.get('calmar_ratio', 0):.3f}",
f"年化波动率: {p.get('volatility', 0):.2%}",
f"胜率: {p.get('win_rate', 0):.2%}",
f"盈亏比: {p.get('profit_loss_ratio', 0):.2f}",
f"总交易次数: {p.get('total_trades', 0)}",
"─── 资金费率 ───",
f"支付: {p.get('total_funding_paid', 0):.2f} USDT",
f"收到: {p.get('total_funding_received', 0):.2f} USDT",
f"净损益: {p.get('net_funding', 0):.2f} USDT",
"─── 费用与风控 ───",
f"累计手续费: {p.get('total_commission', 0):.2f} USDT",
f"累计滑点: {p.get('total_slippage_cost', 0):.2f} USDT",
f"强平次数: {p.get('liquidation_count', 0)}",
]
return "\n".join(lines)
"""
多源数据客户端 — 直接调用公开 API
数据源:
- Binance Futures: K线、资金费率、持仓量、合约信息、标记价格
- Binance Spot: 现货 K线
- CoinGecko: PAXG/XAUT 等加密代币价格
- yfinance: 美股(AAPL/NVDA/SPY...)、大宗商品(WTI/NG/铜)、贵金属现货(XAU/XAG)
- DeFi Llama: 协议 TVL、手续费收入(免费端点)
全部免费公开端点,无需 API Key。
国内访问 Binance 可配置代理: PROXY_URL 环境变量。
"""
import time
from datetime import datetime, timezone
from typing import Optional
import httpx
import pandas as pd
from loguru import logger
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
BINANCE_SPOT_BASE = "https://api.binance.com"
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
DEFILLAMA_BASE = "https://api.llama.fi"
INTERVAL_MAP = {
"1m": "1m", "5m": "5m", "15m": "15m",
"1h": "1h", "4h": "4h", "1d": "1d",
}
COINGECKO_IDS = {
"PAXG": "pax-gold",
"XAUT": "tether-gold",
"OUSG": "ondo-us-government-bond-fund",
"OMMF": "ondo-us-dollar-yield",
}
# yfinance ticker 映射
YFINANCE_TICKERS = {
"RWA:AAPL": "AAPL", "RWA:NVDA": "NVDA", "RWA:TSLA": "TSLA",
"RWA:MSFT": "MSFT", "RWA:GOOGL": "GOOGL", "RWA:AMZN": "AMZN",
"RWA:META": "META", "RWA:SPY": "SPY", "RWA:QQQ": "QQQ",
"COMM:WTI": "CL=F", "COMM:BRENT": "BZ=F",
"COMM:NG": "NG=F", "COMM:COPPER": "HG=F",
"METAL:XAU-SPOT": "GC=F", "METAL:XAG-SPOT": "SI=F",
}
def _ts_ms(dt_str: str) -> int:
"""日期字符串 (YYYY-MM-DD) 转毫秒时间戳。"""
dt = datetime.strptime(dt_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
return int(dt.timestamp() * 1000)
def _symbol_to_binance(symbol: str) -> str:
"""BTC-USDT-PERP → BTCUSDT, BTC-USDT-SPOT → BTCUSDT"""
parts = symbol.upper().replace("-PERP", "").replace("-SPOT", "").split("-")
return "".join(parts)
class DataClient:
"""多源数据客户端,支持 Binance / CoinGecko / yfinance / DeFi Llama。"""
def __init__(self, proxy: Optional[str] = None):
import os
proxy_url = proxy or os.environ.get("PROXY_URL")
self._client = httpx.Client(
timeout=30.0,
proxy=proxy_url,
)
def _get(self, url: str, params: dict = None) -> dict | list:
"""带 429 限流重试的 GET 请求。"""
resp = self._client.get(url, params=params)
if resp.status_code == 429:
retry = int(resp.headers.get("Retry-After", "5"))
logger.warning(f"429 限流,等待 {retry}s")
time.sleep(retry)
resp = self._client.get(url, params=params)
resp.raise_for_status()
return resp.json()
# ════════════════════════════════════════
# Binance Futures — 永续合约
# ════════════════════════════════════════
def get_perp_klines(
self,
symbol: str,
interval: str = "1d",
start_date: str = None,
end_date: str = None,
limit: int = 1500,
) -> pd.DataFrame:
"""
永续合约 K 线。
Binance 端点: GET /fapi/v1/klines
无需 API Key,限流 2400 次/分钟。
单次最多 1500 条,自动分页拉取完整历史。
参数:
symbol: 合约代码,如 "BTC-USDT-PERP"
interval: K 线周期 (1m/5m/15m/1h/4h/1d)
start_date: 起始日期 "YYYY-MM-DD"
end_date: 结束日期 "YYYY-MM-DD"
limit: 单次请求条数(最大 1500)
返回:
DataFrame [datetime, open, high, low, close, volume,
volume_usd, trades, taker_buy_volume_usd, taker_sell_volume_usd]
"""
bn_symbol = _symbol_to_binance(symbol)
all_rows = []
params = {
"symbol": bn_symbol,
"interval": INTERVAL_MAP.get(interval, interval),
"limit": limit,
}
if start_date:
params["startTime"] = _ts_ms(start_date)
if end_date:
params["endTime"] = _ts_ms(end_date)
while True:
data = self._get(f"{BINANCE_FUTURES_BASE}/fapi/v1/klines", params)
if not data:
break
all_rows.extend(data)
if len(data) < limit:
break
params["startTime"] = data[-1][0] + 1
if end_date and params["startTime"] > _ts_ms(end_date):
break
time.sleep(0.1)
if not all_rows:
return pd.DataFrame()
df = pd.DataFrame(all_rows, columns=[
"open_time", "open", "high", "low", "close", "volume",
"close_time", "quote_volume", "trades", "taker_buy_volume",
"taker_buy_quote_volume", "ignore",
])
df["datetime"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
for col in ["open", "high", "low", "close", "volume", "quote_volume",
"taker_buy_volume", "taker_buy_quote_volume"]:
df[col] = df[col].astype(float)
df = df.rename(columns={
"quote_volume": "volume_usd",
"taker_buy_quote_volume": "taker_buy_volume_usd",
})
df["taker_sell_volume_usd"] = df["volume_usd"] - df["taker_buy_volume_usd"]
return df[["datetime", "open", "high", "low", "close", "volume",
"volume_usd", "trades", "taker_buy_volume_usd",
"taker_sell_volume_usd"]].reset_index(drop=True)
def get_funding_rate(
self,
symbol: str,
start_date: str = None,
end_date: str = None,
limit: int = 1000,
) -> pd.DataFrame:
"""
资金费率历史。
Binance 端点: GET /fapi/v1/fundingRate
每 8 小时一条,自动分页拉取。
返回:
DataFrame [datetime, funding_rate, mark_price]
"""
bn_symbol = _symbol_to_binance(symbol)
all_rows = []
params = {"symbol": bn_symbol, "limit": limit}
if start_date:
params["startTime"] = _ts_ms(start_date)
if end_date:
params["endTime"] = _ts_ms(end_date)
while True:
data = self._get(f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate", params)
if not data:
break
all_rows.extend(data)
if len(data) < limit:
break
params["startTime"] = data[-1]["fundingTime"] + 1
time.sleep(0.1)
if not all_rows:
return pd.DataFrame()
df = pd.DataFrame(all_rows)
df["datetime"] = pd.to_datetime(df["fundingTime"], unit="ms", utc=True)
df["funding_rate"] = df["fundingRate"].astype(float)
df["mark_price"] = df["markPrice"].astype(float)
return df[["datetime", "funding_rate", "mark_price"]].reset_index(drop=True)
def get_open_interest(self, symbol: str) -> dict:
"""
当前持仓量快照。
Binance 端点: GET /fapi/v1/openInterest
仅返回当前快照,不含历史。
"""
bn_symbol = _symbol_to_binance(symbol)
data = self._get(
f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest",
{"symbol": bn_symbol},
)
return {
"symbol": symbol,
"open_interest": float(data["openInterest"]),
"timestamp": data["time"],
}
def get_open_interest_hist(
self,
symbol: str,
period: str = "1d",
limit: int = 30,
) -> pd.DataFrame:
"""
持仓量历史统计。
Binance 端点: GET /futures/data/openInterestHist
⚠️ 限制: 仅最近 30 天数据。
"""
bn_symbol = _symbol_to_binance(symbol)
data = self._get(f"{BINANCE_FUTURES_BASE}/futures/data/openInterestHist", {
"pair": bn_symbol,
"contractType": "PERPETUAL",
"period": period,
"limit": limit,
})
if not data:
return pd.DataFrame()
df = pd.DataFrame(data)
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
df["open_interest"] = df["sumOpenInterest"].astype(float)
df["open_interest_usd"] = df["sumOpenInterestValue"].astype(float)
return df[["datetime", "open_interest", "open_interest_usd"]].reset_index(drop=True)
def get_long_short_ratio(
self,
symbol: str,
period: str = "1d",
limit: int = 30,
) -> pd.DataFrame:
"""
Top Trader 多空持仓比。
Binance 端点: GET /futures/data/topLongShortPositionRatio
⚠️ 限制: 仅最近 30 天。
"""
bn_symbol = _symbol_to_binance(symbol)
data = self._get(
f"{BINANCE_FUTURES_BASE}/futures/data/topLongShortPositionRatio",
{"symbol": bn_symbol, "period": period, "limit": limit},
)
if not data:
return pd.DataFrame()
df = pd.DataFrame(data)
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
df["long_short_ratio"] = df["longShortRatio"].astype(float)
df["long_account"] = df["longAccount"].astype(float)
df["short_account"] = df["shortAccount"].astype(float)
return df[["datetime", "long_short_ratio", "long_account",
"short_account"]].reset_index(drop=True)
def get_mark_price(self, symbol: str) -> dict:
"""
当前标记价格和资金费率。
Binance 端点: GET /fapi/v1/premiumIndex
"""
bn_symbol = _symbol_to_binance(symbol)
data = self._get(
f"{BINANCE_FUTURES_BASE}/fapi/v1/premiumIndex",
{"symbol": bn_symbol},
)
return {
"symbol": symbol,
"mark_price": float(data["markPrice"]),
"index_price": float(data["indexPrice"]),
"funding_rate": float(data["lastFundingRate"]),
"next_funding_time": data["nextFundingTime"],
}
def get_exchange_info(self, symbol: str = None) -> dict | list:
"""
合约信息(面值、杠杆上限、最小下单量等)。
Binance 端点: GET /fapi/v1/exchangeInfo
"""
data = self._get(f"{BINANCE_FUTURES_BASE}/fapi/v1/exchangeInfo")
symbols = data.get("symbols", [])
if symbol:
bn_symbol = _symbol_to_binance(symbol)
for s in symbols:
if s["symbol"] == bn_symbol:
return self._parse_contract_info(s, symbol)
raise ValueError(f"合约 {symbol} 未找到")
return [
self._parse_contract_info(s, f"{s.get('baseAsset', '')}-{s.get('quoteAsset', '')}-PERP")
for s in symbols
if s.get("contractType") == "PERPETUAL"
]
@staticmethod
def _parse_contract_info(raw: dict, symbol: str) -> dict:
filters = {f["filterType"]: f for f in raw.get("filters", [])}
price_filter = filters.get("PRICE_FILTER", {})
lot_filter = filters.get("LOT_SIZE", {})
return {
"symbol": symbol,
"base_asset": raw.get("baseAsset", ""),
"quote_asset": raw.get("quoteAsset", ""),
"contract_type": raw.get("contractType", ""),
"tick_size": float(price_filter.get("tickSize", 0)),
"min_qty": float(lot_filter.get("minQty", 0)),
"max_qty": float(lot_filter.get("maxQty", 0)),
"step_size": float(lot_filter.get("stepSize", 0)),
"maintenance_margin_rate": float(raw.get("maintMarginPercent", 2.5)) / 100,
"required_margin_rate": float(raw.get("requiredMarginPercent", 5)) / 100,
}
def list_perp_symbols(self) -> list[str]:
"""列出 Binance 所有永续合约代码。"""
data = self._get(f"{BINANCE_FUTURES_BASE}/fapi/v1/exchangeInfo")
return [
f"{s['baseAsset']}-{s['quoteAsset']}-PERP"
for s in data.get("symbols", [])
if s.get("contractType") == "PERPETUAL" and s.get("status") == "TRADING"
]
# ════════════════════════════════════════
# Binance Spot — 现货
# ════════════════════════════════════════
def get_spot_klines(
self,
symbol: str,
interval: str = "1d",
start_date: str = None,
end_date: str = None,
limit: int = 1000,
) -> pd.DataFrame:
"""
现货 K 线。
Binance 端点: GET /api/v3/klines
自动分页,无限历史。
返回:
DataFrame [datetime, open, high, low, close, volume, volume_usd]
"""
bn_symbol = _symbol_to_binance(symbol)
all_rows = []
params = {"symbol": bn_symbol, "interval": interval, "limit": limit}
if start_date:
params["startTime"] = _ts_ms(start_date)
if end_date:
params["endTime"] = _ts_ms(end_date)
while True:
data = self._get(f"{BINANCE_SPOT_BASE}/api/v3/klines", params)
if not data:
break
all_rows.extend(data)
if len(data) < limit:
break
params["startTime"] = data[-1][0] + 1
time.sleep(0.1)
if not all_rows:
return pd.DataFrame()
df = pd.DataFrame(all_rows, columns=[
"open_time", "open", "high", "low", "close", "volume",
"close_time", "quote_volume", "trades", "taker_buy_volume",
"taker_buy_quote_volume", "ignore",
])
df["datetime"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
for col in ["open", "high", "low", "close", "volume", "quote_volume"]:
df[col] = df[col].astype(float)
df = df.rename(columns={"quote_volume": "volume_usd"})
return df[["datetime", "open", "high", "low", "close", "volume",
"volume_usd"]].reset_index(drop=True)
# ════════════════════════════════════════
# CoinGecko — 代币价格
# ════════════════════════════════════════
def get_token_history(
self,
token: str,
days: int = 365,
) -> pd.DataFrame:
"""
代币价格历史(日线)。
CoinGecko 端点: GET /api/v3/coins/{id}/market_chart
免费版限流 10-30 次/分钟,日线最多 365 天。
参数:
token: 代币名称 (PAXG/XAUT/OUSG 等)
days: 历史天数
返回:
DataFrame [datetime, close, volume_usd, market_cap]
"""
cg_id = COINGECKO_IDS.get(token.upper(), token.lower())
data = self._get(f"{COINGECKO_BASE}/coins/{cg_id}/market_chart", {
"vs_currency": "usd",
"days": days,
"interval": "daily",
})
prices = data.get("prices", [])
volumes = data.get("total_volumes", [])
caps = data.get("market_caps", [])
if not prices:
return pd.DataFrame()
df = pd.DataFrame(prices, columns=["timestamp", "close"])
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
if volumes and len(volumes) == len(prices):
df["volume_usd"] = [v[1] for v in volumes]
if caps and len(caps) == len(prices):
df["market_cap"] = [c[1] for c in caps]
return df.drop(columns=["timestamp"]).reset_index(drop=True)
# ════════════════════════════════════════
# yfinance — 美股 / 大宗商品 / 贵金属
# ════════════════════════════════════════
@staticmethod
def get_stock_klines(
symbol: str,
start_date: str,
end_date: str,
interval: str = "1d",
) -> pd.DataFrame:
"""
美股/ETF K 线。
数据源: yfinance(Yahoo Finance 公开数据)
支持 Symbol: RWA:AAPL / RWA:SPY / RWA:QQQ 等
历史深度: 日线 30+ 年
返回:
DataFrame [datetime, open, high, low, close, volume, volume_usd, dividends?]
"""
import yfinance as yf
ticker = YFINANCE_TICKERS.get(symbol.upper(), symbol.replace("RWA:", ""))
yf_interval = {"1d": "1d", "1h": "1h", "5m": "5m", "1m": "1m"}.get(interval, "1d")
tk = yf.Ticker(ticker)
df = tk.history(start=start_date, end=end_date, interval=yf_interval)
if df.empty:
return pd.DataFrame()
df = df.reset_index()
date_col = "Date" if "Date" in df.columns else "Datetime"
df = df.rename(columns={
date_col: "datetime",
"Open": "open", "High": "high", "Low": "low",
"Close": "close", "Volume": "volume",
})
df["datetime"] = pd.to_datetime(df["datetime"], utc=True)
df["volume_usd"] = df["close"] * df["volume"]
cols = ["datetime", "open", "high", "low", "close", "volume", "volume_usd"]
if "Dividends" in df.columns:
df["dividends"] = df["Dividends"]
cols.append("dividends")
return df[cols].reset_index(drop=True)
@staticmethod
def get_commodity_klines(
symbol: str,
start_date: str,
end_date: str,
interval: str = "1d",
) -> pd.DataFrame:
"""
大宗商品期货 K 线。
数据源: yfinance
支持: COMM:WTI / COMM:BRENT / COMM:NG / COMM:COPPER
历史深度: 10+ 年
返回:
DataFrame [datetime, open, high, low, close, volume, volume_usd]
"""
import yfinance as yf
ticker = YFINANCE_TICKERS.get(symbol.upper())
if not ticker:
raise ValueError(
f"未知大宗商品 Symbol: {symbol},"
f"支持: {[k for k in YFINANCE_TICKERS if k.startswith('COMM:')]}"
)
tk = yf.Ticker(ticker)
df = tk.history(start=start_date, end=end_date, interval=interval)
if df.empty:
return pd.DataFrame()
df = df.reset_index()
date_col = "Date" if "Date" in df.columns else "Datetime"
df = df.rename(columns={
date_col: "datetime",
"Open": "open", "High": "high", "Low": "low",
"Close": "close", "Volume": "volume",
})
df["datetime"] = pd.to_datetime(df["datetime"], utc=True)
df["volume_usd"] = df["close"] * df["volume"]
return df[["datetime", "open", "high", "low", "close",
"volume", "volume_usd"]].reset_index(drop=True)
@staticmethod
def get_metal_spot_klines(
symbol: str,
start_date: str,
end_date: str,
) -> pd.DataFrame:
"""
贵金属现货 K 线(通过期货合约代理)。
数据源: yfinance (GC=F 黄金期货 / SI=F 白银期货)
支持: METAL:XAU-SPOT / METAL:XAG-SPOT
历史深度: 10+ 年
返回:
DataFrame [datetime, open, high, low, close, volume]
"""
import yfinance as yf
ticker = YFINANCE_TICKERS.get(symbol.upper())
if not ticker:
raise ValueError(
f"未知贵金属 Symbol: {symbol},支持: METAL:XAU-SPOT / METAL:XAG-SPOT"
)
tk = yf.Ticker(ticker)
df = tk.history(start=start_date, end=end_date, interval="1d")
if df.empty:
return pd.DataFrame()
df = df.reset_index()
date_col = "Date" if "Date" in df.columns else "Datetime"
df = df.rename(columns={
date_col: "datetime",
"Open": "open", "High": "high", "Low": "low",
"Close": "close", "Volume": "volume",
})
df["datetime"] = pd.to_datetime(df["datetime"], utc=True)
return df[["datetime", "open", "high", "low", "close",
"volume"]].reset_index(drop=True)
# ════════════════════════════════════════
# DeFi Llama — 协议 TVL / 手续费
# ════════════════════════════════════════
def get_protocol_tvl(self, protocol: str) -> pd.DataFrame:
"""
协议 TVL 历史。
DeFi Llama 端点: GET /protocol/{slug}
免费,无需 Key。
支持: aave, compound-v3, lido, curve-dex, uniswap 等
返回:
DataFrame [datetime, tvl_usd]
"""
data = self._get(f"{DEFILLAMA_BASE}/protocol/{protocol}")
tvl_history = data.get("tvl", [])
if not tvl_history:
return pd.DataFrame()
df = pd.DataFrame(tvl_history)
df["datetime"] = pd.to_datetime(df["date"], unit="s", utc=True)
df["tvl_usd"] = df["totalLiquidityUSD"].astype(float)
return df[["datetime", "tvl_usd"]].reset_index(drop=True)
def get_protocol_info(self, protocol: str) -> dict:
"""
协议当前信息(TVL、类别、链等)。
DeFi Llama 端点: GET /protocol/{slug}
"""
data = self._get(f"{DEFILLAMA_BASE}/protocol/{protocol}")
return {
"name": data.get("name", ""),
"category": data.get("category", ""),
"chains": data.get("chains", []),
"current_tvl": data.get("currentChainTvls", {}),
"total_tvl": float(data.get("tvl", [{}])[-1].get("totalLiquidityUSD", 0))
if data.get("tvl") else 0,
}
def get_defi_fees(self, protocol: str = None) -> pd.DataFrame:
"""
协议手续费/收入数据。
DeFi Llama 端点: GET /overview/fees
免费,返回所有协议的 24h 手续费和收入。
返回:
DataFrame [name, category, fees_24h, fees_7d, fees_30d, revenue_24h]
"""
data = self._get(f"{DEFILLAMA_BASE}/overview/fees")
protocols = data.get("protocols", [])
if protocol:
protocols = [
p for p in protocols
if p.get("name", "").lower() == protocol.lower()
or p.get("slug", "") == protocol.lower()
]
if not protocols:
return pd.DataFrame()
rows = []
for p in protocols:
rows.append({
"name": p.get("name", ""),
"category": p.get("category", ""),
"fees_24h": float(p.get("total24h", 0) or 0),
"fees_7d": float(p.get("total7d", 0) or 0),
"fees_30d": float(p.get("total30d", 0) or 0),
"revenue_24h": float(p.get("revenue24h", 0) or 0),
})
return pd.DataFrame(rows)
def list_defi_protocols(self) -> pd.DataFrame:
"""
所有 DeFi 协议列表及 TVL。
DeFi Llama 端点: GET /protocols
返回:
DataFrame [name, slug, category, chains, tvl] (前 200 个)
"""
data = self._get(f"{DEFILLAMA_BASE}/protocols")
rows = []
for p in data[:200]:
rows.append({
"name": p.get("name", ""),
"slug": p.get("slug", ""),
"category": p.get("category", ""),
"chains": ", ".join(p.get("chains", [])),
"tvl": float(p.get("tvl", 0) or 0),
})
return pd.DataFrame(rows)
# ════════════════════════════════════════
# 生命周期
# ════════════════════════════════════════
def close(self):
self._client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
"""
技术指标库 — 纯 NumPy 向量化计算
支持指标:
SMA, EMA, RSI, MACD, 布林带, ATR, KDJ, 成交量均线
所有方法接受 numpy 数组,返回 numpy 数组。
无 pandas 依赖,可直接嵌入回测循环。
"""
from __future__ import annotations
import numpy as np
class Indicators:
"""向量化技术指标计算器。"""
# ════════════════════════════════════════
# 均线类
# ════════════════════════════════════════
@staticmethod
def sma(data: np.ndarray, period: int) -> np.ndarray:
"""
简单移动平均线 (Simple Moving Average)。
参数:
data: 价格序列
period: 均线周期
返回:
SMA 数组,前 period-1 个值为 NaN
"""
if len(data) < period:
return np.full_like(data, np.nan, dtype=float)
result = np.full(len(data), np.nan, dtype=float)
cumsum = np.cumsum(data, dtype=float)
result[period - 1:] = (cumsum[period - 1:] - np.concatenate(([0], cumsum[:-period]))) / period
return result
@staticmethod
def ema(data: np.ndarray, period: int) -> np.ndarray:
"""
指数移动平均线 (Exponential Moving Average)。
使用递推公式: EMA_t = α × price_t + (1 - α) × EMA_{t-1}
其中 α = 2 / (period + 1)
参数:
data: 价格序列
period: 均线周期
返回:
EMA 数组,前 period-1 个值为 NaN
"""
if len(data) < period:
return np.full_like(data, np.nan, dtype=float)
alpha = 2.0 / (period + 1)
result = np.full(len(data), np.nan, dtype=float)
result[period - 1] = np.mean(data[:period])
for i in range(period, len(data)):
result[i] = alpha * data[i] + (1 - alpha) * result[i - 1]
return result
@staticmethod
def volume_ma(volume: np.ndarray, period: int) -> np.ndarray:
"""
成交量移动平均线。
参数:
volume: 成交量序列
period: 均线周期
返回:
成交量 SMA 数组
"""
return Indicators.sma(volume, period)
# ════════════════════════════════════════
# 动量/震荡类
# ════════════════════════════════════════
@staticmethod
def rsi(data: np.ndarray, period: int = 14) -> np.ndarray:
"""
相对强弱指标 (Relative Strength Index)。
使用 Wilder 平滑法:
RS = 平均涨幅 / 平均跌幅
RSI = 100 - 100 / (1 + RS)
参数:
data: 价格序列
period: RSI 周期(默认 14)
返回:
RSI 数组 (0-100),前 period 个值为 NaN
"""
if len(data) < period + 1:
return np.full_like(data, np.nan, dtype=float)
deltas = np.diff(data)
gains = np.where(deltas > 0, deltas, 0.0)
losses = np.where(deltas < 0, -deltas, 0.0)
result = np.full(len(data), np.nan, dtype=float)
avg_gain = np.mean(gains[:period])
avg_loss = np.mean(losses[:period])
if avg_loss == 0:
result[period] = 100.0
else:
rs = avg_gain / avg_loss
result[period] = 100.0 - 100.0 / (1.0 + rs)
for i in range(period, len(deltas)):
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
if avg_loss == 0:
result[i + 1] = 100.0
else:
rs = avg_gain / avg_loss
result[i + 1] = 100.0 - 100.0 / (1.0 + rs)
return result
@staticmethod
def macd(
data: np.ndarray,
fast_period: int = 12,
slow_period: int = 26,
signal_period: int = 9,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
MACD 指标 (Moving Average Convergence Divergence)。
参数:
data: 价格序列
fast_period: 快线 EMA 周期(默认 12)
slow_period: 慢线 EMA 周期(默认 26)
signal_period: 信号线 EMA 周期(默认 9)
返回:
(macd_line, signal_line, histogram)
- macd_line: DIF = EMA(fast) - EMA(slow)
- signal_line: DEA = EMA(DIF, signal_period)
- histogram: MACD 柱状图 = (DIF - DEA) × 2
"""
ema_fast = Indicators.ema(data, fast_period)
ema_slow = Indicators.ema(data, slow_period)
macd_line = ema_fast - ema_slow
valid_start = slow_period - 1
signal_line = np.full(len(data), np.nan, dtype=float)
valid_macd = macd_line[valid_start:]
if len(valid_macd) >= signal_period:
signal_ema = Indicators.ema(valid_macd, signal_period)
signal_line[valid_start:] = signal_ema
histogram = (macd_line - signal_line) * 2
return macd_line, signal_line, histogram
@staticmethod
def kdj(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
k_period: int = 9,
d_period: int = 3,
j_smooth: int = 3,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
KDJ 随机指标。
计算过程:
RSV = (Close - Low_N) / (High_N - Low_N) × 100
K = SMA(RSV, d_period) (递推平滑)
D = SMA(K, j_smooth)
J = 3K - 2D
参数:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
k_period: RSV 窗口(默认 9)
d_period: K 值平滑周期(默认 3)
j_smooth: D 值平滑周期(默认 3)
返回:
(K, D, J) 三元组
"""
n = len(close)
rsv = np.full(n, np.nan, dtype=float)
for i in range(k_period - 1, n):
period_high = np.max(high[i - k_period + 1: i + 1])
period_low = np.min(low[i - k_period + 1: i + 1])
if period_high == period_low:
rsv[i] = 50.0
else:
rsv[i] = (close[i] - period_low) / (period_high - period_low) * 100.0
k_values = np.full(n, np.nan, dtype=float)
d_values = np.full(n, np.nan, dtype=float)
first_valid = k_period - 1
k_values[first_valid] = rsv[first_valid]
d_values[first_valid] = k_values[first_valid]
for i in range(first_valid + 1, n):
if np.isnan(rsv[i]):
continue
k_values[i] = (k_values[i - 1] * (d_period - 1) + rsv[i]) / d_period
d_values[i] = (d_values[i - 1] * (j_smooth - 1) + k_values[i]) / j_smooth
j_values = 3.0 * k_values - 2.0 * d_values
return k_values, d_values, j_values
# ════════════════════════════════════════
# 波动率/通道类
# ════════════════════════════════════════
@staticmethod
def bollinger_bands(
data: np.ndarray,
period: int = 20,
num_std: float = 2.0,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
布林带 (Bollinger Bands)。
参数:
data: 价格序列
period: 均线周期(默认 20)
num_std: 标准差倍数(默认 2.0)
返回:
(upper, middle, lower)
- middle: SMA(period)
- upper: middle + num_std × σ
- lower: middle - num_std × σ
"""
middle = Indicators.sma(data, period)
std = np.full(len(data), np.nan, dtype=float)
for i in range(period - 1, len(data)):
std[i] = np.std(data[i - period + 1: i + 1], ddof=0)
upper = middle + num_std * std
lower = middle - num_std * std
return upper, middle, lower
@staticmethod
def atr(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
period: int = 14,
) -> np.ndarray:
"""
平均真实波幅 (Average True Range)。
TR = max(High-Low, |High-PrevClose|, |Low-PrevClose|)
ATR = SMA(TR, period) 或 Wilder 平滑
参数:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
period: ATR 周期(默认 14)
返回:
ATR 数组
"""
n = len(close)
tr = np.full(n, np.nan, dtype=float)
tr[0] = high[0] - low[0]
for i in range(1, n):
hl = high[i] - low[i]
hc = abs(high[i] - close[i - 1])
lc = abs(low[i] - close[i - 1])
tr[i] = max(hl, hc, lc)
atr_values = np.full(n, np.nan, dtype=float)
if n >= period:
atr_values[period - 1] = np.mean(tr[:period])
for i in range(period, n):
atr_values[i] = (atr_values[i - 1] * (period - 1) + tr[i]) / period
return atr_values
# ════════════════════════════════════════
# 辅助方法
# ════════════════════════════════════════
@staticmethod
def crossover(series_a: np.ndarray, series_b: np.ndarray) -> np.ndarray:
"""
金叉判断: series_a 从下方穿越 series_b。
返回布尔数组,True 表示该 bar 发生金叉。
"""
result = np.zeros(len(series_a), dtype=bool)
for i in range(1, len(series_a)):
if (np.isnan(series_a[i]) or np.isnan(series_b[i]) or
np.isnan(series_a[i - 1]) or np.isnan(series_b[i - 1])):
continue
result[i] = (series_a[i - 1] <= series_b[i - 1]) and (series_a[i] > series_b[i])
return result
@staticmethod
def crossunder(series_a: np.ndarray, series_b: np.ndarray) -> np.ndarray:
"""
死叉判断: series_a 从上方穿越 series_b。
返回布尔数组,True 表示该 bar 发生死叉。
"""
result = np.zeros(len(series_a), dtype=bool)
for i in range(1, len(series_a)):
if (np.isnan(series_a[i]) or np.isnan(series_b[i]) or
np.isnan(series_a[i - 1]) or np.isnan(series_b[i - 1])):
continue
result[i] = (series_a[i - 1] >= series_b[i - 1]) and (series_a[i] < series_b[i])
return result
@staticmethod
def highest(data: np.ndarray, period: int) -> np.ndarray:
"""滚动最高值。"""
result = np.full(len(data), np.nan, dtype=float)
for i in range(period - 1, len(data)):
result[i] = np.max(data[i - period + 1: i + 1])
return result
@staticmethod
def lowest(data: np.ndarray, period: int) -> np.ndarray:
"""滚动最低值。"""
result = np.full(len(data), np.nan, dtype=float)
for i in range(period - 1, len(data)):
result[i] = np.min(data[i - period + 1: i + 1])
return result
@staticmethod
def pct_change(data: np.ndarray, period: int = 1) -> np.ndarray:
"""百分比变化率。"""
result = np.full(len(data), np.nan, dtype=float)
for i in range(period, len(data)):
if data[i - period] != 0:
result[i] = (data[i] - data[i - period]) / data[i - period]
return result
{
"name": "dex-quant-skill",
"version": "2.0.0",
"tagline": "5-skill AI quant platform: strategy design → backtest → review → signal → execution",
"description": "AI 量化 Agent 平台。5 个独立 Skill 覆盖从自然语言策略设计、回测实现、回测评审、实时信号监控到风控执行的完整链路。围绕统一的 StrategySpec 对象工作,研究与实盘完全分离。",
"category": "finance",
"tags": [
"crypto",
"quantitative-trading",
"backtesting",
"perpetual-futures",
"strategy-design",
"signal-monitoring",
"risk-management",
"binance",
"defi"
],
"license": "MIT",
"author": "HashKey AI",
"repository": "https://github.com/miyaosk/dex-quant-skill",
"requires": {
"bins": ["python3"],
"packages": ["httpx", "loguru", "numpy", "pandas", "yfinance"]
}
}
httpx>=0.27
loguru>=0.7
numpy>=1.24
pandas>=2.0
yfinance>=0.2