Signal-file driven rank portfolio strategy for OKX isolated futures.
Timeframe
1h
Direction
Long & Short
Stoploss
-30.0%
Trailing Stop
No
ROI
0m: 50.0%, 2880m: -100.0%
Interface Version
N/A
Startup Candles
N/A
Indicators
0
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Any, Dict, Optional
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, stoploss_from_open
def _inject_project_paths() -> Path:
here = Path(__file__).resolve()
for parent in here.parents:
if (parent / "src" / "agent_market").exists():
sys.path.insert(0, str(parent / "src"))
sys.path.insert(0, str(parent))
return parent
root = here.parents[2]
sys.path.insert(0, str(root / "src"))
sys.path.insert(0, str(root))
return root
PROJECT_ROOT = _inject_project_paths()
def _paths():
from agent_market import paths as _p
return _p
def _pair_token(pair: str) -> str:
raw = str(pair).split(":")[0].replace("/", "_")
if raw.endswith("_USDT"):
return f"{raw}_USDT"
return raw
class ELRankPortfolioLeverageStrategy(IStrategy):
"""Signal-file driven rank portfolio strategy for OKX isolated futures."""
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.50, "2880": -1}
stoploss = -0.30
use_custom_stoploss = True
use_exit_signal = True
process_only_new_candles = True
startup_candle_count: int = 1
_signals_by_pair: Dict[str, DataFrame] = {}
_signal_dir: Optional[Path] = None
@staticmethod
def _env_float(name: str, default: float) -> float:
raw = os.environ.get(name)
if raw in (None, ""):
return float(default)
try:
return float(raw)
except Exception:
return float(default)
def _resolve_signal_dir(self) -> Path:
if self._signal_dir is not None:
return self._signal_dir
env_dir = os.environ.get("RP_SIGNAL_DIR")
if env_dir:
path = _paths().resolve_repo_path(env_dir)
self._signal_dir = path
return path
tag = os.environ.get("RP_TAG", "gpt54_purealpha_v2_full1000_fix1")
path = _paths().artifacts_root() / "rank_portfolio" / tag / "signals"
if path.exists():
self._signal_dir = path
return path
root = _paths().artifacts_root() / "rank_portfolio"
candidates = sorted(root.glob("*/signals"), key=lambda p: p.stat().st_mtime, reverse=True) if root.exists() else []
self._signal_dir = candidates[0] if candidates else path
return self._signal_dir
def _load_pair_signals(self, pair: str) -> DataFrame:
if pair in self._signals_by_pair:
return self._signals_by_pair[pair]
signal_dir = self._resolve_signal_dir()
token = _pair_token(pair)
candidates = [
signal_dir / f"{token}.feather",
signal_dir / f"{token.removesuffix('_USDT')}.feather",
signal_dir / "all.feather",
]
sig = pd.DataFrame()
for path in candidates:
if not path.exists():
continue
loaded = pd.read_feather(path)
loaded["date"] = pd.to_datetime(loaded["date"], utc=True)
if path.name == "all.feather" and "pair" in loaded.columns:
base_pair = str(pair).split(":")[0]
loaded = loaded.loc[loaded["pair"].astype(str) == base_pair]
sig = loaded
break
if sig.empty:
sig = pd.DataFrame(columns=[
"date", "rp_score", "rp_rank", "rp_side", "rp_target_weight",
"rp_leverage", "rp_stop_pct", "rp_kill_mode", "rp_rebalance",
"rp_exit_long", "rp_exit_short",
])
keep = [
"date", "rp_score", "rp_score_z", "rp_rank", "rp_side",
"rp_target_weight", "rp_leverage", "rp_stop_pct", "rp_kill_mode",
"rp_rebalance", "rp_exit_long", "rp_exit_short",
]
keep = [col for col in keep if col in sig.columns]
sig = sig[keep].drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
self._signals_by_pair[pair] = sig
return sig
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = str(metadata.get("pair") or "") if isinstance(metadata, dict) else ""
df = dataframe.copy()
df["date"] = pd.to_datetime(df["date"], utc=True)
sig = self._load_pair_signals(pair)
if not sig.empty:
df = df.merge(sig, on="date", how="left")
defaults: Dict[str, Any] = {
"rp_score": 0.0,
"rp_score_z": 0.0,
"rp_rank": 0,
"rp_side": 0,
"rp_target_weight": 0.0,
"rp_leverage": 1.0,
"rp_stop_pct": 0.10,
"rp_kill_mode": "normal",
"rp_rebalance": False,
"rp_exit_long": False,
"rp_exit_short": False,
}
for col, value in defaults.items():
if col not in df.columns:
df[col] = value
elif col == "rp_kill_mode":
df[col] = df[col].fillna(str(value)).astype(str)
elif col in {"rp_rebalance", "rp_exit_long", "rp_exit_short"}:
default_bool = bool(value)
df[col] = df[col].map(lambda x: bool(x) if pd.notna(x) else default_bool)
else:
df[col] = pd.to_numeric(df[col], errors="coerce").fillna(float(value))
return df
def _current_signal_row(self, pair: str, current_time: Any) -> Optional[pd.Series]:
try:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
except Exception:
return None
if df is None or df.empty:
return None
ts = pd.Timestamp(current_time)
if ts.tzinfo is None:
ts = ts.tz_localize("UTC")
else:
ts = ts.tz_convert("UTC")
dates = pd.to_datetime(df["date"], utc=True)
rows = df.loc[dates <= ts]
if rows.empty:
return None
return rows.iloc[-1]
def leverage(
self,
pair: str,
current_time: Any,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: Optional[str],
side: str,
**kwargs: Any,
) -> float:
row = self._current_signal_row(pair, current_time)
lev = float(proposed_leverage or 1.0)
if row is not None:
lev = float(row.get("rp_leverage", lev) or lev)
env_cap = self._env_float("RP_MAX_LEVERAGE", float(max_leverage))
return max(1.0, min(float(lev), float(max_leverage), env_cap))
def custom_stake_amount(
self,
pair: str,
current_time: Any,
current_rate: float,
proposed_stake: float,
min_stake: Optional[float],
max_stake: float,
leverage: float,
entry_tag: Optional[str],
side: str,
**kwargs: Any,
) -> float:
row = self._current_signal_row(pair, current_time)
if row is None:
return proposed_stake
target_weight = abs(float(row.get("rp_target_weight", 0.0) or 0.0))
lev = max(1.0, float(leverage or row.get("rp_leverage", 1.0) or 1.0))
try:
equity = float(self.wallets.get_total_stake_amount())
except Exception:
equity = float(self.config.get("dry_run_wallet", max_stake) if hasattr(self, "config") else max_stake)
stake = equity * target_weight / lev
if min_stake is not None:
stake = max(stake, float(min_stake))
return max(0.0, min(float(stake), float(max_stake)))
def custom_stoploss(
self,
pair: str,
trade: Any,
current_time: Any,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs: Any,
) -> float:
row = self._current_signal_row(pair, current_time)
stop = float(row.get("rp_stop_pct", 0.10) if row is not None else 0.10)
stop = max(0.005, min(0.30, stop))
lev = max(1.0, float(getattr(trade, "leverage", 1.0) or 1.0))
# Freqtrade custom_stoploss is relative to current_rate and, in
# futures mode, represents leveraged trade risk. Convert the research
# engine's entry-relative price stop to a fixed open-relative stop so
# short trades do not trail down from each candle's low in backtests.
return stoploss_from_open(
-min(0.99, stop * lev),
current_profit,
is_short=bool(getattr(trade, "is_short", False)),
leverage=lev,
)
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
active = (
(df["volume"] > 0)
& (df["rp_kill_mode"].astype(str) != "daily_halt")
& df["rp_rebalance"].astype(bool)
)
df.loc[active & (df["rp_side"].astype(float) > 0), ["enter_long", "enter_tag"]] = (1, "rp_long")
df.loc[active & (df["rp_side"].astype(float) < 0), ["enter_short", "enter_tag"]] = (1, "rp_short")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
active = df["volume"] > 0
side = df["rp_side"].astype(float)
exit_long = df["rp_exit_long"].astype(bool) if "rp_exit_long" in df.columns else False
exit_short = df["rp_exit_short"].astype(bool) if "rp_exit_short" in df.columns else False
df.loc[active & ((side <= 0) | exit_long), "exit_long"] = 1
df.loc[active & ((side >= 0) | exit_short), "exit_short"] = 1
return df