Timeframe
15m
Direction
Long & Short
Stoploss
-3.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisDNA v4 — Session Flow with Flat SL
==========================================
v3 confirmed: the session direction signal WORKS (53.5% WR confirmed)
but graduated/trailing SL was killing good trades (79/106 exits were
premature trailing_stop_loss).
v4 FIX: FLAT stoploss — no trailing, no graduation. Let the session
develop its full move without premature tightening.
KEY INSIGHT from hyperopt:
- TP exits: 13 trades, 100% WR, +1.84% avg ← signal is profitable
- trailing_stop_loss: 79 trades, 10% WR ← SL management destroying edge
v4 APPROACH:
- Flat SL (no custom_stoploss — just use strategy stoploss)
- TP via custom_exit
- Timeout via custom_exit
- EOD close
- ATR-multiple SL/TP option via stoploss parameter
MATH (at 1:1 R:R with 53.5% WR):
EV = 0.535 * TP - 0.465 * SL = 0.07 * TP per trade
At TP=SL=1.0%, 2x leverage: 0.14% per trade per day
200 trading days: 28% annual
"""
import logging
from datetime import datetime
from pandas import DataFrame
import numpy as np
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
)
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisDNA(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = '15m'
startup_candle_count = 200
process_only_new_candles = True
# FLAT stoploss — no custom_stoploss, no trailing
stoploss = -0.03 # 3% account max (with 2x leverage = 1.5% price move)
use_custom_stoploss = False
trailing_stop = False
minimal_roi = {"0": 100}
# === SESSION FLOW params ===
london_thresh = DecimalParameter(0.05, 1.00, default=0.10, decimals=2,
space='buy', optimize=True, load=False)
ny_entry_hour = IntParameter(12, 14, default=14, space='buy', optimize=True)
# TP as percentage (applies before leverage)
sess_tp_pct = DecimalParameter(0.30, 3.00, default=1.51, decimals=2,
space='sell', optimize=True)
# Max hold in 15m candles (48 = 12h)
sess_max_hold = IntParameter(8, 48, default=32, space='sell', optimize=True)
# Leverage
sess_leverage = IntParameter(2, 5, default=2, space='buy', optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['hour'] = dataframe['date'].dt.hour
dataframe['minute'] = dataframe['date'].dt.minute
dataframe['weekday'] = dataframe['date'].dt.weekday
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close'] * 100
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# EMA200 for trend context
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
# London session return tracking
hours = dataframe['hour'].values
minutes = dataframe['minute'].values
closes = dataframe['close'].values
opens = dataframe['open'].values
london_rets = np.zeros(len(dataframe))
london_base = np.nan
for i in range(len(dataframe)):
h, m = int(hours[i]), int(minutes[i])
if h == 8 and m == 0:
london_base = opens[i]
if not np.isnan(london_base) and london_base > 0:
london_rets[i] = (closes[i] - london_base) / london_base * 100
if h == 22 and m == 0:
london_base = np.nan
dataframe['london_ret'] = london_rets
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, 'enter_long'] = 0
dataframe.loc[:, 'enter_short'] = 0
dataframe.loc[:, 'enter_tag'] = ''
entry_h = self.ny_entry_hour.value
thresh = self.london_thresh.value
weekday = dataframe['weekday'] <= 4
ny_candle = (dataframe['hour'] == entry_h) & (dataframe['minute'] == 0)
london_up = dataframe['london_ret'] > thresh
london_dn = dataframe['london_ret'] < -thresh
# Session continuation: LONG ONLY
# DNA evidence: longs +29.18% over 2.2y, shorts -33.68%
# BTC has natural long bias in session flow
dataframe.loc[weekday & ny_candle & london_up, 'enter_long'] = 1
dataframe.loc[weekday & ny_candle & london_up, 'enter_tag'] = 'sess_long'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, 'exit_long'] = 0
dataframe.loc[:, 'exit_short'] = 0
return dataframe
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
**kwargs) -> str | bool:
tp = self.sess_tp_pct.value / 100
hours_held = (current_time - trade.open_date_utc).total_seconds() / 3600
# Take profit
if current_profit >= tp:
return 'tp'
# Cut losers: if still losing >2% after 3h, session thesis failed
if hours_held >= 3.0 and current_profit < -0.02:
return 'cut_loss_3h'
# Reduce further: if losing >1% after 5h, cut
if hours_held >= 5.0 and current_profit < -0.01:
return 'cut_loss_5h'
# Max hold timeout
if hours_held >= 8.0:
return 'timeout_profit' if current_profit > 0 else 'timeout_loss'
# EOD close
if current_time.hour >= 21:
if current_profit > 0.001:
return 'eod_profit'
return 'eod_close'
return False
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: str | None,
side: str, **kwargs) -> float:
return min(float(self.sess_leverage.value), max_leverage)