TEMA slope reversal strategy optimized for limit order spread capture.
Timeframe
5s
Direction
Long Only
Stoploss
-12.5%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
N/A
Indicators
2
"""
SpreadCapture Strategy (5s timeframe + 1m HTF filter)
Based on TemaSlope with limit order management for maker fee capture.
Uses TEMA slope reversals as entry signals with 1m higher timeframe
trend confirmation (must be 3+ bars in same direction).
Config: live_spread_capture.json
- timeframe: 5s (high frequency)
- leverage: 125x
- HTF filter: 1m trend must align + be 3+ bars long
- entry: limit orders at bid (Post-Only)
- exit TP: 0.1% price move = 12.5% account profit
- exit SL: 0.1% price move = 12.5% account loss
- Risk/Reward: 1:1
"""
from datetime import datetime, timedelta
from pandas import DataFrame
import numpy as np
import talib.abstract as ta
import logging
from freqtrade.strategy import IStrategy, informative
from freqtrade.persistence import Trade, Order
logger = logging.getLogger(__name__)
class SpreadCapture(IStrategy):
"""
TEMA slope reversal strategy optimized for limit order spread capture.
Entry: 5s TEMA slope reversal + 1m trend confirmation (3+ bars)
Exit: TP or SL at 0.1% price move (12.5% account at 125x)
Leverage: 125x
- SL: 0.1% price move = 12.5% account loss (~$2.95 for ETH)
- TP: 0.1% price move = 12.5% account gain (~$2.95 for ETH)
HTF Filter:
- Long: 1m TEMA slope UP for at least 3 bars
- Short: 1m TEMA slope DOWN for at least 3 bars
Order Management:
- Entry: limit orders at bid (Post-Only = maker fees)
- Exit TP: limit orders at ask (Post-Only = maker fees)
- Exit SL: market order at 12.5% loss
- adjust_entry_price chases the best bid
- adjust_exit_price maintains TP target
"""
INTERFACE_VERSION = 3
timeframe = '5s'
can_short: bool = True
process_only_new_candles = True
startup_candle_count: int = 150 # More candles needed for 5s
# Stoploss - 0.1% price move at 125x = 12.5% account loss
# For ETH at $2950 = ~$2.95 price move triggers SL
stoploss = -0.125 # 12.5% account loss (= 0.1% price move × 125x)
trailing_stop = False
use_custom_stoploss = False
# ROI disabled - using custom_exit for TP only
minimal_roi = {"0": 100}
# Strategy parameters
tema_period = 50
tp_percent = 0.125 # 12.5% account gain (= 0.1% price move × 125x)
max_chase_minutes = 10 # Max time to chase entry order
min_htf_trend_bars = 3 # Minimum bars for 1m trend confirmation
target_leverage = 125 # Max leverage
# Order types configured in strategy (backup if not in config)
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
order_time_in_force = {
"entry": "PO", # Post-Only = maker fees
"exit": "PO",
}
@informative('1m')
def populate_indicators_1m(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Calculate TEMA trend on 1m timeframe for higher timeframe confirmation.
Columns will be available as tema_slope_1m, trend_duration_1m, etc.
"""
# TEMA calculation on 1m
ema1 = ta.EMA(dataframe['close'], timeperiod=self.tema_period)
ema2 = ta.EMA(ema1, timeperiod=self.tema_period)
ema3 = ta.EMA(ema2, timeperiod=self.tema_period)
dataframe['tema'] = 3 * ema1 - 3 * ema2 + ema3
# TEMA slope: 1=UP, -1=DOWN
dataframe['tema_slope'] = 0
dataframe.loc[dataframe['tema'] > dataframe['tema'].shift(1), 'tema_slope'] = 1
dataframe.loc[dataframe['tema'] < dataframe['tema'].shift(1), 'tema_slope'] = -1
# Track trend duration (how many bars in same direction)
# Create trend change marker
dataframe['trend_change'] = (dataframe['tema_slope'] != dataframe['tema_slope'].shift(1)).astype(int)
dataframe['trend_group'] = dataframe['trend_change'].cumsum()
dataframe['trend_duration'] = dataframe.groupby('trend_group').cumcount() + 1
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Calculate TEMA and slope indicators."""
# TEMA calculation: 3*EMA1 - 3*EMA2 + EMA3
ema1 = ta.EMA(dataframe['close'], timeperiod=self.tema_period)
ema2 = ta.EMA(ema1, timeperiod=self.tema_period)
ema3 = ta.EMA(ema2, timeperiod=self.tema_period)
dataframe['tema'] = 3 * ema1 - 3 * ema2 + ema3
# TEMA slope: 1=UP, -1=DOWN, 0=FLAT
dataframe['tema_slope'] = 0
dataframe.loc[dataframe['tema'] > dataframe['tema'].shift(1), 'tema_slope'] = 1
dataframe.loc[dataframe['tema'] < dataframe['tema'].shift(1), 'tema_slope'] = -1
# Slope change detection
dataframe['slope_prev'] = dataframe['tema_slope'].shift(1)
dataframe['slope_change_up'] = (dataframe['tema_slope'] == 1) & (dataframe['slope_prev'] <= 0)
dataframe['slope_change_down'] = (dataframe['tema_slope'] == -1) & (dataframe['slope_prev'] >= 0)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Entry signals based on TEMA slope reversals with 1m HTF confirmation.
Long: 5s slope changes to UP + 1m trend UP for at least 3 bars
Short: 5s slope changes to DOWN + 1m trend DOWN for at least 3 bars
"""
# Higher timeframe (1m) conditions
# tema_slope_1m: 1=UP, -1=DOWN
# trend_duration_1m: number of bars in current trend direction
htf_uptrend = (
(dataframe['tema_slope_1m'] == 1)
# & (dataframe['trend_duration_1m'] >= self.min_htf_trend_bars) # Disabled: no min bars
)
htf_downtrend = (
(dataframe['tema_slope_1m'] == -1)
# & (dataframe['trend_duration_1m'] >= self.min_htf_trend_bars) # Disabled: no min bars
)
# Long: 5s TEMA slope changes to UP + 1m uptrend (3+ bars)
dataframe.loc[
(dataframe['slope_change_up']) &
(htf_uptrend) &
(dataframe['volume'] > 0),
'enter_long'
] = 1
# Short: 5s TEMA slope changes to DOWN + 1m downtrend (3+ bars)
dataframe.loc[
(dataframe['slope_change_down']) &
(htf_downtrend) &
(dataframe['volume'] > 0),
'enter_short'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""No exit signals - exits only via TP (limit) or SL (market)."""
# Disabled - using only:
# - TP: custom_exit at 0.2% profit (limit order)
# - SL: stoploss at 0.1% loss (market order)
return dataframe
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> str | None:
"""
Custom exit logic - take profit at target.
This triggers placing a limit exit order.
"""
# Check if we've reached TP target
if current_profit >= self.tp_percent:
return 'tp_target'
return None
def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime,
proposed_rate: float, current_profit: float,
exit_tag: str | None, **kwargs) -> float:
"""
Custom exit price - place limit order at TP target level.
"""
if trade.is_short:
# Short: TP is below entry
tp_price = trade.open_rate * (1 - self.tp_percent)
else:
# Long: TP is above entry
tp_price = trade.open_rate * (1 + self.tp_percent)
return tp_price
def adjust_entry_price(
self,
trade: Trade,
order: Order | None,
pair: str,
current_time: datetime,
proposed_rate: float,
current_order_rate: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float | None:
"""
Continuously adjust unfilled entry limit orders to chase the market.
Called every candle for unfilled entry orders.
- proposed_rate: Current market price from entry_pricing config (best bid)
- current_order_rate: Price of existing order
Returns:
- proposed_rate: Cancel and replace at new market price
- current_order_rate: Keep existing order
- None: Cancel order without replacement
"""
if order is None:
return proposed_rate
# Calculate how long we've been chasing
order_age = current_time - order.order_date_utc
max_chase = timedelta(minutes=self.max_chase_minutes)
# Give up after max chase time
if order_age > max_chase:
logger.info(f"Entry order chase timeout for {pair} after {order_age}")
return None # Cancel, don't replace
# Check if price moved significantly (> 0.05%)
price_diff = abs(proposed_rate - current_order_rate) / current_order_rate
if price_diff > 0.0005: # 0.05% threshold
logger.debug(f"Adjusting entry for {pair}: {current_order_rate:.4f} -> {proposed_rate:.4f}")
return proposed_rate # Replace at new price
# Keep existing order
return current_order_rate
def adjust_exit_price(
self,
trade: Trade,
order: Order | None,
pair: str,
current_time: datetime,
proposed_rate: float,
current_order_rate: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float | None:
"""
Adjust unfilled exit limit orders.
For exits, we want to place at our TP target, not chase the market.
But if the market moved favorably, we can adjust.
"""
if order is None or trade is None:
return proposed_rate
# Calculate TP target price
if trade.is_short:
tp_price = trade.open_rate * (1 - self.tp_percent)
# For shorts, lower is better - use min of proposed and TP
target = min(proposed_rate, tp_price)
else:
tp_price = trade.open_rate * (1 + self.tp_percent)
# For longs, higher is better - use max of proposed and TP
target = max(proposed_rate, tp_price)
# Check if adjustment needed
price_diff = abs(target - current_order_rate) / current_order_rate
if price_diff > 0.0005: # 0.05% threshold
logger.debug(f"Adjusting exit for {pair}: {current_order_rate:.4f} -> {target:.4f}")
return target
return current_order_rate
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:
"""Use max leverage (125x)."""
return min(self.target_leverage, max_leverage)
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:
"""Log entry confirmation."""
logger.info(f"📈 ENTRY {side.upper()} {pair} @ {rate:.4f} ({order_type}, {time_in_force})")
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str,
amount: float, rate: float, time_in_force: str,
exit_reason: str, current_time: datetime, **kwargs) -> bool:
"""Log exit confirmation."""
profit = trade.calc_profit_ratio(rate)
logger.info(f"📉 EXIT {trade.trade_direction.upper()} {pair} @ {rate:.4f} | "
f"Profit: {profit:.2%} | Reason: {exit_reason}")
return True