Timeframe
1h
Direction
Long Only
Stoploss
-4.0%
Trailing Stop
No
ROI
0m: 5.0%, 120m: 3.0%, 240m: 0.0%
Interface Version
3
Startup Candles
50
Indicators
2
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""BTCTrend — non-LLM baseline strategy.
EMA20/EMA50 crossover with an RSI gate. Pure indicators, no API calls. Used as
a baseline during the 14-day paper burn-in to compare against LLMScored.
Why a baseline matters: if LLMScored doesn't beat a 50-line EMA crossover after
two weeks, it's not earning its OpenRouter spend. This strategy is your control.
"""
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
import pandas as pd
from freqtrade.strategy import IStrategy
from pandas import DataFrame
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from risk_guard import guard_and_journal # type: ignore # noqa: E402
class BTCTrend(IStrategy):
INTERFACE_VERSION = 3
minimal_roi = {"0": 0.05, "120": 0.03, "240": 0.0}
stoploss = -0.04
trailing_stop = False
timeframe = "1h"
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
startup_candle_count = 50
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema20"] = dataframe["close"].ewm(span=20, adjust=False).mean()
dataframe["ema50"] = dataframe["close"].ewm(span=50, adjust=False).mean()
delta = dataframe["close"].diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / loss.replace({0: pd.NA})
dataframe["rsi14"] = (100 - (100 / (1 + rs))).fillna(50)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "enter_long"] = 0
dataframe.loc[:, "enter_tag"] = ""
# EMA20 crossing above EMA50 (golden-cross-ish on the bar timeframe) +
# RSI not overbought + volume present.
cross_up = (
(dataframe["ema20"] > dataframe["ema50"])
& (dataframe["ema20"].shift(1) <= dataframe["ema50"].shift(1))
)
gate = (dataframe["rsi14"] < 70) & (dataframe["volume"] > 0)
dataframe.loc[cross_up & gate, "enter_long"] = 1
dataframe.loc[cross_up & gate, "enter_tag"] = "ema20_cross_ema50"
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "exit_long"] = 0
# Mirror exit: EMA20 crossing back below EMA50, OR RSI > 80
cross_down = (
(dataframe["ema20"] < dataframe["ema50"])
& (dataframe["ema20"].shift(1) >= dataframe["ema50"].shift(1))
)
overbought = dataframe["rsi14"] > 80
dataframe.loc[cross_down | overbought, "exit_long"] = 1
return dataframe
def confirm_trade_entry(
self,
pair: str,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
current_time: datetime,
entry_tag: str | None,
side: str,
**kwargs,
) -> bool:
proposal = {
"symbol": pair,
"action": "buy",
"size_usd": amount * rate,
"confidence": 0.5,
"reason": entry_tag or "ema_cross",
"side": side,
"leverage": 1.0,
}
result = guard_and_journal(proposal)
return bool(result["accepted"])