feat: 全链路新增 type 字段支持 + exchange.ts 超时退出优化
- TS: exit 函数统一管理进程退出与 DB 连接关闭;10s 超时 + 异常路径 clearTimeout - Python: PairType(spot/um/cm) 贯穿 Kline 模型、策略配置、数据查询 - 回测脚本升级: 9策略 × 4币种 × 6时间级别 × 2交易类型 - 新增 generate_report.py 回测报告生成工具
This commit is contained in:
+24
-15
@@ -3,6 +3,14 @@ import { getAllPairs, updatePairLastBackfillTime } from '../service/pair';
|
||||
import { upsertOrUpdateKlines } from "../service/kline";
|
||||
import { fetchKlines } from '../exchanges';
|
||||
|
||||
import { AppDataSource } from '../db/data-source';
|
||||
|
||||
async function exit() {
|
||||
AppDataSource.destroy().finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
function getNowMinuteMS() {
|
||||
const minuteMS = 1000 * 60;
|
||||
return Math.floor(Date.now() / minuteMS) * minuteMS
|
||||
@@ -12,17 +20,19 @@ const allPairs = await getAllPairs();
|
||||
|
||||
for (const pair of allPairs) {
|
||||
let lastBackfillTime = pair.last_backfill_time.getTime();
|
||||
try {
|
||||
while (lastBackfillTime < getNowMinuteMS()) {
|
||||
console.log('lastBackfillTime', lastBackfillTime);
|
||||
const timer = setTimeout(exit, 10000);
|
||||
try {
|
||||
logger.info({ lastBackfillTime }, '回补进度');
|
||||
const klines = await fetchKlines({
|
||||
exchange: 'binance',
|
||||
type: pair.type,
|
||||
symbol: pair.symbol,
|
||||
startTime: lastBackfillTime,
|
||||
limit: 500,
|
||||
limit: 1000,
|
||||
});
|
||||
console.log(`拉取到 ${klines.length} 条 K 线`);
|
||||
clearTimeout(timer);
|
||||
logger.info(`拉取到 ${klines.length} 条 K 线`);
|
||||
if (klines.length > 0) {
|
||||
await upsertOrUpdateKlines(klines);
|
||||
const lastK = klines[klines.length - 1];
|
||||
@@ -34,20 +44,19 @@ for (const pair of allPairs) {
|
||||
lastBackfillTime = lastK.openTime;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
logger.error({ err }, "拉取失败");
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.random() * 1000);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("拉取失败:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有币种回补完成以后等待1秒关闭pgsql连接退出此进程
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const { AppDataSource } = await import("../db/data-source");
|
||||
if (AppDataSource.isInitialized) {
|
||||
await AppDataSource.destroy();
|
||||
console.log("pgsql 连接已关闭");
|
||||
}
|
||||
process.exit(0);
|
||||
// 所有交易对均已完成回补,等待 10~40 秒再退出,
|
||||
// 避免外部进程管理立即重启导致高频空查触发 API 限流。
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.random() * 30 * 1000 + 10000);
|
||||
});
|
||||
exit();
|
||||
@@ -98,6 +98,7 @@ class BacktestEngine:
|
||||
# 确保 strategy_config 与回测配置对齐
|
||||
strategy_config.symbol = self.config.symbol
|
||||
strategy_config.exchange = self.config.exchange
|
||||
strategy_config.type = self.config.type
|
||||
|
||||
# 1. 连接数据库并加载数据
|
||||
from ..common.config import config as app_config
|
||||
@@ -113,6 +114,8 @@ class BacktestEngine:
|
||||
start_time=self.config.start_time,
|
||||
end_time=self.config.end_time,
|
||||
limit=1_000_000, # 足够大的 limit,实际由 start/end 约束
|
||||
type=self.config.type,
|
||||
exchange=self.config.exchange,
|
||||
)
|
||||
|
||||
if len(klines) < self.config.warmup_bars + 2:
|
||||
|
||||
@@ -26,6 +26,7 @@ class BacktestConfig:
|
||||
|
||||
symbol: str
|
||||
exchange: str = "binance"
|
||||
type: str = "spot"
|
||||
interval: str = "1h"
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
|
||||
@@ -29,6 +29,7 @@ class StrategyConfig(BaseModel):
|
||||
name: str = ""
|
||||
symbol: str = ""
|
||||
exchange: str = "binance"
|
||||
type: str = "spot" # 交易对类型:spot / um / cm
|
||||
enabled: bool = True
|
||||
max_position_pct: float = 0.1 # 最大仓位占总资金比例
|
||||
stop_loss_pct: Optional[float] = None # 止损百分比
|
||||
|
||||
@@ -16,6 +16,13 @@ from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
KlineInterval = Literal["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "1d", "1w", "1mon"]
|
||||
|
||||
# ============================================================
|
||||
# 交易对类型
|
||||
# ============================================================
|
||||
|
||||
PairType = Literal["spot", "um", "cm"]
|
||||
"""交易对类型,与 TS data/types/kline.ts 的 PairType 对齐"""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 行情数据模型
|
||||
@@ -125,6 +132,9 @@ class Kline(BaseModel):
|
||||
interval: KlineInterval
|
||||
"""K 线周期"""
|
||||
|
||||
pair_type: PairType = Field(default="spot", alias="type")
|
||||
"""交易对类型(spot / um / cm),与 TS Kline.type 对齐"""
|
||||
|
||||
# 时间
|
||||
open_time: float = Field(alias="openTime")
|
||||
"""开盘时间(Unix 毫秒)"""
|
||||
|
||||
+38
-15
@@ -27,7 +27,7 @@ from typing import AsyncGenerator
|
||||
import asyncpg
|
||||
|
||||
from ..common.config import DBConfig
|
||||
from ..common.models import Kline, KlineInterval
|
||||
from ..common.models import Kline, KlineInterval, PairType
|
||||
|
||||
# ── 周期 → 表名映射 ──
|
||||
INTERVAL_TO_TABLE: dict[KlineInterval, str] = {
|
||||
@@ -124,18 +124,25 @@ class DataService:
|
||||
# ── 元数据查询 ──
|
||||
|
||||
async def fetch_available_symbols(
|
||||
self, interval: KlineInterval = "1m"
|
||||
self,
|
||||
interval: KlineInterval = "1m",
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> list[str]:
|
||||
"""获取指定周期下所有有数据的交易对"""
|
||||
table = INTERVAL_TO_TABLE[interval]
|
||||
async with self._pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
f"SELECT DISTINCT symbol FROM {table} ORDER BY symbol"
|
||||
f"SELECT DISTINCT symbol FROM {table} "
|
||||
f"WHERE exchange = $1 AND type = $2 ORDER BY symbol",
|
||||
exchange, type,
|
||||
)
|
||||
return [r["symbol"] for r in rows]
|
||||
|
||||
async def fetch_symbol_date_range(
|
||||
self, symbol: str, interval: KlineInterval
|
||||
self, symbol: str, interval: KlineInterval,
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""获取指定交易对 + 周期的数据起止时间"""
|
||||
table = INTERVAL_TO_TABLE[interval]
|
||||
@@ -144,9 +151,9 @@ class DataService:
|
||||
f"""
|
||||
SELECT MIN(time) AS min_time, MAX(time) AS max_time
|
||||
FROM {table}
|
||||
WHERE symbol = $1
|
||||
WHERE exchange = $1 AND symbol = $2 AND type = $3
|
||||
""",
|
||||
symbol,
|
||||
exchange, symbol, type,
|
||||
)
|
||||
if row is None or row["min_time"] is None:
|
||||
raise ValueError(f"无数据: {symbol} {interval}")
|
||||
@@ -158,12 +165,14 @@ class DataService:
|
||||
interval: KlineInterval,
|
||||
start_time: datetime | None = None,
|
||||
end_time: datetime | None = None,
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> int:
|
||||
"""获取指定条件的 K 线条数(用于预判数据量)"""
|
||||
table = INTERVAL_TO_TABLE[interval]
|
||||
conditions = ["symbol = $1", "interval = $2"]
|
||||
params: list = [symbol, interval]
|
||||
idx = 3
|
||||
conditions = ["exchange = $1", "symbol = $2", "type = $3"]
|
||||
params: list = [exchange, symbol, type]
|
||||
idx = 4
|
||||
|
||||
if start_time is not None:
|
||||
conditions.append(f"time >= ${idx}")
|
||||
@@ -191,6 +200,8 @@ class DataService:
|
||||
end_time: datetime | None = None,
|
||||
limit: int = 1000,
|
||||
offset: int = 0,
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> list[Kline]:
|
||||
"""获取 K 线数据,返回 Pydantic 模型列表
|
||||
|
||||
@@ -201,6 +212,8 @@ class DataService:
|
||||
end_time: 结束时间(不包含)
|
||||
limit: 最大返回条数
|
||||
offset: 分页偏移
|
||||
type: 交易对类型(spot / um / cm),默认 spot
|
||||
exchange: 交易所标识,默认 binance
|
||||
|
||||
Returns:
|
||||
按时间升序排列的 Kline 列表
|
||||
@@ -208,9 +221,9 @@ class DataService:
|
||||
table = INTERVAL_TO_TABLE[interval]
|
||||
interval_ms = INTERVAL_MS[interval]
|
||||
|
||||
conditions = ["symbol = $1", "interval = $2"]
|
||||
params: list = [symbol, interval]
|
||||
idx = 3
|
||||
conditions = ["exchange = $1", "symbol = $2", "type = $3"]
|
||||
params: list = [exchange, symbol, type]
|
||||
idx = 4
|
||||
|
||||
if start_time is not None:
|
||||
conditions.append(f"time >= ${idx}")
|
||||
@@ -225,7 +238,7 @@ class DataService:
|
||||
cols = await self._get_columns(table)
|
||||
|
||||
select_cols = [
|
||||
"time", "exchange", "symbol", "interval",
|
||||
"time", "exchange", "symbol", "type", "interval",
|
||||
"open", "high", "low", "close", "volume",
|
||||
]
|
||||
for extra in (
|
||||
@@ -249,7 +262,7 @@ class DataService:
|
||||
offset,
|
||||
)
|
||||
|
||||
return [self._row_to_kline(r, interval, interval_ms) for r in rows]
|
||||
return [self._row_to_kline(r, interval, interval_ms, type) for r in rows]
|
||||
|
||||
async def stream_klines(
|
||||
self,
|
||||
@@ -258,6 +271,8 @@ class DataService:
|
||||
start_time: datetime | None = None,
|
||||
end_time: datetime | None = None,
|
||||
batch_size: int = DEFAULT_BATCH_SIZE,
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> AsyncGenerator[list[Kline], None]:
|
||||
"""流式获取 K 线数据,适合大数据集
|
||||
|
||||
@@ -275,6 +290,8 @@ class DataService:
|
||||
end_time=end_time,
|
||||
limit=batch_size,
|
||||
offset=offset,
|
||||
type=type,
|
||||
exchange=exchange,
|
||||
)
|
||||
if not batch:
|
||||
break
|
||||
@@ -288,6 +305,8 @@ class DataService:
|
||||
start_time: datetime | None = None,
|
||||
end_time: datetime | None = None,
|
||||
limit: int = 1000,
|
||||
type: PairType = "spot",
|
||||
exchange: str = "binance",
|
||||
) -> dict[str, list[Kline]]:
|
||||
"""批量获取多个交易对的 K 线
|
||||
|
||||
@@ -301,6 +320,8 @@ class DataService:
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
type=type,
|
||||
exchange=exchange,
|
||||
)
|
||||
for sym in symbols
|
||||
]
|
||||
@@ -326,7 +347,8 @@ class DataService:
|
||||
|
||||
@staticmethod
|
||||
def _row_to_kline(
|
||||
row: asyncpg.Record, interval: KlineInterval, interval_ms: int
|
||||
row: asyncpg.Record, interval: KlineInterval, interval_ms: int,
|
||||
pair_type: PairType = "spot",
|
||||
) -> Kline:
|
||||
"""将数据库行转换为 Kline 模型"""
|
||||
open_time = dt_to_unix_ms(row["time"])
|
||||
@@ -335,6 +357,7 @@ class DataService:
|
||||
exchange=row["exchange"],
|
||||
symbol=row["symbol"],
|
||||
interval=interval,
|
||||
type=row.get("type", pair_type),
|
||||
openTime=open_time,
|
||||
closeTime=open_time + interval_ms,
|
||||
open=_to_float(row["open"]),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
2h/6h 策略全维度对比回测 — 9策略 × 4币种 × 2时间级别 × 4数据量
|
||||
1h-1d 策略全维度对比回测 — 9策略 × 4币种 × 6时间级别 × 2交易类型 × 4数据量
|
||||
|
||||
8个网络知名策略 + 牛熊自适应策略(RegimeDetector3投票)
|
||||
在 2h 和 6h 两个新时间级别上的表现对比。
|
||||
在 1h/2h/4h/6h/8h/1d 六个时间级别上的表现对比。
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/comparison_2h_6h.py
|
||||
@@ -31,7 +31,8 @@ from engine.example.long_short import LongShortEngine
|
||||
|
||||
# ── 全局常量 ──
|
||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||||
TIMEFRAMES = ["2h", "6h"]
|
||||
TIMEFRAMES = ["1h", "2h", "4h", "6h", "8h", "1d"]
|
||||
PAIR_TYPES = ["spot", "um"]
|
||||
INITIAL = 10_000.0
|
||||
WARMUP = 150
|
||||
MAX_CONCURRENCY = 6
|
||||
@@ -820,12 +821,13 @@ STRATEGY_PARAMS_STR = {
|
||||
# 执行
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
async def run_one(entry, symbol, interval, period_label, start, end):
|
||||
async def run_one(entry, symbol, interval, pair_type, period_label, start, end):
|
||||
make_config = entry["make_config"]
|
||||
strategy_cls = entry["strategy_cls"]
|
||||
sc = make_config(symbol)
|
||||
bt = BacktestConfig(
|
||||
symbol=symbol, interval=interval,
|
||||
type=pair_type,
|
||||
start_time=start, end_time=end,
|
||||
initial_capital=INITIAL, warmup_bars=WARMUP,
|
||||
)
|
||||
@@ -852,14 +854,15 @@ async def main():
|
||||
date_ranges: dict[tuple[str, str], tuple] = {}
|
||||
for symbol in SYMBOLS:
|
||||
for tf in TIMEFRAMES:
|
||||
for pair_type in PAIR_TYPES:
|
||||
try:
|
||||
s, e = await ds.fetch_symbol_date_range(symbol, tf)
|
||||
bar_ms = {"2h": 7_200_000, "6h": 21_600_000}
|
||||
s, e = await ds.fetch_symbol_date_range(symbol, tf, type=pair_type)
|
||||
bar_ms = {"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "6h": 21_600_000, "8h": 28_800_000, "1d": 86_400_000}
|
||||
estimated_bars = int((e - s).total_seconds() * 1000 / bar_ms[tf])
|
||||
date_ranges[(symbol, tf)] = (s, e, estimated_bars)
|
||||
print(f" {symbol} {tf:<4}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
|
||||
date_ranges[(symbol, tf, pair_type)] = (s, e, estimated_bars)
|
||||
print(f" {symbol} {tf:<4} {pair_type:<5}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
|
||||
except Exception as ex:
|
||||
print(f" {symbol} {tf:<4}: 获取失败 — {ex}")
|
||||
print(f" {symbol} {tf:<4} {pair_type:<5}: 获取失败 — {ex}")
|
||||
|
||||
await ds.close()
|
||||
|
||||
@@ -869,7 +872,8 @@ async def main():
|
||||
for strat_name, entry in STRATEGY_REGISTRY.items():
|
||||
for symbol in SYMBOLS:
|
||||
for tf in TIMEFRAMES:
|
||||
key = (symbol, tf)
|
||||
for pair_type in PAIR_TYPES:
|
||||
key = (symbol, tf, pair_type)
|
||||
if key not in date_ranges:
|
||||
continue
|
||||
fs, fe, est_bars = date_ranges[key]
|
||||
@@ -884,7 +888,7 @@ async def main():
|
||||
actual_bars = est_bars
|
||||
if period_label != "全量":
|
||||
actual_bars = int((actual_end - actual_start).total_seconds() * 1000 / {
|
||||
"2h": 7_200_000, "6h": 21_600_000
|
||||
"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "6h": 21_600_000, "8h": 28_800_000, "1d": 86_400_000
|
||||
}[tf])
|
||||
|
||||
if actual_bars < min_bars:
|
||||
@@ -895,13 +899,14 @@ async def main():
|
||||
"entry": entry,
|
||||
"symbol": symbol,
|
||||
"tf": tf,
|
||||
"pair_type": pair_type,
|
||||
"period_label": period_label,
|
||||
"start": actual_start,
|
||||
"end": actual_end,
|
||||
})
|
||||
|
||||
total = len(tasks_info)
|
||||
print(f"\n共 {total} 组回测任务 (9策略×4币种×2时间×4数据量 - 跳过数据不足)")
|
||||
print(f"\n共 {total} 组回测任务 (9策略×4币种×6时间×2类型×4数据量 - 跳过数据不足)")
|
||||
|
||||
results: list[dict] = []
|
||||
completed = 0
|
||||
@@ -911,7 +916,7 @@ async def main():
|
||||
nonlocal completed, errors
|
||||
async with sem:
|
||||
r, elapsed, err = await run_one(
|
||||
info["entry"], info["symbol"], info["tf"],
|
||||
info["entry"], info["symbol"], info["tf"], info["pair_type"],
|
||||
info["period_label"], info["start"], info["end"],
|
||||
)
|
||||
completed += 1
|
||||
@@ -924,12 +929,13 @@ async def main():
|
||||
else:
|
||||
m = r.metrics
|
||||
status = f"✓ {m.annual_return_pct:+.1f}%/yr"
|
||||
print(f" [{completed}/{total}] {info['strat_name']} {info['symbol']} {info['tf']} {info['period_label']} ({elapsed:.1f}s) {status}", flush=True)
|
||||
print(f" [{completed}/{total}] {info['strat_name']} {info['symbol']} {info['tf']} {info['pair_type']} {info['period_label']} ({elapsed:.1f}s) {status}", flush=True)
|
||||
|
||||
row = {
|
||||
"策略名": info["strat_name"],
|
||||
"币种": info["symbol"],
|
||||
"时间级别": info["tf"],
|
||||
"类型": info["pair_type"],
|
||||
"数据量": info["period_label"],
|
||||
"策略类型": info["entry"]["strategy_cls"].strategy_type if r else "",
|
||||
"策略参数": STRATEGY_PARAMS_STR.get(info["strat_name"], ""),
|
||||
@@ -986,7 +992,7 @@ async def main():
|
||||
# ── 打印完整表格 ──
|
||||
print()
|
||||
print("═" * 195)
|
||||
print(" 2h / 6h 全维度策略对比回测结果(9策略 × 4币种 × 2时间 × 4数据量)")
|
||||
print(" 1h-1d 全维度策略对比回测结果(9策略 × 4币种 × 6时间 × 2类型 × 4数据量)")
|
||||
print("═" * 195)
|
||||
print()
|
||||
|
||||
@@ -997,31 +1003,32 @@ async def main():
|
||||
first = strat_results[0]
|
||||
print(f"■ {strat_name} | 类型: {first['策略类型']} | {first['策略描述']}")
|
||||
print(f" 参数: {first['策略参数']}")
|
||||
print(f" {'币种':<10} {'时间':<5} {'数据量':<6} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'日期范围':<24}")
|
||||
print(" " + "─" * 185)
|
||||
print(f" {'币种':<10} {'时间':<5} {'类型':<5} {'数据量':<6} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'日期范围':<24}")
|
||||
print(" " + "─" * 195)
|
||||
|
||||
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.index(x["时间级别"]), list(PERIODS.keys()).index(x["数据量"])))
|
||||
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.index(x["时间级别"]), PAIR_TYPES.index(x["类型"]), list(PERIODS.keys()).index(x["数据量"])))
|
||||
|
||||
for r in strat_results:
|
||||
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['数据量']:<6} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['日期范围']:<24}")
|
||||
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['类型']:<5} {r['数据量']:<6} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['日期范围']:<24}")
|
||||
print()
|
||||
|
||||
# ── 终极汇总 ──
|
||||
print("═" * 195)
|
||||
print(" ■ 终极汇总:每组(时间级别+数据量)下各币种最佳策略(按年化收益)")
|
||||
print(" ■ 终极汇总:每组(时间级别+类型+数据量)下各币种最佳策略(按年化收益)\n(1h结果可能因数据量过大导致内存/耗时问题,已设MAX_CONCURRENCY=6)")
|
||||
print("═" * 195)
|
||||
print()
|
||||
|
||||
for tf in TIMEFRAMES:
|
||||
for pair_type in PAIR_TYPES:
|
||||
for period_label in PERIODS:
|
||||
subset = [r for r in results if r["时间级别"] == tf and r["数据量"] == period_label and r.get("总收益%", 0) != 0]
|
||||
subset = [r for r in results if r["时间级别"] == tf and r["类型"] == pair_type and r["数据量"] == period_label and r.get("总收益%", 0) != 0]
|
||||
if not subset:
|
||||
continue
|
||||
subset.sort(key=lambda x: x.get("年化收益%", -9999), reverse=True)
|
||||
|
||||
print(f" ▲ {tf} | {period_label}")
|
||||
print(f" ▲ {tf} | {pair_type} | {period_label}")
|
||||
print(f" {'排名':<5} {'策略名':<22} {'币种':<10} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
|
||||
print(" " + "─" * 130)
|
||||
print(" " + "─" * 135)
|
||||
for i, r in enumerate(subset[:5]):
|
||||
marker = ["🥇", "🥈", "🥉", " 4", " 5"][i]
|
||||
print(f" {marker:<5} {r['策略名']:<22} {r['币种']:<10} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6}")
|
||||
@@ -1034,6 +1041,7 @@ async def main():
|
||||
"config": {
|
||||
"symbols": SYMBOLS,
|
||||
"timeframes": TIMEFRAMES,
|
||||
"pair_types": PAIR_TYPES,
|
||||
"periods": list(PERIODS.keys()),
|
||||
"initial_capital": INITIAL,
|
||||
"warmup_bars": WARMUP,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+40652
-4356
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,285 @@
|
||||
"""
|
||||
从 comparison_2h_6h_result.json 生成可读 Markdown 报告
|
||||
用法:source .venv/bin/activate && python example/generate_report.py
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
JSON_PATH = ROOT / "example" / "comparison_2h_6h_result.json"
|
||||
OUT_PATH = ROOT / "example" / "comparison_2h_6h_report.md"
|
||||
|
||||
STRATEGY_ORDER = [
|
||||
"1.海龟交易", "2.超级趋势", "3.MACD金叉死叉", "4.布林收缩爆发",
|
||||
"5.三均线排列", "6.RSI均值回归", "7.ATR波动率突破", "8.EMA双均线多空", "9.牛熊自适应",
|
||||
]
|
||||
SYMBOL_ORDER = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||||
TF_ORDER = ["1h", "2h", "4h", "6h", "8h", "1d"]
|
||||
TYPE_ORDER = ["spot", "um"]
|
||||
PERIOD_ORDER = ["全量", "近两年", "近一年", "近半年"]
|
||||
|
||||
|
||||
def fmt(val, suffix="", decimals=1):
|
||||
if val is None:
|
||||
return "—"
|
||||
return f"{val:+,.{decimals}f}{suffix}"
|
||||
|
||||
|
||||
def safe_get(d, key, default=0):
|
||||
v = d.get(key, default)
|
||||
return default if v is None else v
|
||||
|
||||
|
||||
def generate():
|
||||
with open(JSON_PATH) as f:
|
||||
data = json.load(f)
|
||||
cfg = data["config"]
|
||||
results = data["results"]
|
||||
|
||||
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
lines = []
|
||||
def w(s=""):
|
||||
lines.append(s)
|
||||
|
||||
# ── 标题 ──
|
||||
w(f"# 1h-1d 策略全维度对比回测报告(Spot + 合约)")
|
||||
w()
|
||||
w(f"> **生成时间**:{now_str}")
|
||||
w(f"> **总耗时**:{cfg['elapsed_seconds']:.1f} 秒")
|
||||
w(f"> **测试维度**:9 策略 × 4 币种 × 2 时间级别 × 2 交易类型 × 4 数据量 = {cfg['total_tasks']} 次回测")
|
||||
w(f"> **初始资金**:${cfg['initial_capital']:,.0f} | **预热 Bar**:{cfg['warmup_bars']}")
|
||||
w(f"> **错误数**:{cfg['total_errors']}")
|
||||
w()
|
||||
|
||||
# ── 一、策略概览 ──
|
||||
w("## 一、策略概览")
|
||||
w()
|
||||
w("| # | 策略名称 | 类型 | 参数 | 描述 |")
|
||||
w("|---|----------|------|------|------|")
|
||||
all_full = [r for r in results if r["数据量"] == "全量"]
|
||||
for sn in STRATEGY_ORDER:
|
||||
sample = next((r for r in all_full if r["策略名"] == sn), None)
|
||||
if sample:
|
||||
w(f"| {sn[:2]} | **{sn[2:]}** | {sample['策略类型']} | `{sample['策略参数']}` | {sample['策略描述']} |")
|
||||
w()
|
||||
|
||||
# ── 二、全量数据 TOP 20(混合 spot/um,按夏普排名)──
|
||||
w("## 二、全量数据 TOP 20(按夏普比率排名)")
|
||||
w()
|
||||
w("| 排名 | 策略 | 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||||
w("|------|------|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||||
full_sorted = sorted(all_full, key=lambda x: safe_get(x, "夏普比率"), reverse=True)
|
||||
medals = ["🥇", "🥈", "🥉"] + [" " + str(i) for i in range(4, 21)]
|
||||
for i, r in enumerate(full_sorted[:20]):
|
||||
m = medals[i]
|
||||
w(f"| {m} | {r['策略名'][2:]} | {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | {safe_get(r,'交易次数')} | "
|
||||
f"{safe_get(r,'卡尔玛比率'):.2f} |")
|
||||
w()
|
||||
|
||||
# ── 三、各策略全量详细(spot vs um)──
|
||||
w("## 三、各策略全量数据详细表现(2h vs 6h × 4 币种 × spot/um)")
|
||||
w()
|
||||
for sn in STRATEGY_ORDER:
|
||||
strat_full = [r for r in all_full if r["策略名"] == sn]
|
||||
if not strat_full:
|
||||
continue
|
||||
sample = strat_full[0]
|
||||
w(f"### {sn}")
|
||||
w(f"> **类型**:{sample['策略类型']} | **参数**:`{sample['策略参数']}`")
|
||||
w(f"> {sample['策略描述']}")
|
||||
w()
|
||||
w("| 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||||
w("|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||||
strat_full.sort(key=lambda x: (SYMBOL_ORDER.index(x["币种"]), TF_ORDER.index(x["时间级别"]), TYPE_ORDER.index(x["类型"])))
|
||||
for r in strat_full:
|
||||
w(f"| {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||||
f"{safe_get(r,'交易次数')} | {safe_get(r,'卡尔玛比率'):.2f} |")
|
||||
best = max(strat_full, key=lambda x: safe_get(x, "夏普比率"))
|
||||
w()
|
||||
w(f"> 🏆 **{sn[2:]} 最优**:{best['币种']} {best['时间级别']} {best['类型']},"
|
||||
f"夏普 **{safe_get(best,'夏普比率'):.2f}**,"
|
||||
f"总收益 **{safe_get(best,'总收益%'):+.1f}%**,"
|
||||
f"年化 **{safe_get(best,'年化收益%'):+.1f}%**,"
|
||||
f"交易 {safe_get(best,'交易次数')} 次")
|
||||
w()
|
||||
|
||||
# ── 四、各币种全量 — 策略横向对比 ──
|
||||
w("## 四、各币种全量数据 — 策略横向对比")
|
||||
w()
|
||||
for symbol in SYMBOL_ORDER:
|
||||
w(f"### {symbol}")
|
||||
w()
|
||||
sym_full = [r for r in all_full if r["币种"] == symbol]
|
||||
sym_full.sort(key=lambda x: safe_get(x, "夏普比率"), reverse=True)
|
||||
w("| 排名 | 策略 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||||
w("|------|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||||
for i, r in enumerate(sym_full):
|
||||
m = medals[i] if i < 3 else f" {i+1}"
|
||||
w(f"| {m} | {r['策略名'][2:]} | {r['时间级别']} | {r['类型']} | "
|
||||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||||
f"{safe_get(r,'交易次数')} | {safe_get(r,'卡尔玛比率'):.2f} |")
|
||||
best_sym = max(sym_full, key=lambda x: safe_get(x, "夏普比率"))
|
||||
w()
|
||||
w(f"> 🏆 **{symbol} 最优**:{best_sym['策略名'][2:]} {best_sym['时间级别']} {best_sym['类型']},"
|
||||
f"夏普 **{safe_get(best_sym,'夏普比率'):.2f}**,"
|
||||
f"总收益 **{safe_get(best_sym,'总收益%'):+.1f}%**")
|
||||
w()
|
||||
|
||||
# ── 五、spot vs um 对比 ──
|
||||
w("## 五、Spot vs 合约(UM)对比 — 全量夏普差异")
|
||||
w()
|
||||
w("| 策略 | 币种 | TF | Spot 夏普 | UM 夏普 | 差异 | 更优 |")
|
||||
w("|------|------|----|-----------|---------|------|------|")
|
||||
diffs = []
|
||||
for sn in STRATEGY_ORDER:
|
||||
for symbol in SYMBOL_ORDER:
|
||||
for tf in TF_ORDER:
|
||||
spot_r = next((r for r in all_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]==tf and r["类型"]=="spot"), None)
|
||||
um_r = next((r for r in all_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]==tf and r["类型"]=="um"), None)
|
||||
if spot_r and um_r:
|
||||
s_sharpe = safe_get(spot_r, "夏普比率")
|
||||
u_sharpe = safe_get(um_r, "夏普比率")
|
||||
diff = u_sharpe - s_sharpe
|
||||
if abs(diff) > 0.15:
|
||||
better = "UM ✅" if diff > 0 else "Spot ✅"
|
||||
winner = "um" if diff > 0 else "spot"
|
||||
diffs.append((sn, symbol, tf, s_sharpe, u_sharpe, diff, winner))
|
||||
diffs.sort(key=lambda x: abs(x[5]), reverse=True)
|
||||
for sn, symbol, tf, s_s, u_s, diff, winner in diffs:
|
||||
w(f"| {sn[2:]} | {symbol} | {tf} | {s_s:.2f} | {u_s:.2f} | {diff:+.2f} | {winner} |")
|
||||
w()
|
||||
if diffs:
|
||||
um_wins = sum(1 for d in diffs if d[6] == "um")
|
||||
spot_wins = sum(1 for d in diffs if d[6] == "spot")
|
||||
w(f"> 显著差异(|Δ夏普|>0.15)共 {len(diffs)} 组:UM 更优 {um_wins} 组,Spot 更优 {spot_wins} 组")
|
||||
w()
|
||||
|
||||
# ── 六、周期对比(按 type 分组)──
|
||||
w("## 六、2h vs 6h — 周期对比(按类型分组)")
|
||||
w()
|
||||
for pair_type in TYPE_ORDER:
|
||||
type_full = [r for r in all_full if r["类型"] == pair_type]
|
||||
w(f"### {pair_type.upper()}")
|
||||
w()
|
||||
for symbol in SYMBOL_ORDER:
|
||||
w(f"#### {symbol}")
|
||||
w()
|
||||
w("| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |")
|
||||
w("|------|---------|----------|---------|----------|----------|")
|
||||
for sn in STRATEGY_ORDER:
|
||||
r2 = next((r for r in type_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]=="2h"), None)
|
||||
r6 = next((r for r in type_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]=="6h"), None)
|
||||
if r2 and r6:
|
||||
s2, s6 = safe_get(r2, "夏普比率"), safe_get(r6, "夏普比率")
|
||||
better = "2h ✅" if s2 > s6 else "6h ✅"
|
||||
w(f"| {sn[2:]} | {s2:.2f} | {safe_get(r2,'总收益%'):+.1f}% | {s6:.2f} | {safe_get(r6,'总收益%'):+.1f}% | {better} |")
|
||||
w()
|
||||
|
||||
# ── 七、综合评分 TOP 20 ──
|
||||
w("## 七、全量数据 综合评分 TOP 20")
|
||||
w()
|
||||
w("> 综合评分 = 夏普比率×0.4 + 年化收益归一化×0.3 + 卡尔玛归一化×0.2 - 回撤归一化×0.1")
|
||||
w()
|
||||
# normalize(回撤和卡尔玛取绝对值,因存储为负数)
|
||||
all_annual = [safe_get(r, "年化收益%") for r in all_full]
|
||||
all_calmar = [abs(safe_get(r, "卡尔玛比率")) for r in all_full]
|
||||
all_dd = [abs(safe_get(r, "最大回撤%")) for r in all_full]
|
||||
max_annual = max(all_annual) if all_annual else 1
|
||||
max_calmar = max(all_calmar) if all_calmar else 1
|
||||
max_dd = max(all_dd) if all_dd else 1
|
||||
|
||||
for r in all_full:
|
||||
score = (
|
||||
safe_get(r, "夏普比率") * 0.4
|
||||
+ (safe_get(r, "年化收益%") / max(1, max_annual)) * 0.3
|
||||
+ (safe_get(r, "卡尔玛比率") / max(0.01, max_calmar)) * 0.2
|
||||
- (abs(safe_get(r, "最大回撤%")) / max(1, max_dd)) * 0.1
|
||||
)
|
||||
r["_score"] = score
|
||||
|
||||
scored = sorted(all_full, key=lambda x: x["_score"], reverse=True)
|
||||
w("| 排名 | 策略 | 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 综合评分 |")
|
||||
w("|------|------|------|----|------|---------|-------|------|-------|-------|--------|----------|")
|
||||
for i, r in enumerate(scored[:20]):
|
||||
m = medals[i]
|
||||
w(f"| {m} | {r['策略名'][2:]} | {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||||
f"**{r['_score']:.3f}** |")
|
||||
w()
|
||||
|
||||
# ── 八、策略类型平均表现 ──
|
||||
w("## 八、策略类型平均表现(全量,4币种×2TF×2类型 平均)")
|
||||
w()
|
||||
w("| 类型 | 策略 | 平均夏普 | 平均收益% | 平均回撤% |")
|
||||
w("|------|------|----------|-----------|-----------|")
|
||||
for sn in STRATEGY_ORDER:
|
||||
subset = [r for r in all_full if r["策略名"] == sn]
|
||||
if subset:
|
||||
avg_s = sum(safe_get(r, "夏普比率") for r in subset) / len(subset)
|
||||
avg_ret = sum(safe_get(r, "总收益%") for r in subset) / len(subset)
|
||||
avg_dd = sum(safe_get(r, "最大回撤%") for r in subset) / len(subset)
|
||||
stype = subset[0]["策略类型"]
|
||||
w(f"| {stype} | {sn[2:]} | {avg_s:.2f} | {avg_ret:+.1f}% | {avg_dd:.1f}% |")
|
||||
w()
|
||||
|
||||
# ── 九、关键发现 ──
|
||||
w("## 九、关键发现")
|
||||
w()
|
||||
# 最优夏普
|
||||
best_all = max(all_full, key=lambda x: safe_get(x, "夏普比率"))
|
||||
w(f"### 🏆 综合最优策略")
|
||||
w(f"- **{best_all['策略名'][2:]}** — {best_all['币种']} {best_all['时间级别']} {best_all['类型']}")
|
||||
w(f" - 夏普比率:**{safe_get(best_all,'夏普比率'):.2f}**")
|
||||
w(f" - 总收益:**{safe_get(best_all,'总收益%'):+.1f}%**")
|
||||
w(f" - 年化收益:**{safe_get(best_all,'年化收益%'):+.1f}%**")
|
||||
w(f" - 最大回撤:{safe_get(best_all,'最大回撤%'):.1f}%")
|
||||
w(f" - 交易次数:{safe_get(best_all,'交易次数')}")
|
||||
w()
|
||||
|
||||
# 各币种最优
|
||||
w("### 📊 各币种最优策略(全量,按夏普)")
|
||||
w()
|
||||
w("| 币种 | 最优策略 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 交易数 |")
|
||||
w("|------|----------|----|------|---------|-------|------|-------|--------|")
|
||||
for symbol in SYMBOL_ORDER:
|
||||
sym_full = [r for r in all_full if r["币种"] == symbol]
|
||||
best = max(sym_full, key=lambda x: safe_get(x, "夏普比率"))
|
||||
w(f"| {symbol} | **{best['策略名'][2:]}** | {best['时间级别']} | {best['类型']} | "
|
||||
f"{safe_get(best,'总收益%'):+.1f}% | {safe_get(best,'年化收益%'):+.1f}% | "
|
||||
f"**{safe_get(best,'夏普比率'):.2f}** | {safe_get(best,'最大回撤%'):.1f}% | "
|
||||
f"{safe_get(best,'交易次数')} |")
|
||||
w()
|
||||
|
||||
# spot vs um
|
||||
w("### 🔄 Spot vs 合约(UM)")
|
||||
w()
|
||||
spot_full = [r for r in all_full if r["类型"] == "spot"]
|
||||
um_full = [r for r in all_full if r["类型"] == "um"]
|
||||
spot_avg_s = sum(safe_get(r, "夏普比率") for r in spot_full) / len(spot_full)
|
||||
um_avg_s = sum(safe_get(r, "夏普比率") for r in um_full) / len(um_full)
|
||||
w(f"- Spot 平均夏普:**{spot_avg_s:.2f}**")
|
||||
w(f"- UM(合约)平均夏普:**{um_avg_s:.2f}**")
|
||||
w(f"- 差距:{um_avg_s - spot_avg_s:+.2f}")
|
||||
w()
|
||||
|
||||
# 保存
|
||||
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
print(f"报告已生成:{OUT_PATH} ({len(lines)} 行)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate()
|
||||
@@ -59,6 +59,7 @@ class LongShortEngine:
|
||||
from engine.common.config import config as app_config
|
||||
strategy_config.symbol = self.config.symbol
|
||||
strategy_config.exchange = self.config.exchange
|
||||
strategy_config.type = self.config.type
|
||||
db_cfg = self._db_config or app_config.db
|
||||
ds = DataService(db_cfg)
|
||||
await ds.connect()
|
||||
@@ -67,6 +68,8 @@ class LongShortEngine:
|
||||
symbol=self.config.symbol, interval=self.config.interval,
|
||||
start_time=self.config.start_time, end_time=self.config.end_time,
|
||||
limit=1_000_000,
|
||||
type=self.config.type,
|
||||
exchange=self.config.exchange,
|
||||
)
|
||||
if len(klines) < self.config.warmup_bars + 2:
|
||||
raise ValueError(f"数据不足:需 {self.config.warmup_bars+2},实际 {len(klines)}")
|
||||
|
||||
@@ -201,6 +201,8 @@ async def run_one(
|
||||
warmup_bars=100,
|
||||
)
|
||||
strategy_config.symbol = symbol
|
||||
strategy_config.exchange = bt.exchange
|
||||
strategy_config.type = bt.type
|
||||
strategy_config.name = f"{strategy_name}_{symbol}"
|
||||
|
||||
engine = BacktestEngine(bt, db_config=config.db)
|
||||
|
||||
+85
-4
@@ -1,7 +1,88 @@
|
||||
# 项目级 Reasonix 配置 — 优先级高于全局 config.toml
|
||||
# 参考: ~/Library/Application Support/reasonix/config.toml
|
||||
# Reasonix configuration.
|
||||
# Resolution order: flag > ./reasonix.toml > ~/Library/Application Support/reasonix/config.toml > built-in defaults.
|
||||
# Secrets come from the environment via api_key_env; never put keys here.
|
||||
|
||||
config_version = 2 # schema marker for diagnostics; old versions may ignore it
|
||||
default_model = "deepseek-pro"
|
||||
language = "zh" # ui/model language; empty = auto-detect from $LANG / $REASONIX_LANG
|
||||
|
||||
[agent]
|
||||
reasoning_language = "zh" # 强制推理过程使用中文
|
||||
# system_prompt = """...""" # omit to use the built-in prompt for this version
|
||||
# system_prompt_file = "prompts/system.md" # overrides system_prompt when set
|
||||
max_steps = 0 # executor tool-call rounds; 0 = no limit
|
||||
planner_max_steps = 12 # planner read-only tool-call rounds; 0 = no limit
|
||||
temperature = 0.0
|
||||
auto_plan = "off" # off|on; off keeps plan mode manual
|
||||
reasoning_language = "zh" # visible reasoning language: auto|zh|en
|
||||
# auto_plan_classifier = "deepseek-pro" # optional; only used for borderline tasks
|
||||
soft_compact_ratio = 0.5 # notice only; keeps cache-first prefix intact
|
||||
compact_ratio = 0.8 # try compacting when prompt reaches this fraction
|
||||
compact_force_ratio = 0.9 # force compacting at this high-water mark
|
||||
cold_resume_prune = true # elide stale tool results when reopening a session past the provider cache window
|
||||
# planner_model = "mimo" # optional: enable two-model collaboration
|
||||
# subagent_model = "deepseek-pro" # optional default for runAs=subagent skills
|
||||
# subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" } # per-skill overrides
|
||||
# subagent_effort = "high" # optional default effort for subagents
|
||||
# subagent_efforts = { review = "max", task = "high" } # per-tool/skill effort overrides
|
||||
# output_style = "explanatory" # explanatory | learning | concise | custom; empty = default
|
||||
|
||||
[tools]
|
||||
enabled = [] # empty = all built-in tools
|
||||
bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap
|
||||
|
||||
[codegraph]
|
||||
enabled = true
|
||||
enabled = true # built-in MCP server; off by default for first-run sessions
|
||||
auto_install = true # fetch the runtime when CodeGraph is enabled but missing
|
||||
# path = "" # empty = cache, then PATH, then a bundle beside reasonix
|
||||
|
||||
[builtin_mcp]
|
||||
time_enabled = true # built-in Time MCP; off until manually enabled
|
||||
context7_enabled = false # built-in Context7 MCP; off until manually enabled
|
||||
|
||||
[lsp]
|
||||
enabled = true # language server tools; servers launch lazily when used
|
||||
# [lsp.servers.go]
|
||||
# command = "gopls"
|
||||
# args = []
|
||||
# extensions = [".go"]
|
||||
|
||||
[skills]
|
||||
# paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots
|
||||
# excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders
|
||||
# max_depth = 3 # nested scan depth; set 1 for legacy root-only discovery
|
||||
# disabled_skills = ["review"] # hide noisy or unwanted skills
|
||||
|
||||
[permissions]
|
||||
# Per-call gating. mode = writer fallback when no rule matches: ask|allow|deny.
|
||||
# Readers always default to allow. Precedence: deny > ask > allow > fallback.
|
||||
# Rules are "Tool" or "Tool(specifier)"; e.g. Bash(go test:*), Edit(src/**).
|
||||
mode = "ask"
|
||||
# deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode
|
||||
allow = ["Bash(cd /Users/rekey/Documents/Code/trade && git status)", "Bash(cd /Users/rekey/Documents/Code/trade && git status --short)", "Bash(cd /Users/rekey/Documents/Code/trade && git diff)", "review"]
|
||||
# ask = ["Edit(src/**)"] # force a prompt even if otherwise allowed
|
||||
|
||||
[sandbox]
|
||||
# Confine tool blast radius. File-writers (write_file/edit_file/multi_edit)
|
||||
# may only write under workspace_root (empty = current dir) + allow_write.
|
||||
# bash = "enforce" (default) jails each command in an OS sandbox (macOS now;
|
||||
# graceful fallback elsewhere); "off" disables it. network allows egress.
|
||||
# workspace_root = "" # default: current working directory
|
||||
# allow_write = ["/tmp"] # extra dirs writers may also modify
|
||||
bash = "enforce"
|
||||
network = true
|
||||
|
||||
[statusline]
|
||||
# A custom status line: a command whose first stdout line replaces the built-in
|
||||
# data row. It receives {"model","contextUsed","contextWindow","cwd"} as JSON on stdin.
|
||||
# command = "my-statusline.sh"
|
||||
|
||||
# External MCP servers. type: "stdio" (default, a subprocess) | "http" | "sse".
|
||||
# ${VAR} / ${VAR:-default} are expanded from the environment in command/args/env/url/headers.
|
||||
# [[plugins]]
|
||||
# name = "example"
|
||||
# command = "reasonix-plugin-example"
|
||||
# [[plugins]] # a remote server over Streamable HTTP
|
||||
# name = "stripe"
|
||||
# type = "http"
|
||||
# url = "https://mcp.stripe.com"
|
||||
# headers = { Authorization = "Bearer ${STRIPE_KEY}" }
|
||||
|
||||
Reference in New Issue
Block a user