Timeframe
5m
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 10.0%
Interface Version
N/A
Startup Candles
N/A
Indicators
2
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
import requests
import logging
import json
from datetime import datetime
from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
class BackendStrategy(IStrategy):
# URL of the external trade signal server
external_server_url = "http://trading-server:3000/api/v1/trading/tradePlan"
# Enable short trading
can_short: bool = True
# Enable custom stoploss
use_custom_stoploss = True
# disable trailing stop if using custom stoploss
trailing_stop = False
# Maximum loss before stopping out
stoploss = -0.05
# Trading timeframe
timeframe = '5m'
# Minimum return on investment (ROI)
minimal_roi = {
"0": 0.10
}
def is_trade_active(self, pair: str) -> bool:
"""
Determines if there is an open trade for trading pair.
"""
open_trades = Trade.get_open_trades()
return any(trade for trade in open_trades if trade.pair == pair)
def send_trade_signal(self, metadata: list) -> dict:
"""
Sends a trade signal request to an external server.
"""
if self.is_trade_active(metadata['pair']):
logger.info(f"There is already an open trade for {metadata['pair']}")
return None
exchange_name = self.config.get("exchange", {}).get("name")
payload = {
"exchange": exchange_name,
"pair": metadata['pair'],
"timeframe": self.timeframe
}
headers = {}
# headers = {"Authorization": f"Bearer {self.api_key}"}
try:
# TODO: set timeout
# Sending POST request to the external server
response = requests.post(self.external_server_url, json=payload, headers=headers)
response.raise_for_status() # Raise exception for HTTP errors
response_data = response.json()
logger.info(f"trade signal from backend: {response_data}")
return response_data
except requests.exceptions.RequestException as e:
logger.error(f"Error sending signal:{e}")
return None
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:
"""
Customize leverage for each new trade. This method is only called in futures mode.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: "long" or "short" - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
if entry_tag:
try:
tag_data = json.loads(entry_tag)
custom_leverage = float(tag_data.get("leverage", proposed_leverage))
except Exception as e:
logger.error(f"Error parsing leverage entry_tag: {e}")
custom_leverage = proposed_leverage
else:
logger.warning("leverage entry_tag not set")
custom_leverage = proposed_leverage
return min(custom_leverage, max_leverage)
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> float | None:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param after_fill: True if the stoploss is called after the order was filled.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current_rate
"""
if after_fill and trade.enter_tag:
try:
tag_data = json.loads(trade.enter_tag)
sl = tag_data.get("stoploss")
if sl is not None:
val = float(sl)
# Maximum SL in percent from self.stoploss (e.g. -0.05 -> 5.0)
max_sl_pct = abs(self.stoploss) * 100.0
# Only normalize if val > max_sl_pct
if val > max_sl_pct:
# If decimal places, limit directly to max.
if not val.is_integer():
val = max_sl_pct
else:
# Integer: divide by the appropriate power of 10
digits = len(str(int(val)))
corrected = val / (10 ** (digits - 1))
val = corrected if corrected <= max_sl_pct else max_sl_pct
# Round cleanly to 4 decimal places in percent fractions
stop_pct = round(val / 100.0, 4)
return -stop_pct
except Exception as e:
logger.error(f"Error parsing trade.enter_tag: {e}")
return None
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
entry_tag: str | None, side: str, **kwargs) -> float:
"""
Custom entry price.
"""
if entry_tag:
try:
tag_data = json.loads(entry_tag)
entry_price = float(tag_data.get("price", proposed_rate))
if side == "long" and entry_price > proposed_rate:
entry_price = proposed_rate
elif side == "short" and entry_price < proposed_rate:
entry_price = proposed_rate
except Exception as e:
logger.error(f"Error parsing price entry_tag: {e}")
entry_price = proposed_rate
else:
logger.warning("price entry_tag not set")
entry_price = proposed_rate
return entry_price
def custom_exit(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> bool:
"""
Closes the trade individually based on the "takeProfit" value from the trade.enter_tag
"""
target_roi = None
if trade.enter_tag:
try:
tag_data = json.loads(trade.enter_tag)
target_roi = tag_data.get("takeProfit")
except Exception as e:
logger.error(f"Error parsing enter_tag: {e}")
if target_roi is not None:
# If the current profit is greater than or equal to target_roi, close the trade
if current_profit >= (target_roi / 100):
return True
return False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
A placeholder method for populating indicators.
"""
#dataframe['rsi_period_14_close'] = ta.RSI(dataframe, timeperiod=14, price='close')
#dataframe['sma_period_20_close'] = ta.SMA(dataframe, timeperiod=20, price='close')
#dataframe['sma_period_50_close'] = ta.SMA(dataframe, timeperiod=50, price='close')
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Determines entry signals based on external trade signals.
"""
# Initialize entry signal columns
dataframe['enter_long'] = 0
dataframe['enter_short'] = 0
signal = self.send_trade_signal(metadata)
if signal is None:
return dataframe
tag = json.dumps({
"id": signal.get('id'),
"price": signal.get('entryPoint'),
"stoploss": signal.get("stoploss"),
"takeProfit": signal.get("takeProfit"),
"leverage": signal.get("leverage"),
"pos": signal.get("probabilityOfSuccess")
})
if signal.get('direction') == "long":
dataframe.loc[dataframe.index[-1], ['enter_long', 'enter_tag']] = (1, tag)
elif signal.get('direction') == "short":
dataframe.loc[dataframe.index[-1], ['enter_short', 'enter_tag']] = (1, tag)
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Determines exit signals. Currently, no exit logic is implemented.
"""
# Initialize exit signal columns
dataframe['exit_long'] = 0
dataframe['exit_short'] = 0
return dataframe