633 lines
27 KiB
Python
633 lines
27 KiB
Python
# sma_zscore_trailing_dca.py
|
||
from freqtrade.strategy.interface import IStrategy
|
||
from freqtrade.persistence import Trade
|
||
from pandas import DataFrame
|
||
import pandas as pd
|
||
import numpy as np
|
||
from typing import Optional
|
||
import logging
|
||
from datetime import timedelta, datetime
|
||
import talib.abstract as talib
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class Simple_01(IStrategy):
|
||
"""
|
||
SMA z-score derivative strategy with trailing exit and intelligent DCA (large pullback).
|
||
- timeframe: 1h
|
||
- sma5, relative derivatives, z-score normalization (rolling z over z_window)
|
||
- smoothing: ewm(span=5) on z-scores
|
||
- entry: z_d1_smooth > entry_z1 AND z_d2_smooth > entry_z2
|
||
- exit: inversion (z_d1_smooth < 0) AND retrace from highest since entry >= trailing_stop_pct
|
||
- adjust_trade_position: add to position on controlled pullback (large = 4-6%)
|
||
"""
|
||
|
||
timeframe = "1h"
|
||
startup_candle_count = 24
|
||
# Risk mgmt (we handle exit in custom_exit)
|
||
minimal_roi = {"0": 0.99}
|
||
stoploss = -0.99
|
||
use_custom_exit = True
|
||
position_adjustment_enable = True
|
||
|
||
columns_logged = False
|
||
|
||
# Parameters
|
||
z_window = 10 # window for rolling mean/std to compute zscore
|
||
entry_z1 = 0.1 # threshold on z-score of first derivative
|
||
entry_z2 = 0 # threshold on z-score of second derivative
|
||
min_volume = 0.0 # minimal volume to accept an entry
|
||
min_relative_d1 = 1e-6 # clip tiny d1 relative values to reduce noise
|
||
|
||
# Trailing parameters for "large" trailing (exit)
|
||
trailing_stop_pct = 0.01 # 5% retracement from highest since entry
|
||
|
||
# Smoothing for z-scores
|
||
ewm_span = 5
|
||
|
||
# DCA intelligent (adjust_trade_position) parameters for "large" pullback
|
||
dca_enabled = True
|
||
# Pullback bounds (large): allow adding when retrace is between 4% and 6%
|
||
dca_pullback_min = 0.01
|
||
dca_pullback_max = 0.02
|
||
# Maximum number of adds per trade (to avoid infinite pyramiding)
|
||
dca_max_adds = 8
|
||
# Percentage of base position to add on each reinforcement (50% of original size by default)
|
||
dca_add_ratio = 0.5
|
||
# Require momentum still positive to add
|
||
dca_require_z1_positive = True
|
||
dca_require_z2_positive = True
|
||
# Do not add if current_profit < min_profit_to_add (avoid averaging down when deep in loss)
|
||
min_profit_to_add = -0.02 # allow small loss but not big drawdown
|
||
|
||
pairs = {
|
||
pair: {
|
||
"first_buy": 0,
|
||
"last_buy": 0.0,
|
||
"first_amount": 0.0,
|
||
"last_min": 999999999999999.5,
|
||
"last_max": 0,
|
||
"trade_info": {},
|
||
"max_touch": 0.0,
|
||
"last_sell": 0.0,
|
||
'count_of_buys': 0,
|
||
'current_profit': 0,
|
||
'expected_profit': 0,
|
||
"last_candle": {},
|
||
"last_trade": None,
|
||
"last_count_of_buys": 0,
|
||
'base_stake_amount': 0,
|
||
'stop_buy': False,
|
||
'last_date': 0,
|
||
'stop': False,
|
||
'max_profit': 0,
|
||
'last_palier_index': -1,
|
||
'total_amount': 0,
|
||
'has_gain': 0,
|
||
'force_sell': False,
|
||
'force_buy': False
|
||
}
|
||
for pair in ["BTC/USDC", "ETH/USDC", "DOGE/USDC", "XRP/USDC", "SOL/USDC",
|
||
"BTC/USDT", "ETH/USDT", "DOGE/USDT", "XRP/USDT", "SOL/USDT"]
|
||
}
|
||
|
||
# Plot config
|
||
plot_config = {
|
||
"main_plot": {
|
||
"close": {"color": "blue"},
|
||
"sma5": {"color": "orange"},
|
||
},
|
||
"subplots": {
|
||
"slope_and_accel": {
|
||
"z_d1_smooth": {"color": "green"},
|
||
"z_d2_smooth": {"color": "red"},
|
||
},
|
||
"sma_deriv": {
|
||
"sma5_deriv1_rel": {"color": "green"},
|
||
"sma5_deriv2_rel": {"color": "red"},
|
||
},
|
||
"rsi": {
|
||
"rsi": {"color": "blue"}
|
||
}
|
||
|
||
},
|
||
"markers": [
|
||
{"type": "buy", "column": "enter_long", "marker": "^", "color": "green", "markersize": 10},
|
||
{"type": "sell", "column": "exit_long", "marker": "v", "color": "red", "markersize": 10},
|
||
],
|
||
}
|
||
|
||
def informative_pairs(self):
|
||
return []
|
||
|
||
def _zscore(self, series: pd.Series, window: int) -> pd.Series:
|
||
mean = series.rolling(window=window, min_periods=3).mean()
|
||
std = series.rolling(window=window, min_periods=3).std(ddof=0)
|
||
z = (series - mean) / std
|
||
z = z.replace([np.inf, -np.inf], np.nan)
|
||
return z
|
||
|
||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
df = dataframe
|
||
|
||
# SMA(5)
|
||
df['sma5'] = df['close'].rolling(5, min_periods=1).mean()
|
||
|
||
# Absolute derivatives
|
||
df['sma5_deriv1'] = df['sma5'].diff()
|
||
df['sma5_deriv2'] = df['sma5_deriv1'].diff()
|
||
|
||
# Relative derivatives (percentage-like)
|
||
eps = 1e-9
|
||
df['sma5_deriv1_rel'] = df['sma5_deriv1'] / (df['sma5'].shift(1).replace(0, np.nan) + eps)
|
||
df['sma5_deriv2_rel'] = df['sma5_deriv2'] / (df['sma5'].shift(2).replace(0, np.nan) + eps)
|
||
|
||
# Clip micro-noise
|
||
df.loc[df['sma5_deriv1_rel'].abs() < self.min_relative_d1, 'sma5_deriv1_rel'] = 0.0
|
||
df.loc[df['sma5_deriv2_rel'].abs() < self.min_relative_d1, 'sma5_deriv2_rel'] = 0.0
|
||
|
||
# Z-scores on relative derivatives
|
||
df['z_d1'] = self._zscore(df['sma5_deriv1_rel'], self.z_window)
|
||
df['z_d2'] = self._zscore(df['sma5_deriv2_rel'], self.z_window)
|
||
|
||
# Smoothing z-scores with EWM to reduce jitter
|
||
df['z_d1_smooth'] = df['z_d1'].ewm(span=self.ewm_span, adjust=False).mean()
|
||
df['z_d2_smooth'] = df['z_d2'].ewm(span=self.ewm_span, adjust=False).mean()
|
||
|
||
# Prepare marker columns (for plots)
|
||
df['enter_long'] = 0
|
||
df['exit_long'] = 0
|
||
df['rsi'] = talib.RSI(df['close'], timeperiod=14)
|
||
df['max_rsi_12'] = talib.MAX(df['rsi'], timeperiod=12)
|
||
df['min_rsi_12'] = talib.MIN(df['rsi'], timeperiod=12)
|
||
# self.calculeDerivees(df, 'rsi', horizon=12)
|
||
|
||
return df
|
||
|
||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
df = dataframe
|
||
|
||
# Use last closed candle for signals -> shift(1)
|
||
cond_entry = (
|
||
(df['z_d1_smooth'] > self.entry_z1) &
|
||
(df['z_d2_smooth'] > self.entry_z2) &
|
||
(df['max_rsi_12'] < 70) &
|
||
(df['volume'] > self.min_volume)
|
||
)
|
||
|
||
df.loc[cond_entry, 'enter_long'] = 1
|
||
df.loc[~cond_entry, 'enter_long'] = 0
|
||
|
||
return df
|
||
|
||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
df = dataframe
|
||
# df['exit_long'] = 0
|
||
#
|
||
# # compute rolling max (best-effort for plotting)
|
||
# rolling_max = df['close'].cummax()
|
||
# retrace = 1.0 - (df['close'] / rolling_max.replace(0, np.nan))
|
||
#
|
||
# cond_exit = (df['z_d1_smooth'] < 0) & (retrace >= self.trailing_stop_pct)
|
||
# df.loc[cond_exit, 'exit_long'] = 1
|
||
|
||
return df
|
||
|
||
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, **kwargs, ) -> bool:
|
||
# allow_to_sell = (minutes > 30)
|
||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
last_candle = dataframe.iloc[-1].squeeze()
|
||
|
||
force = self.pairs[pair]['force_sell']
|
||
allow_to_sell = True #(last_candle['percent'] < 0) #or force
|
||
|
||
minutes = int(round((current_time - trade.date_last_filled_utc).total_seconds() / 60, 0))
|
||
|
||
if allow_to_sell:
|
||
self.trades = list()
|
||
self.pairs[pair]['last_count_of_buys'] = trade.nr_of_successful_entries # self.pairs[pair]['count_of_buys']
|
||
self.pairs[pair]['last_sell'] = rate
|
||
self.pairs[pair]['last_trade'] = trade
|
||
self.pairs[pair]['last_candle'] = last_candle
|
||
self.trades = list()
|
||
dispo = round(self.wallets.get_available_stake_amount())
|
||
print(f"Sell {pair} {current_time} {exit_reason} dispo={dispo} amount={amount} rate={rate} open_rate={trade.open_rate}")
|
||
self.log_trade(
|
||
last_candle=last_candle,
|
||
date=current_time,
|
||
action="🟥Sell " + str(minutes),
|
||
pair=pair,
|
||
trade_type=exit_reason,
|
||
rate=last_candle['close'],
|
||
dispo=dispo,
|
||
profit=round(trade.calc_profit(rate, amount), 2)
|
||
)
|
||
self.pairs[pair]['max_profit'] = 0
|
||
self.pairs[pair]['force_sell'] = False
|
||
self.pairs[pair]['has_gain'] = 0
|
||
self.pairs[pair]['current_profit'] = 0
|
||
self.pairs[pair]['total_amount'] = 0
|
||
self.pairs[pair]['count_of_buys'] = 0
|
||
self.pairs[pair]['max_touch'] = 0
|
||
self.pairs[pair]['last_buy'] = 0
|
||
self.pairs[pair]['last_date'] = current_time
|
||
self.pairs[pair]['last_palier_index'] = -1
|
||
self.pairs[pair]['last_trade'] = trade
|
||
self.pairs[pair]['current_trade'] = None
|
||
|
||
return (allow_to_sell) | (exit_reason == 'force_exit')
|
||
|
||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str,
|
||
current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool:
|
||
|
||
minutes = 0
|
||
if self.pairs[pair]['last_date'] != 0:
|
||
minutes = round(int((current_time - self.pairs[pair]['last_date']).total_seconds() / 60))
|
||
|
||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
last_candle = dataframe.iloc[-1].squeeze()
|
||
last_candle_2 = dataframe.iloc[-2].squeeze()
|
||
last_candle_3 = dataframe.iloc[-3].squeeze()
|
||
# val = self.getProbaHausse144(last_candle)
|
||
|
||
# allow_to_buy = True #(not self.stop_all) #& (not self.all_down)
|
||
allow_to_buy = not self.pairs[pair]['stop'] # and val > self.buy_val.value #not last_candle['tendency'] in ('B-', 'B--') # (rate <= float(limit)) | (entry_tag == 'force_entry')
|
||
|
||
# force = self.pairs[pair]['force_buy']
|
||
# if self.pairs[pair]['force_buy']:
|
||
# self.pairs[pair]['force_buy'] = False
|
||
# allow_to_buy = True
|
||
# else:
|
||
# if not self.should_enter_trade(pair, last_candle, current_time):
|
||
# allow_to_buy = False
|
||
|
||
if allow_to_buy:
|
||
self.trades = list()
|
||
self.pairs[pair]['first_buy'] = rate
|
||
self.pairs[pair]['last_buy'] = rate
|
||
self.pairs[pair]['max_touch'] = last_candle['close']
|
||
self.pairs[pair]['last_candle'] = last_candle
|
||
self.pairs[pair]['count_of_buys'] = 1
|
||
self.pairs[pair]['current_profit'] = 0
|
||
self.pairs[pair]['last_palier_index'] = -1
|
||
self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max'])
|
||
self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min'])
|
||
|
||
dispo = round(self.wallets.get_available_stake_amount())
|
||
self.printLineLog()
|
||
|
||
stake_amount = self.adjust_stake_amount(pair, last_candle)
|
||
|
||
self.pairs[pair]['total_amount'] = stake_amount
|
||
|
||
self.log_trade(
|
||
last_candle=last_candle,
|
||
date=current_time,
|
||
action=("🟩Buy" if allow_to_buy else "Canceled") + " " + str(minutes),
|
||
pair=pair,
|
||
rate=rate,
|
||
dispo=dispo,
|
||
profit=0,
|
||
trade_type=entry_tag,
|
||
buys=1,
|
||
stake=round(stake_amount, 2)
|
||
)
|
||
|
||
return allow_to_buy
|
||
|
||
def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs) -> Optional[str]:
|
||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
last_candle = df.iloc[-1].squeeze()
|
||
|
||
self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max'])
|
||
self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min'])
|
||
self.pairs[pair]['current_trade'] = trade
|
||
count_of_buys = trade.nr_of_successful_entries
|
||
|
||
profit = round(current_profit * trade.stake_amount, 1)
|
||
self.pairs[pair]['max_profit'] = max(self.pairs[pair]['max_profit'], profit)
|
||
max_profit = self.pairs[pair]['max_profit']
|
||
baisse = 0
|
||
if profit > 0:
|
||
baisse = 100 * abs(max_profit - profit) / max_profit
|
||
mx = max_profit / 5
|
||
self.pairs[pair]['count_of_buys'] = count_of_buys
|
||
self.pairs[pair]['current_profit'] = profit
|
||
|
||
dispo = round(self.wallets.get_available_stake_amount())
|
||
hours_since_first_buy = (current_time - trade.open_date_utc).seconds / 3600.0
|
||
days_since_first_buy = (current_time - trade.open_date_utc).days
|
||
hours = (current_time - trade.date_last_filled_utc).total_seconds() / 3600.0
|
||
|
||
if hours % 4 == 0:
|
||
self.log_trade(
|
||
last_candle=last_candle,
|
||
date=current_time,
|
||
action="🔴 CURRENT" if self.pairs[pair]['stop'] else "🟢 CURRENT",
|
||
dispo=dispo,
|
||
pair=pair,
|
||
rate=last_candle['close'],
|
||
trade_type='',
|
||
profit=profit,
|
||
buys='',
|
||
stake=0
|
||
)
|
||
|
||
z1 = last_candle.get('z_d1_smooth', None)
|
||
if z1 is None:
|
||
return None
|
||
|
||
if z1 >= 0:
|
||
return None
|
||
|
||
if current_profit is None:
|
||
return None
|
||
|
||
# highest close since entry
|
||
highest_since_entry = self.pairs[trade.pair]['max_touch']
|
||
# try:
|
||
# if hasattr(trade, 'open_date_utc') and trade.open_date_utc:
|
||
# entry_time = pd.to_datetime(trade.open_date_utc)
|
||
# mask = df.index >= entry_time
|
||
# if mask.any():
|
||
# highest_since_entry = df.loc[mask, 'close'].max()
|
||
# except Exception as e:
|
||
# logger.debug(f"Couldn't compute highest_since_entry: {e}")
|
||
#
|
||
# if highest_since_entry is None:
|
||
# highest_since_entry = df['close'].max() if not df['close'].empty else current_rate
|
||
|
||
if highest_since_entry and highest_since_entry > 0:
|
||
retrace = 1.0 - (current_rate / highest_since_entry)
|
||
else:
|
||
retrace = 0.0
|
||
z1 = last_candle.get('z_d1_smooth', None)
|
||
z2 = last_candle.get('z_d2_smooth', None)
|
||
|
||
if (current_profit > 0) and (current_profit >= self.trailing_stop_pct) and last_candle['sma5_deriv1'] < -0.002:
|
||
return str(count_of_buys) + '_' + "zscore"
|
||
|
||
self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch'])
|
||
|
||
return None
|
||
|
||
def adjust_stake_amount(self, pair: str, last_candle: DataFrame):
|
||
# Calculer le minimum des 14 derniers jours
|
||
return self.config.get('stake_amount')
|
||
|
||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||
current_rate: float, current_profit: float, min_stake: float,
|
||
max_stake: float, **kwargs):
|
||
"""
|
||
DCA intelligent (mode C - 'large'):
|
||
- Only add if:
|
||
* DCA is enabled
|
||
* number of adds done so far < dca_max_adds
|
||
* retracement since highest_since_entry is between dca_pullback_min and dca_pullback_max
|
||
* momentum still positive (z_d1_smooth, z_d2_smooth) if required
|
||
* current_profit >= min_profit_to_add (avoid averaging down into large loss)
|
||
- Returns a dict describing the desired order for Freqtrade to place (common format).
|
||
Example returned dict: {'type': 'market', 'amount': 0.01}
|
||
"""
|
||
if not self.dca_enabled:
|
||
return None
|
||
pair = trade.pair
|
||
|
||
# Basic guards
|
||
if current_profit is None:
|
||
current_profit = 0.0
|
||
|
||
# Do not add if we're already deeply in loss
|
||
if current_profit < self.min_profit_to_add:
|
||
return None
|
||
|
||
df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||
last_candle = df.iloc[-1].squeeze()
|
||
|
||
# Compute highest close since entry
|
||
highest_since_entry = self.pairs[trade.pair]['last_buy']
|
||
|
||
last = df.iloc[-1]
|
||
z1 = last.get('z_d1_smooth', None)
|
||
z2 = last.get('z_d2_smooth', None)
|
||
|
||
# Count how many adds have been done for this trade.
|
||
# Trade.extra might contain meta; otherwise try trade.tags or use trade.open_rate/amount heuristics.
|
||
adds_done = trade.nr_of_successful_entries
|
||
# Calculate retracement since the highest
|
||
retrace = 0.0
|
||
if highest_since_entry and highest_since_entry > 0:
|
||
retrace = (last['close'] - highest_since_entry) / highest_since_entry
|
||
|
||
# logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done} z1={z1} z2={z2}")
|
||
|
||
# Enforce momentum requirements if requested
|
||
if self.dca_require_z1_positive and (z1 is None or z1 <= 0):
|
||
return None
|
||
if self.dca_require_z2_positive and (z2 is None or z2 <= 0):
|
||
return None
|
||
|
||
#
|
||
# try:
|
||
# meta = getattr(trade, 'meta', None) or {}
|
||
# adds_done = int(meta.get('adds_done', 0))
|
||
# except Exception:
|
||
# adds_done = 0
|
||
|
||
if adds_done >= self.dca_max_adds:
|
||
return None
|
||
|
||
# try:
|
||
# if hasattr(trade, 'open_date_utc') and trade.open_date_utc:
|
||
# entry_time = pd.to_datetime(trade.open_date_utc)
|
||
# mask = df.index >= entry_time
|
||
# if mask.any():
|
||
# highest_since_entry = df.loc[mask, 'close'].max()
|
||
# except Exception as e:
|
||
# logger.debug(f"adjust_trade_position: couldn't compute highest_since_entry: {e}")
|
||
#
|
||
# if highest_since_entry is None:
|
||
# highest_since_entry = df['close'].max() if not df['close'].empty else current_rate
|
||
|
||
# Check if retrace is inside the allowed DCA window (large)
|
||
if ((retrace >= self.dca_pullback_min) and (retrace <= self.dca_pullback_max)):
|
||
# Determine amount to add: a fraction of the original trade amount
|
||
# Try to get trade.amount (base asset amount). If not available, fall back to stake percentage
|
||
add_amount = None
|
||
try:
|
||
base_amount = self.config.get('stake_amount')
|
||
if base_amount:
|
||
add_amount = base_amount * self.dca_add_ratio
|
||
else:
|
||
# Fallback: attempt to compute amount from trade.open_rate and desired quote stake
|
||
# We'll propose to use a fraction of current rate worth (this is best-effort)
|
||
add_amount = None
|
||
except Exception:
|
||
add_amount = None
|
||
|
||
# If we couldn't compute an absolute amount, propose a relative size via a suggested stake (user must map)
|
||
if add_amount is None:
|
||
# Return a suggested instruction; adapt according to your freqtrade version.
|
||
suggested = {
|
||
'type': 'market',
|
||
'amount': None, # caller should compute actual amount from stake management
|
||
'note': f'suggest_add_ratio={self.dca_add_ratio}'
|
||
}
|
||
# logger.info(f"{pair} {current_rate} DCA suggestion (no absolute amount): retrace={retrace:.4f}, adds_done={adds_done}")
|
||
return None
|
||
|
||
dispo = round(self.wallets.get_available_stake_amount())
|
||
trade_type = last_candle['enter_tag'] if last_candle['enter_long'] == 1 else 'pct48'
|
||
self.pairs[trade.pair]['count_of_buys'] += 1
|
||
self.pairs[pair]['total_amount'] += add_amount
|
||
self.log_trade(
|
||
last_candle=last_candle,
|
||
date=current_time,
|
||
action="🟧 Loss -",
|
||
dispo=dispo,
|
||
pair=trade.pair,
|
||
rate=current_rate,
|
||
trade_type=trade_type,
|
||
profit=round(current_profit * trade.stake_amount, 1),
|
||
buys=trade.nr_of_successful_entries + 1,
|
||
stake=round(add_amount, 2)
|
||
)
|
||
|
||
self.pairs[trade.pair]['last_buy'] = current_rate
|
||
self.pairs[trade.pair]['max_touch'] = last_candle['close']
|
||
self.pairs[trade.pair]['last_candle'] = last_candle
|
||
|
||
# All checks passed -> create market order instruction
|
||
# logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done}, add_amount={add_amount:.8f}")
|
||
return add_amount
|
||
|
||
# Not in allowed retrace window -> no action
|
||
return None
|
||
|
||
def getPctFirstBuy(self, pair, last_candle):
|
||
return round((last_candle['close'] - self.pairs[pair]['first_buy']) / self.pairs[pair]['first_buy'], 3)
|
||
|
||
def getPctLastBuy(self, pair, last_candle):
|
||
return round((last_candle['close'] - self.pairs[pair]['last_buy']) / self.pairs[pair]['last_buy'], 4)
|
||
|
||
def getPct60D(self, pair, last_candle):
|
||
return round((last_candle['max60'] - last_candle['min60']) / last_candle['max60'], 4)
|
||
|
||
def getPctClose60D(self, pair, last_candle):
|
||
if last_candle['close'] > last_candle['max12']:
|
||
return 1
|
||
if last_candle['close'] < last_candle['min12']:
|
||
return 0
|
||
return round(
|
||
(last_candle['close'] - last_candle['min12']) / (last_candle['max12'] - last_candle['min12']), 4)
|
||
|
||
|
||
def printLineLog(self):
|
||
self.printLog(
|
||
f"+{'-' * 18}+{'-' * 12}+{'-' * 5}+{'-' * 20}+{'-' * 9}+{'-' * 8}+{'-' * 12}+{'-' * 8}+{'-' * 13}+{'-' * 14}+{'-' * 9}{'-' * 9}+{'-' * 5}+{'-' * 7}+"
|
||
f"{'-' * 3}"
|
||
# "+{'-' * 3}+{'-' * 3}
|
||
f"+{'-' * 6}+{'-' * 7}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+"
|
||
)
|
||
|
||
def printLog(self, str):
|
||
if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'):
|
||
return
|
||
if not self.dp.runmode.value in ('backtest', 'hyperopt', 'lookahead-analysis'):
|
||
logger.info(str)
|
||
else:
|
||
if not self.dp.runmode.value in ('hyperopt'):
|
||
print(str)
|
||
|
||
def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None,
|
||
stake=None,
|
||
last_candle=None):
|
||
# Couleurs ANSI de base
|
||
RED = "\033[31m"
|
||
GREEN = "\033[32m"
|
||
YELLOW = "\033[33m"
|
||
BLUE = "\033[34m"
|
||
MAGENTA = "\033[35m"
|
||
CYAN = "\033[36m"
|
||
RESET = "\033[0m"
|
||
|
||
# Afficher les colonnes une seule fois
|
||
if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'):
|
||
return
|
||
if self.columns_logged % 10 == 0:
|
||
self.printLog(
|
||
f"| {'Date':<16} | {'Action':<10} |{'Pair':<5}| {'Trade Type':<18} |{'Rate':>8} | {'Dispo':>6} | {'Profit':>8} | {'Pct':>6} | {'max_touch':>11} | {'last_lost':>12} | {'last_max':>7}| {'last_max':>7}|{'Buys':>5}| {'Stake':>5} |"
|
||
f"Tdc|{'val':>6}| RSI |s201d|s5_1d|s5_2d|s51h|s52h"
|
||
)
|
||
self.printLineLog()
|
||
df = pd.DataFrame.from_dict(self.pairs, orient='index')
|
||
colonnes_a_exclure = ['last_candle', 'last_trade', 'last_palier_index',
|
||
'trade_info', 'last_date', 'expected_profit', 'last_count_of_buys',
|
||
'base_stake_amount', 'stop_buy']
|
||
df_filtered = df[df['count_of_buys'] > 0].drop(columns=colonnes_a_exclure)
|
||
# df_filtered = df_filtered["first_buy", "last_max", "max_touch", "last_sell","last_buy", 'count_of_buys', 'current_profit']
|
||
|
||
print(df_filtered)
|
||
|
||
self.columns_logged += 1
|
||
date = str(date)[:16] if date else "-"
|
||
limit = None
|
||
# if buys is not None:
|
||
# limit = round(last_rate * (1 - self.fibo[buys] / 100), 4)
|
||
|
||
rsi = ''
|
||
rsi_pct = ''
|
||
sma5_1d = ''
|
||
sma5_1h = ''
|
||
|
||
sma5 = str(sma5_1d) + ' ' + str(sma5_1h)
|
||
|
||
last_lost = '' #self.getLastLost(last_candle, pair)
|
||
|
||
if buys is None:
|
||
buys = ''
|
||
|
||
max_touch = '' # round(last_candle['max12'], 1) #round(self.pairs[pair]['max_touch'], 1)
|
||
pct_max = '' #self.getPctFirstBuy(pair, last_candle)
|
||
|
||
total_counts = str(buys) + '/' + str(
|
||
sum(pair_data['count_of_buys'] for pair_data in self.pairs.values()))
|
||
|
||
dist_max = '' #self.getDistMax(last_candle, pair)
|
||
val = 0 #self.getProbaHausseSma5d(last_candle)
|
||
|
||
pct60 = 0 #round(100 * self.getPct60D(pair, last_candle), 2)
|
||
|
||
color = GREEN if profit > 0 else RED
|
||
# color_sma20 = GREEN if last_candle['sma20_deriv1'] > 0 else RED
|
||
# color_sma5 = GREEN if last_candle['mid_smooth_5_deriv1'] > 0 else RED
|
||
# color_sma5_2 = GREEN if last_candle['mid_smooth_5_deriv2'] > 0 else RED
|
||
# color_sma5_1h = GREEN if last_candle['sma5_deriveriv1'] > 0 else RED
|
||
# color_sma5_2h = GREEN if last_candle['sma5_deriveriv2'] > 0 else RED
|
||
|
||
last_max = int(self.pairs[pair]['last_max']) if self.pairs[pair]['last_max'] > 1 else round(
|
||
self.pairs[pair]['last_max'], 3)
|
||
last_min = int(self.pairs[pair]['last_min']) if self.pairs[pair]['last_min'] > 1 else round(
|
||
self.pairs[pair]['last_min'], 3)
|
||
|
||
profit = str(profit) + '/' + str(round(self.pairs[pair]['max_profit'], 2))
|
||
|
||
# 🟢 Dérivée 1 > 0 et dérivée 2 > 0: tendance haussière qui s’accélère.
|
||
# 🟡 Dérivée 1 > 0 et dérivée 2 < 0: tendance haussière qui ralentit → essoufflement potentiel.
|
||
# 🔴 Dérivée 1 < 0 et dérivée 2 < 0: tendance baissière qui s’accélère.
|
||
# 🟠 Dérivée 1 < 0 et dérivée 2 > 0: tendance baissière qui ralentit → possible bottom.
|
||
|
||
# tdc last_candle['tendency_12']
|
||
self.printLog(
|
||
f"| {date:<16} |{action:<10} | {pair[0:3]:<3} | {trade_type or '-':<18} |{rate or '-':>9}| {dispo or '-':>6} "
|
||
f"|{color}{profit or '-':>10}{RESET}| {pct_max or '-':>6} | {round(self.pairs[pair]['max_touch'], 2) or '-':>11} | {last_lost or '-':>12} "
|
||
f"| {last_max or '-':>7} | {last_min or '-':>7} |{total_counts or '-':>5}|{stake or '-':>7}"
|
||
f"|{'-':>3}|"
|
||
f"{round(val, 1) or '-' :>6}|"
|
||
# f"{round(last_candle['rsi'], 0):>7}|{color_sma20}{round(last_candle['sma20_deriv1'], 2):>5}{RESET}"
|
||
# f"|{color_sma5}{round(last_candle['mid_smooth_5_deriv1'], 2):>5}{RESET}|{color_sma5_2}{round(last_candle['mid_smooth_5_deriv2'], 2):>5}{RESET}"
|
||
# f"|{color_sma5_1h}{round(last_candle['sma5_deriveriv1'], 2):>5}{RESET}|{color_sma5_2h}{round(last_candle['sma5_deriveriv2'], 2):>5}{RESET}"
|
||
# f"|{last_candle['min60']}|{last_candle['max60']}"
|
||
) |