Files

365 lines
10 KiB
Python
Raw Permalink Normal View History

2026-07-09 21:23:10 +08:00
"""
策略模板生成器 — 为 AI agent 提供标准化起点
支持模板:
- crossover: 均线交叉 (SMA/EMA)
- mean_reversion: RSI/Bollinger 均值回归
- trend_following: ADX + DI 趋势跟踪
- breakout: Donchian 通道突破
- custom: 空白模板
用法:
from scaffold import scaffold_strategy
path = scaffold_strategy("my_rsi", template="mean_reversion",
description="RSI 超卖反弹策略")
print(f"模板已生成: {path}")
# 接着编辑 strategies/my_rsi.py 填入具体逻辑
"""
from __future__ import annotations
import os
def _to_class_name(name: str) -> str:
"""snake_case → PascalCase (my_strategy → MyStrategy)"""
return "".join(w.capitalize() for w in name.split("_"))
# 策略文件头部注释 (包含前视偏差警告, 提醒 AI agent 遵守)
_HEADER = '''"""{name}{description}
⚠️ 前视偏差 (Look-Ahead Bias) 注意事项:
信号生成时只能用当前 bar 及之前的数据, 严禁使用未来 bar。
以下模式会引入前视偏差, 必须避免:
- .shift(-N) # 访问未来 bar (N>0)
- close[-1] / high[-1] # 负索引访问未来
- df.iloc[i+N:] # 切片到未来索引
- 滚动统计后 shift 负值
正确做法:
- 用 cross_above / cross_below (基类已内置前视安全)
- 信号在 bar 收盘后生成, 用 close 成交 (引擎默认 upon_bar_close=True)
- 检测: python -m app.main check {name}
"""
'''
_TEMPLATES = {
"crossover": '''"""{name}{description}"""
from __future__ import annotations
import numpy as np
import raptorbt
from .base import Strategy, SignalResult
class {ClassName}Strategy(Strategy):
"""{description}"""
name = "{name}"
def __init__(self, fast: int = 10, slow: int = 20):
self.fast = fast
self.slow = slow
def warmup_bars(self) -> int:
return self.slow + 1
def generate_signals(self, df) -> SignalResult:
close = df["close"].values.astype(np.float64)
ma_fast = raptorbt.sma(close, period=self.fast)
ma_slow = raptorbt.sma(close, period=self.slow)
entries = self.cross_above(ma_fast, ma_slow).astype(bool)
exits = self.cross_below(ma_fast, ma_slow).astype(bool)
entries, exits = self.apply_warmup(entries, exits)
return SignalResult(
entries=entries, exits=exits, direction=1,
extra={{"ma_fast": ma_fast, "ma_slow": ma_slow}},
)
def build_config(self) -> raptorbt.PyBacktestConfig:
config = raptorbt.PyBacktestConfig(
initial_capital=100000.0, fees=0.001, slippage=0.0005,
)
config.set_fixed_stop(0.02)
config.set_fixed_target(0.04)
return config
def description(self) -> str:
return f"SMA({{self.fast}})/SMA({{self.slow}}) 交叉, 2% 止损/4% 止盈"
STRATEGY_CLASS = {ClassName}Strategy
''',
"mean_reversion": '''"""{name}{description}"""
from __future__ import annotations
import numpy as np
import raptorbt
from .base import Strategy, SignalResult
class {ClassName}Strategy(Strategy):
"""{description}"""
name = "{name}"
def __init__(self, period: int = 14, oversold: float = 30.0, overbought: float = 70.0):
self.period = period
self.oversold = oversold
self.overbought = overbought
def warmup_bars(self) -> int:
return self.period + 1
def generate_signals(self, df) -> SignalResult:
close = df["close"].values.astype(np.float64)
rsi = raptorbt.rsi(close, period=self.period)
entries = (rsi < self.oversold).astype(bool)
exits = (rsi > self.overbought).astype(bool)
entries, exits = self.apply_warmup(entries, exits)
return SignalResult(
entries=entries, exits=exits, direction=1,
extra={{"rsi": rsi}},
)
def build_config(self) -> raptorbt.PyBacktestConfig:
config = raptorbt.PyBacktestConfig(
initial_capital=100000.0, fees=0.001, slippage=0.0005,
)
config.set_trailing_stop(0.03)
return config
def description(self) -> str:
return f"RSI({{self.period}}) 均值回归, <{{self.oversold}} 买入 / >{{self.overbought}} 卖出"
STRATEGY_CLASS = {ClassName}Strategy
''',
"trend_following": '''"""{name}{description}"""
from __future__ import annotations
import numpy as np
import raptorbt
from .base import Strategy, SignalResult
class {ClassName}Strategy(Strategy):
"""{description}"""
name = "{name}"
def __init__(self, adx_period: int = 14, adx_threshold: float = 25.0):
self.adx_period = adx_period
self.adx_threshold = adx_threshold
def warmup_bars(self) -> int:
return 2 * self.adx_period + 10
def generate_signals(self, df) -> SignalResult:
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
adx, plus_di, minus_di = raptorbt.adx_all(high, low, close, period=self.adx_period)
adx_strong = adx > self.adx_threshold
entries = (adx_strong & (plus_di > minus_di)).astype(bool)
exits = (adx_strong & (minus_di > plus_di)).astype(bool)
entries, exits = self.apply_warmup(entries, exits)
return SignalResult(
entries=entries, exits=exits, direction=1,
extra={{"adx": adx, "plus_di": plus_di, "minus_di": minus_di}},
)
def build_config(self) -> raptorbt.PyBacktestConfig:
config = raptorbt.PyBacktestConfig(
initial_capital=100000.0, fees=0.001, slippage=0.0005,
)
config.set_atr_stop(multiplier=2.5, period=14)
config.set_fixed_target(0.05)
return config
def description(self) -> str:
return f"ADX({{self.adx_period}})>{{self.adx_threshold}} + DI 方向确认, 2.5×ATR 止损"
STRATEGY_CLASS = {ClassName}Strategy
''',
"breakout": '''"""{name}{description}"""
from __future__ import annotations
import numpy as np
import raptorbt
from .base import Strategy, SignalResult
class {ClassName}Strategy(Strategy):
"""{description}"""
name = "{name}"
def __init__(self, period: int = 20):
self.period = period
def warmup_bars(self) -> int:
return self.period + 1
def generate_signals(self, df) -> SignalResult:
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
upper, middle, lower = raptorbt.donchian(high, low, period=self.period)
entries = (close > np.roll(upper, 1)).astype(bool)
exits = (close < np.roll(lower, 1)).astype(bool)
entries, exits = self.apply_warmup(entries, exits)
return SignalResult(
entries=entries, exits=exits, direction=1,
extra={{"donchian_upper": upper, "donchian_lower": lower}},
)
def build_config(self) -> raptorbt.PyBacktestConfig:
config = raptorbt.PyBacktestConfig(
initial_capital=100000.0, fees=0.001, slippage=0.0005,
)
config.set_trailing_stop(0.05)
return config
def description(self) -> str:
return f"Donchian({{self.period}}) 通道突破, 5% 追踪止损"
STRATEGY_CLASS = {ClassName}Strategy
''',
"custom": '''"""{name}{description}"""
from __future__ import annotations
import numpy as np
import raptorbt
from .base import Strategy, SignalResult
class {ClassName}Strategy(Strategy):
"""{description}"""
name = "{name}"
def __init__(self, period: int = 14):
self.period = period
def warmup_bars(self) -> int:
return self.period + 1
def generate_signals(self, df) -> SignalResult:
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
volume = df.get("tick_volume", df.get("volume")).values.astype(np.float64)
# TODO: 在此添加指标计算和信号生成逻辑
# 例如:
# rsi = raptorbt.rsi(close, period=self.period)
# entries = rsi < 30
# exits = rsi > 70
entries = np.zeros(len(close), dtype=bool)
exits = np.zeros(len(close), dtype=bool)
entries, exits = self.apply_warmup(entries, exits)
return SignalResult(entries=entries, exits=exits, direction=1)
def build_config(self) -> raptorbt.PyBacktestConfig:
config = raptorbt.PyBacktestConfig(
initial_capital=100000.0, fees=0.001, slippage=0.0005,
)
config.set_fixed_stop(0.02)
config.set_fixed_target(0.04)
return config
def description(self) -> str:
return "{description}"
STRATEGY_CLASS = {ClassName}Strategy
''',
}
def scaffold_strategy(
name: str,
template: str = "custom",
description: str = "",
overwrite: bool = False,
) -> str:
"""
生成策略模板文件
参数:
name: 策略名称 (snake_case, 如 "my_rsi")
template: 模板类型 (crossover/mean_reversion/trend_following/breakout/custom)
description: 策略描述文字
overwrite: 是否覆盖已存在的文件
返回:
生成的文件路径
"""
if template not in _TEMPLATES:
raise ValueError(
f"未知模板 '{template}', 可选: {', '.join(_TEMPLATES.keys())}"
)
class_name = _to_class_name(name)
if not description:
description = f"{template} 策略"
# 组装: 头部警告 + 模板正文 (去掉模板自带的 docstring 行)
header = _HEADER.format(name=name, description=description)
template_body = _TEMPLATES[template].format(
name=name,
ClassName=class_name,
description=description,
)
# 去掉模板第一行的 docstring (已被 _HEADER 取代)
template_body = template_body.split("\n", 1)[1] if template_body.startswith('"""') else template_body
code = header + template_body
file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"strategies",
f"{name}.py",
)
if os.path.exists(file_path) and not overwrite:
raise FileExistsError(
f"策略文件已存在: {file_path}\n使用 overwrite=True 覆盖"
)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(code)
return file_path