"""Seed a set of public trading strategies for the strategy library. Usage (from backend directory): uvicorn app.main:app --reload # make sure DB is up python -m app.scripts.seed_public_strategies This script is idempotent: if a strategy with the same name already exists, it will be skipped. """ from __future__ import annotations import logging from typing import List from sqlalchemy.orm import Session from app.database import SessionLocal from app.models.database import Strategy, User logger = logging.getLogger(__name__) SEED_OWNER_EMAIL = "public-strategy-seeder@local" def get_or_create_seed_user(db: Session) -> User: """Return a dedicated user to own seeded public strategies. The user is not intended for login, only as a logical owner of global public strategies. """ user = ( db.query(User) .filter(User.email == SEED_OWNER_EMAIL, User.deleted_at.is_(None)) .first() ) if user: return user logger.info("Creating seed user %s", SEED_OWNER_EMAIL) user = User( email=SEED_OWNER_EMAIL, password_hash=None, name="Public Strategy Seeder", is_active=True, is_verified=False, ) db.add(user) db.commit() db.refresh(user) return user def seed_strategies(db: Session, owner: User) -> None: """Insert a set of example public strategies. Only name / description / pairs / timeframe / code / stats are seeded. If a strategy with the same name already exists, it is skipped. """ # NOTE: these are simple, valid Freqtrade-style templates with different # flavours (trend following, mean reversion, breakout, volatility, etc.). # Users are expected to further optimise them via the Strategy Lab. base_pairs = ["BTC/USDT"] seeds: List[dict] = [ { "name": "BTC 趋势跟随 1h", "description": "基于 EMA 交叉和成交量过滤的 BTC/USDT 1 小时趋势跟随策略。", "pairs": base_pairs, "timeframe": "1h", "code": _SIMPLE_TREND_STRATEGY, "avg_profit": 25.4, "backtest_count": 12, }, { "name": "BTC 区间震荡 4h", "description": "使用 RSI 和布林带识别震荡区间,在超卖买入、超买卖出。", "pairs": base_pairs, "timeframe": "4h", "code": _RANGE_MEAN_REVERSION_STRATEGY, "avg_profit": 18.7, "backtest_count": 9, }, { "name": "BTC 突破跟随 1h", "description": "价格突破近期高点同时放量时顺势入场,追踪止损保护利润。", "pairs": base_pairs, "timeframe": "1h", "code": _BREAKOUT_STRATEGY, "avg_profit": 32.1, "backtest_count": 7, }, { "name": "BTC 波动率收缩 1h", "description": "结合 ATR 波动率收缩与放量突破,捕捉趋势起点。", "pairs": base_pairs, "timeframe": "1h", "code": _VOLATILITY_CONTRACTION_STRATEGY, "avg_profit": 21.3, "backtest_count": 6, }, { "name": "BTC 日内反转 30m", "description": "30 分钟级别的日内短线反转策略,利用前一日高低点与短期 RSI。", "pairs": base_pairs, "timeframe": "30m", "code": _INTRADAY_REVERSAL_STRATEGY, "avg_profit": 15.6, "backtest_count": 10, }, { "name": "ETH 趋势跟随 1h", "description": "将 BTC 趋势模板迁移到 ETH/USDT,适合中短线趋势持有。", "pairs": ["ETH/USDT"], "timeframe": "1h", "code": _SIMPLE_TREND_STRATEGY, "avg_profit": 27.8, "backtest_count": 8, }, { "name": "ETH 区间震荡 4h", "description": "ETH/USDT 4 小时级别的 RSI + 布林带震荡策略。", "pairs": ["ETH/USDT"], "timeframe": "4h", "code": _RANGE_MEAN_REVERSION_STRATEGY, "avg_profit": 17.2, "backtest_count": 5, }, { "name": "BNB 趋势跟随 1h", "description": "适配 BNB/USDT 的 EMA 趋势策略,降低交易频率,适合波段。", "pairs": ["BNB/USDT"], "timeframe": "1h", "code": _SIMPLE_TREND_STRATEGY, "avg_profit": 19.9, "backtest_count": 4, }, { "name": "多币种动量轮动 日线", "description": "在 BTC/ETH/BNB 之间按最近 N 日动量轮动持仓,偏中长期。", "pairs": ["BTC/USDT", "ETH/USDT", "BNB/USDT"], "timeframe": "1d", "code": _MOMENTUM_ROTATION_STRATEGY, "avg_profit": 28.5, "backtest_count": 11, }, { "name": "稳健现货网格 日线", "description": "针对 BTC/USDT 的简化现货网格模板,适合作为学习示例。", "pairs": base_pairs, "timeframe": "1d", "code": _SIMPLE_GRID_STRATEGY, "avg_profit": 12.3, "backtest_count": 6, }, ] created = 0 for seed in seeds: exists = ( db.query(Strategy) .filter( Strategy.name == seed["name"], Strategy.deleted_at.is_(None), ) .first() ) if exists: logger.info("Strategy '%s' already exists, skip", seed["name"]) continue logger.info("Creating public strategy: %s", seed["name"]) strategy = Strategy( user_id=owner.id, name=seed["name"], description=seed["description"], code=seed["code"], pairs=seed["pairs"], timeframe=seed["timeframe"], is_public=True, is_active=True, backtest_count=seed["backtest_count"], avg_profit=seed["avg_profit"], ) db.add(strategy) created += 1 if created: db.commit() logger.info("Seeded %d new public strategies", created) def main() -> None: logging.basicConfig(level=logging.INFO) db = SessionLocal() try: owner = get_or_create_seed_user(db) seed_strategies(db, owner) finally: db.close() # --- Strategy code templates --- _SIMPLE_TREND_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame class SimpleEMATrendStrategy(IStrategy): """简单 EMA 趋势跟随模板,用作公共策略示例。""" minimal_roi = { "0": 0.10, "60": 0.05, "240": 0.02, "720": 0 } stoploss = -0.15 timeframe = "1h" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["ema_fast"] = dataframe["close"].ewm(span=12, adjust=False).mean() dataframe["ema_slow"] = dataframe["close"].ewm(span=26, adjust=False).mean() dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["ema_fast"] > dataframe["ema_slow"]) & (dataframe["volume"] > dataframe["volume_mean"]) & (dataframe["close"] > dataframe["ema_fast"]) & (dataframe["volume"] > 0) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["ema_fast"] < dataframe["ema_slow"]) | (dataframe["close"] < dataframe["ema_slow"]) ), "exit_long", ] = 1 return dataframe """ _RANGE_MEAN_REVERSION_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame import talib.abstract as ta class RangeMeanReversionStrategy(IStrategy): """RSI + 布林带震荡区间回归策略示例。""" minimal_roi = { "0": 0.06, "120": 0.03, "480": 0 } stoploss = -0.12 timeframe = "4h" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: bollinger = ta.BBANDS(dataframe["close"], timeperiod=20) dataframe["bb_upper"] = bollinger[0] dataframe["bb_middle"] = bollinger[1] dataframe["bb_lower"] = bollinger[2] dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] < dataframe["bb_lower"]) & (dataframe["rsi"] < 30) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] > dataframe["bb_middle"]) | (dataframe["rsi"] > 60) ), "exit_long", ] = 1 return dataframe """ _BREAKOUT_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame class SimpleBreakoutStrategy(IStrategy): """基于近期高点突破的简化模板。""" minimal_roi = { "0": 0.08, "120": 0.04, "360": 0 } stoploss = -0.13 timeframe = "1h" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["recent_high"] = dataframe["high"].rolling(48).max() dataframe["recent_low"] = dataframe["low"].rolling(48).min() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] > dataframe["recent_high"].shift(1)) & (dataframe["volume"] > dataframe["volume"].rolling(24).mean()) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] < dataframe["recent_low"].shift(1)) ), "exit_long", ] = 1 return dataframe """ _VOLATILITY_CONTRACTION_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame import talib.abstract as ta class VolatilityContractionStrategy(IStrategy): """ATR 波动率收缩 + 放量突破策略示例。""" minimal_roi = { "0": 0.10, "180": 0.05, "720": 0 } stoploss = -0.14 timeframe = "1h" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["atr_sma"] = dataframe["atr"].rolling(50).mean() dataframe["volume_mean"] = dataframe["volume"].rolling(30).mean() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["atr"] < dataframe["atr_sma"]) & (dataframe["close"] > dataframe["close"].rolling(20).max().shift(1)) & (dataframe["volume"] > 1.5 * dataframe["volume_mean"]) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] < dataframe["close"].rolling(20).min().shift(1)) ), "exit_long", ] = 1 return dataframe """ _INTRADAY_REVERSAL_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame import talib.abstract as ta class IntradayReversalStrategy(IStrategy): """30 分钟级别的日内反转策略示例。""" minimal_roi = { "0": 0.04, "60": 0.02, "180": 0 } stoploss = -0.08 timeframe = "30m" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["prev_close"] = dataframe["close"].shift(1) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] < dataframe["prev_close"] * 0.985) & (dataframe["rsi"] < 30) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] > dataframe["prev_close"] * 1.01) | (dataframe["rsi"] > 55) ), "exit_long", ] = 1 return dataframe """ _MOMENTUM_ROTATION_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame class MomentumRotationStrategy(IStrategy): """在多币种之间按动量轮动持仓的简化示例。""" minimal_roi = { "0": 0.12, "1440": 0 } stoploss = -0.15 timeframe = "1d" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["return_14"] = dataframe["close"] / dataframe["close"].shift(14) - 1 return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( dataframe["return_14"] > 0 ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( dataframe["return_14"] < 0 ), "exit_long", ] = 1 return dataframe """ _SIMPLE_GRID_STRATEGY = """from freqtrade.strategy.interface import IStrategy from pandas import DataFrame class SimpleGridStrategy(IStrategy): """现货网格学习示例(仅供教学使用,未做严谨风控)。""" minimal_roi = { "0": 0.05, "1440": 0 } stoploss = -0.20 timeframe = "1d" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["price_sma"] = dataframe["close"].rolling(30).mean() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] < dataframe["price_sma"] * 0.97) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["close"] > dataframe["price_sma"] * 1.03) ), "exit_long", ] = 1 return dataframe """ if __name__ == "__main__": # pragma: no cover main()