mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into feature_keyval_storage
This commit is contained in:
commit
f1af00dd39
|
@ -16,10 +16,10 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.7
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.20240125
|
- types-requests==2.31.0.20240218
|
||||||
- types-tabulate==0.9.0.20240106
|
- types-tabulate==0.9.0.20240106
|
||||||
- types-python-dateutil==2.8.19.20240106
|
- types-python-dateutil==2.8.19.20240106
|
||||||
- SQLAlchemy==2.0.26
|
- SQLAlchemy==2.0.27
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
@ -31,7 +31,7 @@ repos:
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.2.1'
|
rev: 'v0.2.2'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ When used in the leading position of the chain of Pairlist Handlers, the `pair_w
|
||||||
|
|
||||||
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
||||||
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
|
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
|
||||||
Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data.
|
Filtering instances (not the first position in the list) will not apply any cache (beyond caching candles for the duration of the candle in advanced mode) and will always use up-to-date data.
|
||||||
|
|
||||||
`VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library:
|
`VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library:
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
||||||
|
|
||||||
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
|
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
|
||||||
|
|
||||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append".
|
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
|
||||||
|
|
||||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ Freqtrade does not depend or install any additional database driver. Please refe
|
||||||
The following systems have been tested and are known to work with freqtrade:
|
The following systems have been tested and are known to work with freqtrade:
|
||||||
|
|
||||||
* sqlite (default)
|
* sqlite (default)
|
||||||
* PostgreSQL)
|
* PostgreSQL
|
||||||
* MariaDB
|
* MariaDB
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
|
|
|
@ -48,10 +48,12 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||||
logger.info(f"Converting pairs: {', '.join(m[0] for m in markets)}.")
|
logger.info(f"Converting pairs: {', '.join(m[0] for m in markets)}.")
|
||||||
|
|
||||||
for pair, name in markets:
|
for pair, name in markets:
|
||||||
|
logger.debug(f"Converting pair {pair}, files */{name}.csv")
|
||||||
dfs = []
|
dfs = []
|
||||||
# Load and combine all csv files for this pair
|
# Load and combine all csv files for this pair
|
||||||
for f in tradesdir.rglob(f"{name}.csv"):
|
for f in tradesdir.rglob(f"{name}.csv"):
|
||||||
df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS)
|
df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS)
|
||||||
|
if not df.empty:
|
||||||
dfs.append(df)
|
dfs.append(df)
|
||||||
|
|
||||||
# Load existing trades data
|
# Load existing trades data
|
||||||
|
|
|
@ -143,8 +143,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
||||||
starting_balance=starting_balance
|
starting_balance=starting_balance
|
||||||
)
|
)
|
||||||
|
|
||||||
idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative \
|
idxmin = (
|
||||||
else max_drawdown_df['drawdown'].idxmin()
|
max_drawdown_df['drawdown_relative'].idxmax()
|
||||||
|
if relative else max_drawdown_df['drawdown'].idxmin()
|
||||||
|
)
|
||||||
if idxmin == 0:
|
if idxmin == 0:
|
||||||
raise ValueError("No losing trade, therefore no drawdown.")
|
raise ValueError("No losing trade, therefore no drawdown.")
|
||||||
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
||||||
|
@ -191,6 +193,9 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
||||||
:param final_balance: Final balance to calculate CAGR against
|
:param final_balance: Final balance to calculate CAGR against
|
||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
|
if final_balance < 0:
|
||||||
|
# With leveraged trades, final_balance can become negative.
|
||||||
|
return 0
|
||||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class Bybit(Exchange):
|
||||||
officially supported by the Freqtrade development team. So some features
|
officially supported by the Freqtrade development team. So some features
|
||||||
may still not work as expected.
|
may still not work as expected.
|
||||||
"""
|
"""
|
||||||
|
unified_account = False
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
|
@ -82,9 +83,20 @@ class Bybit(Exchange):
|
||||||
Must be overridden in child methods if required.
|
Must be overridden in child methods if required.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
if not self._config['dry_run']:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
position_mode = self._api.set_position_mode(False)
|
position_mode = self._api.set_position_mode(False)
|
||||||
self._log_exchange_response('set_position_mode', position_mode)
|
self._log_exchange_response('set_position_mode', position_mode)
|
||||||
|
is_unified = self._api.is_unified_enabled()
|
||||||
|
# Returns a tuple of bools, first for margin, second for Account
|
||||||
|
if is_unified and len(is_unified) > 1 and is_unified[1]:
|
||||||
|
self.unified_account = True
|
||||||
|
logger.info("Bybit: Unified account.")
|
||||||
|
raise OperationalException("Bybit: Unified account is not supported. "
|
||||||
|
"Please use a standard (sub)account.")
|
||||||
|
else:
|
||||||
|
self.unified_account = False
|
||||||
|
logger.info("Bybit: Standard account.")
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
from typing import Any, Callable, Dict, List, Optional, TypeVar, cast, overload
|
||||||
|
|
||||||
from freqtrade.constants import ExchangeConfig
|
from freqtrade.constants import ExchangeConfig
|
||||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||||
|
@ -60,16 +60,17 @@ SUPPORTED_EXCHANGES = [
|
||||||
'okx',
|
'okx',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXCHANGE_HAS_REQUIRED = [
|
# either the main, or replacement methods (array) is required
|
||||||
|
EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = {
|
||||||
# Required / private
|
# Required / private
|
||||||
'fetchOrder',
|
'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'],
|
||||||
'cancelOrder',
|
'cancelOrder': [],
|
||||||
'createOrder',
|
'createOrder': [],
|
||||||
'fetchBalance',
|
'fetchBalance': [],
|
||||||
|
|
||||||
# Public endpoints
|
# Public endpoints
|
||||||
'fetchOHLCV',
|
'fetchOHLCV': [],
|
||||||
]
|
}
|
||||||
|
|
||||||
EXCHANGE_HAS_OPTIONAL = [
|
EXCHANGE_HAS_OPTIONAL = [
|
||||||
# Private
|
# Private
|
||||||
|
@ -86,6 +87,7 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||||
# 'fetchPositions', # Futures trading
|
# 'fetchPositions', # Futures trading
|
||||||
# 'fetchLeverageTiers', # Futures initialization
|
# 'fetchLeverageTiers', # Futures initialization
|
||||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||||
|
# 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder
|
||||||
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.util import dt_from_ts, dt_now
|
from freqtrade.util import dt_from_ts, dt_now
|
||||||
from freqtrade.util.datetime_helpers import dt_humanize, dt_ts
|
from freqtrade.util.datetime_helpers import dt_humanize, dt_ts
|
||||||
|
from freqtrade.util.periodic_cache import PeriodicCache
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -131,6 +132,7 @@ class Exchange:
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
|
self._expiring_candle_cache: Dict[str, PeriodicCache] = {}
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
@ -1242,7 +1244,7 @@ class Exchange:
|
||||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
||||||
f'stop-price {stop_price_norm}. Message: {e}') from e
|
f'stop-price {stop_price_norm}. Message: {e}') from e
|
||||||
except (ccxt.InvalidOrder, ccxt.BadRequest) as e:
|
except (ccxt.InvalidOrder, ccxt.BadRequest, ccxt.OperationRejected) as e:
|
||||||
# Errors:
|
# Errors:
|
||||||
# `Order would trigger immediately.`
|
# `Order would trigger immediately.`
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
|
@ -1258,11 +1260,43 @@ class Exchange:
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate
|
||||||
|
calls for open and closed orders.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
order = self._api.fetch_open_order(order_id, pair, params=params)
|
||||||
|
self._log_exchange_response('fetch_open_order', order)
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
|
return order
|
||||||
|
except ccxt.OrderNotFound:
|
||||||
|
try:
|
||||||
|
order = self._api.fetch_closed_order(order_id, pair, params=params)
|
||||||
|
self._log_exchange_response('fetch_closed_order', order)
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
|
return order
|
||||||
|
except ccxt.OrderNotFound as e:
|
||||||
|
raise RetryableOrderError(
|
||||||
|
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
try:
|
try:
|
||||||
|
if not self.exchange_has('fetchOrder'):
|
||||||
|
return self.fetch_order_emulated(order_id, pair, params)
|
||||||
order = self._api.fetch_order(order_id, pair, params=params)
|
order = self._api.fetch_order(order_id, pair, params=params)
|
||||||
self._log_exchange_response('fetch_order', order)
|
self._log_exchange_response('fetch_order', order)
|
||||||
order = self._order_contracts_to_amount(order)
|
order = self._order_contracts_to_amount(order)
|
||||||
|
@ -2124,6 +2158,39 @@ class Exchange:
|
||||||
|
|
||||||
return results_df
|
return results_df
|
||||||
|
|
||||||
|
def refresh_ohlcv_with_cache(
|
||||||
|
self,
|
||||||
|
pairs: List[PairWithTimeframe],
|
||||||
|
since_ms: int
|
||||||
|
) -> Dict[PairWithTimeframe, DataFrame]:
|
||||||
|
"""
|
||||||
|
Refresh ohlcv data for all pairs in needed_pairs if necessary.
|
||||||
|
Caches data with expiring per timeframe.
|
||||||
|
Should only be used for pairlists which need "on time" expirarion, and no longer cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
timeframes = [p[1] for p in pairs]
|
||||||
|
for timeframe in timeframes:
|
||||||
|
if timeframe not in self._expiring_candle_cache:
|
||||||
|
timeframe_in_sec = timeframe_to_seconds(timeframe)
|
||||||
|
# Initialise cache
|
||||||
|
self._expiring_candle_cache[timeframe] = PeriodicCache(ttl=timeframe_in_sec,
|
||||||
|
maxsize=1000)
|
||||||
|
|
||||||
|
# Get candles from cache
|
||||||
|
candles = {
|
||||||
|
c: self._expiring_candle_cache[c[1]].get(c, None) for c in pairs
|
||||||
|
if c in self._expiring_candle_cache[c[1]]
|
||||||
|
}
|
||||||
|
pairs_to_download = [p for p in pairs if p not in candles]
|
||||||
|
if pairs_to_download:
|
||||||
|
candles = self.refresh_latest_ohlcv(
|
||||||
|
pairs_to_download, since_ms=since_ms, cache=False
|
||||||
|
)
|
||||||
|
for c, val in candles.items():
|
||||||
|
self._expiring_candle_cache[c[1]][c] = val
|
||||||
|
return candles
|
||||||
|
|
||||||
def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
|
def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
|
||||||
# Timeframe in seconds
|
# Timeframe in seconds
|
||||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||||
|
@ -2685,7 +2752,7 @@ class Exchange:
|
||||||
self._log_exchange_response('set_leverage', res)
|
self._log_exchange_response('set_leverage', res)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
|
except (ccxt.BadRequest, ccxt.OperationRejected, ccxt.InsufficientFunds) as e:
|
||||||
if not accept_fail:
|
if not accept_fail:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
@ -2727,7 +2794,7 @@ class Exchange:
|
||||||
self._log_exchange_response('set_margin_mode', res)
|
self._log_exchange_response('set_margin_mode', res)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except ccxt.BadRequest as e:
|
except (ccxt.BadRequest, ccxt.OperationRejected) as e:
|
||||||
if not accept_fail:
|
if not accept_fail:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
|
|
@ -40,21 +40,34 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st
|
||||||
|
|
||||||
|
|
||||||
def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
returns: can_use, reason
|
||||||
|
with Reason including both missing and missing_opt
|
||||||
|
"""
|
||||||
ex_mod = getattr(ccxt, exchange.lower())()
|
ex_mod = getattr(ccxt, exchange.lower())()
|
||||||
|
result = True
|
||||||
|
reason = ''
|
||||||
if not ex_mod or not ex_mod.has:
|
if not ex_mod or not ex_mod.has:
|
||||||
return False, ''
|
return False, ''
|
||||||
missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True]
|
missing = [
|
||||||
|
k for k, v in EXCHANGE_HAS_REQUIRED.items()
|
||||||
|
if ex_mod.has.get(k) is not True
|
||||||
|
and not (all(ex_mod.has.get(x) for x in v))
|
||||||
|
]
|
||||||
if missing:
|
if missing:
|
||||||
return False, f"missing: {', '.join(missing)}"
|
result = False
|
||||||
|
reason += f"missing: {', '.join(missing)}"
|
||||||
|
|
||||||
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
||||||
|
|
||||||
if exchange.lower() in BAD_EXCHANGES:
|
if exchange.lower() in BAD_EXCHANGES:
|
||||||
return False, BAD_EXCHANGES.get(exchange.lower(), '')
|
result = False
|
||||||
if missing_opt:
|
reason = BAD_EXCHANGES.get(exchange.lower(), '')
|
||||||
return True, f"missing opt: {', '.join(missing_opt)}"
|
|
||||||
|
|
||||||
return True, ''
|
if missing_opt:
|
||||||
|
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
||||||
|
|
||||||
|
return result, reason
|
||||||
|
|
||||||
|
|
||||||
def _build_exchange_list_entry(
|
def _build_exchange_list_entry(
|
||||||
|
|
|
@ -702,7 +702,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
delta = f"Delta: {bids_ask_delta}"
|
delta = f"Delta: {bids_ask_delta}"
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{bids}, {asks}, {delta}, Direction: {side.value}"
|
f"{bids}, {asks}, {delta}, Direction: {side.value} "
|
||||||
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
||||||
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
||||||
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
||||||
|
|
|
@ -24,7 +24,7 @@ from freqtrade.leverage import interest
|
||||||
from freqtrade.misc import safe_value_fallback
|
from freqtrade.misc import safe_value_fallback
|
||||||
from freqtrade.persistence.base import ModelBase, SessionType
|
from freqtrade.persistence.base import ModelBase, SessionType
|
||||||
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
|
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
|
||||||
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts
|
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -225,8 +225,7 @@ class Order(ModelBase):
|
||||||
'amount': self.safe_amount,
|
'amount': self.safe_amount,
|
||||||
'safe_price': self.safe_price,
|
'safe_price': self.safe_price,
|
||||||
'ft_order_side': self.ft_order_side,
|
'ft_order_side': self.ft_order_side,
|
||||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
'order_filled_timestamp': dt_ts_none(self.order_filled_utc),
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
|
||||||
'ft_is_entry': self.ft_order_side == entry_side,
|
'ft_is_entry': self.ft_order_side == entry_side,
|
||||||
'ft_order_tag': self.ft_order_tag,
|
'ft_order_tag': self.ft_order_tag,
|
||||||
}
|
}
|
||||||
|
@ -463,6 +462,17 @@ class LocalTrade:
|
||||||
return self.open_date_utc
|
return self.open_date_utc
|
||||||
return max([self.open_date_utc, dt_last_filled])
|
return max([self.open_date_utc, dt_last_filled])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_entry_fill_utc(self) -> Optional[datetime]:
|
||||||
|
""" Date of the first filled order"""
|
||||||
|
orders = self.select_filled_orders(self.entry_side)
|
||||||
|
if (
|
||||||
|
orders
|
||||||
|
and len(filled_date := [o.order_filled_utc for o in orders if o.order_filled_utc])
|
||||||
|
):
|
||||||
|
return min(filled_date)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_date_utc(self):
|
def open_date_utc(self):
|
||||||
return self.open_date.replace(tzinfo=timezone.utc)
|
return self.open_date.replace(tzinfo=timezone.utc)
|
||||||
|
@ -627,15 +637,17 @@ class LocalTrade:
|
||||||
'fee_close_currency': self.fee_close_currency,
|
'fee_close_currency': self.fee_close_currency,
|
||||||
|
|
||||||
'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
|
'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
'open_timestamp': dt_ts_none(self.open_date_utc),
|
||||||
|
'open_fill_date': (self.date_entry_fill_utc.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
if self.date_entry_fill_utc else None),
|
||||||
|
'open_fill_timestamp': dt_ts_none(self.date_entry_fill_utc),
|
||||||
'open_rate': self.open_rate,
|
'open_rate': self.open_rate,
|
||||||
'open_rate_requested': self.open_rate_requested,
|
'open_rate_requested': self.open_rate_requested,
|
||||||
'open_trade_value': round(self.open_trade_value, 8),
|
'open_trade_value': round(self.open_trade_value, 8),
|
||||||
|
|
||||||
'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
|
'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
'close_timestamp': int(self.close_date.replace(
|
'close_timestamp': dt_ts_none(self.close_date_utc),
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
|
||||||
'realized_profit': self.realized_profit or 0.0,
|
'realized_profit': self.realized_profit or 0.0,
|
||||||
# Close-profit corresponds to relative realized_profit ratio
|
# Close-profit corresponds to relative realized_profit ratio
|
||||||
'realized_profit_ratio': self.close_profit or None,
|
'realized_profit_ratio': self.close_profit or None,
|
||||||
|
@ -661,8 +673,7 @@ class LocalTrade:
|
||||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||||
'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT)
|
'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT)
|
||||||
if self.stoploss_last_update_utc else None),
|
if self.stoploss_last_update_utc else None),
|
||||||
'stoploss_last_update_timestamp': int(self.stoploss_last_update_utc.timestamp() * 1000
|
'stoploss_last_update_timestamp': dt_ts_none(self.stoploss_last_update_utc),
|
||||||
) if self.stoploss_last_update_utc else None,
|
|
||||||
'initial_stop_loss_abs': self.initial_stop_loss,
|
'initial_stop_loss_abs': self.initial_stop_loss,
|
||||||
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
||||||
if self.initial_stop_loss_pct else None),
|
if self.initial_stop_loss_pct else None),
|
||||||
|
|
|
@ -104,10 +104,7 @@ class VolatilityFilter(IPairList):
|
||||||
|
|
||||||
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days))
|
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days))
|
||||||
# Get all candles
|
# Get all candles
|
||||||
candles = {}
|
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
|
||||||
if needed_pairs:
|
|
||||||
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms,
|
|
||||||
cache=False)
|
|
||||||
|
|
||||||
if self._enabled:
|
if self._enabled:
|
||||||
for p in deepcopy(pairlist):
|
for p in deepcopy(pairlist):
|
||||||
|
@ -125,8 +122,7 @@ class VolatilityFilter(IPairList):
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
# Check symbol in cache
|
# Check symbol in cache
|
||||||
cached_res = self._pair_cache.get(pair, None)
|
if (cached_res := self._pair_cache.get(pair, None)) is not None:
|
||||||
if cached_res is not None:
|
|
||||||
return cached_res
|
return cached_res
|
||||||
|
|
||||||
result = False
|
result = False
|
||||||
|
|
|
@ -229,12 +229,8 @@ class VolumePairList(IPairList):
|
||||||
if p not in self._pair_cache
|
if p not in self._pair_cache
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get all candles
|
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
|
||||||
candles = {}
|
|
||||||
if needed_pairs:
|
|
||||||
candles = self._exchange.refresh_latest_ohlcv(
|
|
||||||
needed_pairs, since_ms=since_ms, cache=False
|
|
||||||
)
|
|
||||||
for i, p in enumerate(filtered_tickers):
|
for i, p in enumerate(filtered_tickers):
|
||||||
contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0
|
contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0
|
||||||
pair_candles = candles[
|
pair_candles = candles[
|
||||||
|
|
|
@ -101,11 +101,7 @@ class RangeStabilityFilter(IPairList):
|
||||||
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache]
|
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache]
|
||||||
|
|
||||||
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1))
|
since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1))
|
||||||
# Get all candles
|
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms)
|
||||||
candles = {}
|
|
||||||
if needed_pairs:
|
|
||||||
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms,
|
|
||||||
cache=False)
|
|
||||||
|
|
||||||
if self._enabled:
|
if self._enabled:
|
||||||
for p in deepcopy(pairlist):
|
for p in deepcopy(pairlist):
|
||||||
|
@ -123,8 +119,7 @@ class RangeStabilityFilter(IPairList):
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
# Check symbol in cache
|
# Check symbol in cache
|
||||||
cached_res = self._pair_cache.get(pair, None)
|
if (cached_res := self._pair_cache.get(pair, None)) is not None:
|
||||||
if cached_res is not None:
|
|
||||||
return cached_res
|
return cached_res
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
|
@ -288,6 +288,8 @@ class TradeSchema(BaseModel):
|
||||||
|
|
||||||
open_date: str
|
open_date: str
|
||||||
open_timestamp: int
|
open_timestamp: int
|
||||||
|
open_fill_date: Optional[str]
|
||||||
|
open_fill_timestamp: Optional[int]
|
||||||
open_rate: float
|
open_rate: float
|
||||||
open_rate_requested: Optional[float] = None
|
open_rate_requested: Optional[float] = None
|
||||||
open_trade_value: float
|
open_trade_value: float
|
||||||
|
|
|
@ -1365,7 +1365,7 @@ class Telegram(RPCHandler):
|
||||||
@authorized_only
|
@authorized_only
|
||||||
async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /buys PAIR .
|
Handler for /entries PAIR .
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
|
@ -1376,28 +1376,28 @@ class Telegram(RPCHandler):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
|
|
||||||
trades = self._rpc._rpc_enter_tag_performance(pair)
|
trades = self._rpc._rpc_enter_tag_performance(pair)
|
||||||
output = "<b>Entry Tag Performance:</b>\n"
|
output = "*Entry Tag Performance:*\n"
|
||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i + 1}.\t <code>{trade['enter_tag']}\t"
|
f"{i + 1}.\t `{trade['enter_tag']}\t"
|
||||||
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})`\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML)
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN)
|
||||||
output = stat_line
|
output = stat_line
|
||||||
else:
|
else:
|
||||||
output += stat_line
|
output += stat_line
|
||||||
|
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML,
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN,
|
||||||
reload_able=True, callback_path="update_enter_tag_performance",
|
reload_able=True, callback_path="update_enter_tag_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /sells.
|
Handler for /exits.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
|
@ -1408,21 +1408,21 @@ class Telegram(RPCHandler):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
|
|
||||||
trades = self._rpc._rpc_exit_reason_performance(pair)
|
trades = self._rpc._rpc_exit_reason_performance(pair)
|
||||||
output = "<b>Exit Reason Performance:</b>\n"
|
output = "*Exit Reason Performance:*\n"
|
||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i + 1}.\t <code>{trade['exit_reason']}\t"
|
f"{i + 1}.\t `{trade['exit_reason']}\t"
|
||||||
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})`\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML)
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN)
|
||||||
output = stat_line
|
output = stat_line
|
||||||
else:
|
else:
|
||||||
output += stat_line
|
output += stat_line
|
||||||
|
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML,
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN,
|
||||||
reload_able=True, callback_path="update_exit_reason_performance",
|
reload_able=True, callback_path="update_exit_reason_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
|
|
||||||
|
@ -1440,21 +1440,21 @@ class Telegram(RPCHandler):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
|
|
||||||
trades = self._rpc._rpc_mix_tag_performance(pair)
|
trades = self._rpc._rpc_mix_tag_performance(pair)
|
||||||
output = "<b>Mix Tag Performance:</b>\n"
|
output = "*Mix Tag Performance:*\n"
|
||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i + 1}.\t <code>{trade['mix_tag']}\t"
|
f"{i + 1}.\t `{trade['mix_tag']}\t"
|
||||||
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} "
|
||||||
f"({trade['profit_ratio']:.2%}) "
|
f"({trade['profit_ratio']:.2%}) "
|
||||||
f"({trade['count']})</code>\n")
|
f"({trade['count']})`\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML)
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN)
|
||||||
output = stat_line
|
output = stat_line
|
||||||
else:
|
else:
|
||||||
output += stat_line
|
output += stat_line
|
||||||
|
|
||||||
await self._send_msg(output, parse_mode=ParseMode.HTML,
|
await self._send_msg(output, parse_mode=ParseMode.MARKDOWN,
|
||||||
reload_able=True, callback_path="update_mix_tag_performance",
|
reload_able=True, callback_path="update_mix_tag_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
|
|
||||||
|
@ -1677,8 +1677,8 @@ class Telegram(RPCHandler):
|
||||||
" *table :* `will display trades in a table`\n"
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
|
"*/entries <pair|none>:* `Shows the enter_tag performance`\n"
|
||||||
"*/sells <pair|none>:* `Shows the exit reason performance`\n"
|
"*/exits <pair|none>:* `Shows the exit reason performance`\n"
|
||||||
"*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
|
"*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
|
||||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
|
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
|
||||||
dt_ts_def, dt_utc, format_date, format_ms_time,
|
dt_ts_def, dt_ts_none, dt_utc, format_date,
|
||||||
shorten_date)
|
format_ms_time, shorten_date)
|
||||||
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value
|
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value
|
||||||
from freqtrade.util.ft_precise import FtPrecise
|
from freqtrade.util.ft_precise import FtPrecise
|
||||||
from freqtrade.util.periodic_cache import PeriodicCache
|
from freqtrade.util.periodic_cache import PeriodicCache
|
||||||
|
@ -14,6 +14,7 @@ __all__ = [
|
||||||
'dt_now',
|
'dt_now',
|
||||||
'dt_ts',
|
'dt_ts',
|
||||||
'dt_ts_def',
|
'dt_ts_def',
|
||||||
|
'dt_ts_none',
|
||||||
'dt_utc',
|
'dt_utc',
|
||||||
'format_date',
|
'format_date',
|
||||||
'format_ms_time',
|
'format_ms_time',
|
||||||
|
|
|
@ -31,13 +31,23 @@ def dt_ts(dt: Optional[datetime] = None) -> int:
|
||||||
def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int:
|
def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int:
|
||||||
"""
|
"""
|
||||||
Return dt in ms as a timestamp in UTC.
|
Return dt in ms as a timestamp in UTC.
|
||||||
If dt is None, return the current datetime in UTC.
|
If dt is None, return the given default.
|
||||||
"""
|
"""
|
||||||
if dt:
|
if dt:
|
||||||
return int(dt.timestamp() * 1000)
|
return int(dt.timestamp() * 1000)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def dt_ts_none(dt: Optional[datetime]) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Return dt in ms as a timestamp in UTC.
|
||||||
|
If dt is None, return the given default.
|
||||||
|
"""
|
||||||
|
if dt:
|
||||||
|
return int(dt.timestamp() * 1000)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dt_floor_day(dt: datetime) -> datetime:
|
def dt_floor_day(dt: datetime) -> datetime:
|
||||||
"""Return the floor of the day for the given datetime."""
|
"""Return the floor of the day for the given datetime."""
|
||||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
|
@ -7,10 +7,10 @@
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
ruff==0.2.1
|
ruff==0.2.2
|
||||||
mypy==1.8.0
|
mypy==1.8.0
|
||||||
pre-commit==3.6.1
|
pre-commit==3.6.2
|
||||||
pytest==8.0.0
|
pytest==8.0.1
|
||||||
pytest-asyncio==0.23.5
|
pytest-asyncio==0.23.5
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
|
@ -26,6 +26,6 @@ nbconvert==7.16.0
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.7
|
types-cachetools==5.3.0.7
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.31.0.20240125
|
types-requests==2.31.0.20240218
|
||||||
types-tabulate==0.9.0.20240106
|
types-tabulate==0.9.0.20240106
|
||||||
types-python-dateutil==2.8.19.20240106
|
types-python-dateutil==2.8.19.20240106
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
|
|
||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.4.0
|
scikit-learn==1.4.1.post1
|
||||||
joblib==1.3.2
|
joblib==1.3.2
|
||||||
catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12'
|
catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12'
|
||||||
lightgbm==4.3.0
|
lightgbm==4.3.0
|
||||||
xgboost==2.0.3
|
xgboost==2.0.3
|
||||||
tensorboard==2.15.2
|
tensorboard==2.16.2
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.12.0
|
scipy==1.12.0
|
||||||
scikit-learn==1.4.0
|
scikit-learn==1.4.1.post1
|
||||||
ft-scikit-optimize==0.9.2
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.13.1
|
filelock==3.13.1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.18.0
|
plotly==5.19.0
|
||||||
|
|
|
@ -2,17 +2,17 @@ numpy==1.26.4
|
||||||
pandas==2.1.4
|
pandas==2.1.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.2.42
|
ccxt==4.2.47
|
||||||
cryptography==42.0.2
|
cryptography==42.0.3
|
||||||
aiohttp==3.9.3
|
aiohttp==3.9.3
|
||||||
SQLAlchemy==2.0.26
|
SQLAlchemy==2.0.27
|
||||||
python-telegram-bot==20.8
|
python-telegram-bot==20.8
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
arrow==1.3.0
|
arrow==1.3.0
|
||||||
cachetools==5.3.2
|
cachetools==5.3.2
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
urllib3==2.2.0
|
urllib3==2.2.1
|
||||||
jsonschema==4.21.1
|
jsonschema==4.21.1
|
||||||
TA-Lib==0.4.28
|
TA-Lib==0.4.28
|
||||||
technical==1.4.3
|
technical==1.4.3
|
||||||
|
@ -30,7 +30,7 @@ py_find_1st==1.1.6
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.14
|
python-rapidjson==1.14
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.9.13
|
orjson==3.9.14
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -70,7 +70,7 @@ setup(
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=4.2.15',
|
'ccxt>=4.2.47',
|
||||||
'SQLAlchemy>=2.0.6',
|
'SQLAlchemy>=2.0.6',
|
||||||
'python-telegram-bot>=20.1',
|
'python-telegram-bot>=20.1',
|
||||||
'arrow>=1.0.0',
|
'arrow>=1.0.0',
|
||||||
|
|
|
@ -455,6 +455,13 @@ def test_calculate_max_drawdown2():
|
||||||
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
||||||
calculate_max_drawdown(df, date_col='open_date', value_col='profit')
|
calculate_max_drawdown(df, date_col='open_date', value_col='profit')
|
||||||
|
|
||||||
|
df1 = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])
|
||||||
|
df1.loc[:, 'profit'] = df1['profit'] * -1
|
||||||
|
# No winning trade ...
|
||||||
|
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
|
||||||
|
df1, date_col='open_date', value_col='profit')
|
||||||
|
assert drawdown == 0.043965
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [
|
@pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [
|
||||||
([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909),
|
([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909),
|
||||||
|
|
|
@ -1,20 +1,40 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
from freqtrade.enums.tradingmode import TradingMode
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
def test_additional_exchange_init_bybit(default_conf, mocker):
|
def test_additional_exchange_init_bybit(default_conf, mocker, caplog):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
default_conf['trading_mode'] = TradingMode.FUTURES
|
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False})
|
api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False})
|
||||||
get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
|
api_mock.is_unified_enabled = MagicMock(return_value=[False, False])
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
|
||||||
assert api_mock.set_position_mode.call_count == 1
|
assert api_mock.set_position_mode.call_count == 1
|
||||||
|
assert api_mock.is_unified_enabled.call_count == 1
|
||||||
|
assert exchange.unified_account is False
|
||||||
|
|
||||||
|
assert log_has("Bybit: Standard account.", caplog)
|
||||||
|
|
||||||
|
api_mock.set_position_mode.reset_mock()
|
||||||
|
api_mock.is_unified_enabled = MagicMock(return_value=[False, True])
|
||||||
|
with pytest.raises(OperationalException, match=r"Bybit: Unified account is not supported.*"):
|
||||||
|
get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
|
||||||
|
assert log_has("Bybit: Unified account.", caplog)
|
||||||
|
# exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
|
||||||
|
# assert api_mock.set_position_mode.call_count == 1
|
||||||
|
# assert api_mock.is_unified_enabled.call_count == 1
|
||||||
|
# assert exchange.unified_account is True
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit',
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit',
|
||||||
"additional_exchange_init", "set_position_mode")
|
"additional_exchange_init", "set_position_mode")
|
||||||
|
|
||||||
|
@ -111,6 +131,7 @@ def test_bybit_fetch_order_canceled_empty(default_conf_usdt, mocker):
|
||||||
'amount': 20.0,
|
'amount': 20.0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit')
|
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit')
|
||||||
|
|
||||||
res = exchange.fetch_order('123', 'BTC/USDT')
|
res = exchange.fetch_order('123', 'BTC/USDT')
|
||||||
|
|
|
@ -2303,6 +2303,66 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
|
||||||
assert res[pair2].at[0, 'open']
|
assert res[pair2].at[0, 'open']
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||||
|
start = datetime(2021, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
ohlcv = generate_test_data_raw('1h', 100, start.strftime('%Y-%m-%d'))
|
||||||
|
time_machine.move_to(start, tick=False)
|
||||||
|
pairs = [
|
||||||
|
('ETH/BTC', '1d', CandleType.SPOT),
|
||||||
|
('TKN/BTC', '1d', CandleType.SPOT),
|
||||||
|
('LTC/BTC', '1d', CandleType.SPOT),
|
||||||
|
('LTC/BTC', '5m', CandleType.SPOT),
|
||||||
|
('LTC/BTC', '1h', CandleType.SPOT),
|
||||||
|
]
|
||||||
|
|
||||||
|
ohlcv_data = {
|
||||||
|
p: ohlcv for p in pairs
|
||||||
|
}
|
||||||
|
ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data)
|
||||||
|
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
assert len(exchange._expiring_candle_cache) == 0
|
||||||
|
|
||||||
|
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||||
|
assert ohlcv_mock.call_count == 1
|
||||||
|
assert ohlcv_mock.call_args_list[0][0][0] == pairs
|
||||||
|
assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
|
||||||
|
|
||||||
|
assert len(res) == 5
|
||||||
|
# length of 3 - as we have 3 different timeframes
|
||||||
|
assert len(exchange._expiring_candle_cache) == 3
|
||||||
|
|
||||||
|
ohlcv_mock.reset_mock()
|
||||||
|
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||||
|
assert ohlcv_mock.call_count == 0
|
||||||
|
|
||||||
|
# Expire 5m cache
|
||||||
|
time_machine.move_to(start + timedelta(minutes=6), tick=False)
|
||||||
|
|
||||||
|
ohlcv_mock.reset_mock()
|
||||||
|
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||||
|
assert ohlcv_mock.call_count == 1
|
||||||
|
assert len(ohlcv_mock.call_args_list[0][0][0]) == 1
|
||||||
|
|
||||||
|
# Expire 5m and 1h cache
|
||||||
|
time_machine.move_to(start + timedelta(hours=2), tick=False)
|
||||||
|
|
||||||
|
ohlcv_mock.reset_mock()
|
||||||
|
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||||
|
assert ohlcv_mock.call_count == 1
|
||||||
|
assert len(ohlcv_mock.call_args_list[0][0][0]) == 2
|
||||||
|
|
||||||
|
# Expire all caches
|
||||||
|
time_machine.move_to(start + timedelta(days=1, hours=2), tick=False)
|
||||||
|
|
||||||
|
ohlcv_mock.reset_mock()
|
||||||
|
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||||
|
assert ohlcv_mock.call_count == 1
|
||||||
|
assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
|
||||||
|
assert ohlcv_mock.call_args_list[0][0][0] == pairs
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
|
@ -3177,6 +3237,7 @@ def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, or
|
||||||
def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
|
def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
|
||||||
call_corder, call_forder):
|
call_corder, call_forder):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.cancel_order = MagicMock(return_value=corder)
|
api_mock.cancel_order = MagicMock(return_value=corder)
|
||||||
api_mock.fetch_order = MagicMock(return_value={})
|
api_mock.fetch_order = MagicMock(return_value={})
|
||||||
|
@ -3190,6 +3251,7 @@ def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog):
|
def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||||
|
@ -3287,6 +3349,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
order.symbol = 'TKN/BTC'
|
order.symbol = 'TKN/BTC'
|
||||||
|
|
||||||
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
exchange._dry_run_open_orders['X'] = order
|
exchange._dry_run_open_orders['X'] = order
|
||||||
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
|
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
|
||||||
|
@ -3331,10 +3394,80 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog):
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
default_conf['exchange']['log_responses'] = True
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
order.symbol = 'TKN/BTC'
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
mocker.patch(f'{EXMS}.exchange_has', return_value=False)
|
||||||
|
exchange._dry_run_open_orders['X'] = order
|
||||||
|
# Dry run - regular fetch_order behavior
|
||||||
|
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.fetch_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch(f'{EXMS}.exchange_has', return_value=False)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_open_order = MagicMock(
|
||||||
|
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
|
||||||
|
api_mock.fetch_closed_order = MagicMock(
|
||||||
|
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.fetch_order(
|
||||||
|
'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}
|
||||||
|
assert log_has(
|
||||||
|
("API fetch_open_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}"
|
||||||
|
),
|
||||||
|
caplog
|
||||||
|
)
|
||||||
|
assert api_mock.fetch_open_order.call_count == 1
|
||||||
|
assert api_mock.fetch_closed_order.call_count == 0
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# open_order doesn't find order
|
||||||
|
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
|
||||||
|
api_mock.fetch_closed_order = MagicMock(
|
||||||
|
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.fetch_order(
|
||||||
|
'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}
|
||||||
|
assert log_has(
|
||||||
|
("API fetch_closed_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}"
|
||||||
|
),
|
||||||
|
caplog
|
||||||
|
)
|
||||||
|
assert api_mock.fetch_open_order.call_count == 1
|
||||||
|
assert api_mock.fetch_closed_order.call_count == 1
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
api_mock.fetch_closed_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.fetch_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_open_order.call_count == 1
|
||||||
|
|
||||||
|
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
|
'fetch_order_emulated', 'fetch_open_order',
|
||||||
|
retries=1,
|
||||||
|
order_id='_', pair='TKN/BTC', params={})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
|
|
@ -324,7 +324,8 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||||
def exchange(request, exchange_conf):
|
def exchange(request, exchange_conf, class_mocker):
|
||||||
|
class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init')
|
||||||
yield from get_exchange(request.param, exchange_conf)
|
yield from get_exchange(request.param, exchange_conf)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1400,6 +1400,8 @@ def test_to_json(fee):
|
||||||
'is_open': None,
|
'is_open': None,
|
||||||
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
|
'open_fill_date': None,
|
||||||
|
'open_fill_timestamp': None,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'close_timestamp': None,
|
'close_timestamp': None,
|
||||||
'open_rate': 0.123,
|
'open_rate': 0.123,
|
||||||
|
@ -1486,6 +1488,8 @@ def test_to_json(fee):
|
||||||
'quote_currency': 'BTC',
|
'quote_currency': 'BTC',
|
||||||
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
|
'open_fill_date': None,
|
||||||
|
'open_fill_timestamp': None,
|
||||||
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
|
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'close_timestamp': int(trade.close_date.timestamp() * 1000),
|
'close_timestamp': int(trade.close_date.timestamp() * 1000),
|
||||||
'open_rate': 0.123,
|
'open_rate': 0.123,
|
||||||
|
|
|
@ -223,7 +223,7 @@ def test_trade_serialize_load_back(fee):
|
||||||
'realized_profit_ratio', 'close_profit_pct',
|
'realized_profit_ratio', 'close_profit_pct',
|
||||||
'trade_duration_s', 'trade_duration',
|
'trade_duration_s', 'trade_duration',
|
||||||
'profit_ratio', 'profit_pct', 'profit_abs', 'stop_loss_abs',
|
'profit_ratio', 'profit_pct', 'profit_abs', 'stop_loss_abs',
|
||||||
'initial_stop_loss_abs',
|
'initial_stop_loss_abs', 'open_fill_date', 'open_fill_timestamp',
|
||||||
'orders',
|
'orders',
|
||||||
]
|
]
|
||||||
failed = []
|
failed = []
|
||||||
|
|
|
@ -621,13 +621,20 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
|
||||||
"lookback_timeframe": "1d", "lookback_period": 6, "refresh_period": 86400}],
|
"lookback_timeframe": "1d", "lookback_period": 6, "refresh_period": 86400}],
|
||||||
"BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'HOT/BTC', 'NEO/BTC']),
|
"BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'HOT/BTC', 'NEO/BTC']),
|
||||||
|
# VolumePairlist in range mode as filter.
|
||||||
|
# TKN/BTC is removed because it doesn't have enough candles
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5},
|
||||||
|
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
|
||||||
|
"lookback_timeframe": "1d", "lookback_period": 2, "refresh_period": 86400}],
|
||||||
|
"BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'TKN/BTC', 'HOT/BTC']),
|
||||||
# ftx data is already in Quote currency, therefore won't require conversion
|
# ftx data is already in Quote currency, therefore won't require conversion
|
||||||
# ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
|
# ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
|
||||||
# "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],
|
# "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],
|
||||||
# "BTC", "ftx", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']),
|
# "BTC", "ftx", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']),
|
||||||
])
|
])
|
||||||
def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history,
|
def test_VolumePairList_range(
|
||||||
pairlists, base_currency, exchange, volumefilter_result) -> None:
|
mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history,
|
||||||
|
pairlists, base_currency, exchange, volumefilter_result, time_machine) -> None:
|
||||||
whitelist_conf['pairlists'] = pairlists
|
whitelist_conf['pairlists'] = pairlists
|
||||||
whitelist_conf['stake_currency'] = base_currency
|
whitelist_conf['stake_currency'] = base_currency
|
||||||
whitelist_conf['exchange']['name'] = exchange
|
whitelist_conf['exchange']['name'] = exchange
|
||||||
|
@ -686,23 +693,36 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||||
get_tickers=tickers,
|
get_tickers=tickers,
|
||||||
markets=PropertyMock(return_value=shitcoinmarkets)
|
markets=PropertyMock(return_value=shitcoinmarkets)
|
||||||
)
|
)
|
||||||
|
start_dt = dt_now()
|
||||||
|
time_machine.move_to(start_dt)
|
||||||
# remove ohlcv when looback_timeframe != 1d
|
# remove ohlcv when looback_timeframe != 1d
|
||||||
# to enforce fallback to ticker data
|
# to enforce fallback to ticker data
|
||||||
if 'lookback_timeframe' in pairlists[0]:
|
if 'lookback_timeframe' in pairlists[0]:
|
||||||
if pairlists[0]['lookback_timeframe'] != '1d':
|
if pairlists[0]['lookback_timeframe'] != '1d':
|
||||||
ohlcv_data = []
|
ohlcv_data = {}
|
||||||
|
|
||||||
mocker.patch.multiple(
|
ohclv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data)
|
||||||
EXMS,
|
|
||||||
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtrade.pairlists.refresh_pairlist()
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
whitelist = freqtrade.pairlists.whitelist
|
whitelist = freqtrade.pairlists.whitelist
|
||||||
|
assert ohclv_mock.call_count == 1
|
||||||
|
|
||||||
assert isinstance(whitelist, list)
|
assert isinstance(whitelist, list)
|
||||||
assert whitelist == volumefilter_result
|
assert whitelist == volumefilter_result
|
||||||
|
# Test caching
|
||||||
|
ohclv_mock.reset_mock()
|
||||||
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
# in "filter" mode, caching is disabled.
|
||||||
|
assert ohclv_mock.call_count == 0
|
||||||
|
whitelist = freqtrade.pairlists.whitelist
|
||||||
|
assert whitelist == volumefilter_result
|
||||||
|
|
||||||
|
time_machine.move_to(start_dt + timedelta(days=2))
|
||||||
|
ohclv_mock.reset_mock()
|
||||||
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
assert ohclv_mock.call_count == 1
|
||||||
|
whitelist = freqtrade.pairlists.whitelist
|
||||||
|
assert whitelist == volumefilter_result
|
||||||
|
|
||||||
|
|
||||||
def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
|
def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
|
||||||
|
|
|
@ -25,6 +25,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||||
'quote_currency': 'BTC',
|
'quote_currency': 'BTC',
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
|
'open_fill_date': ANY,
|
||||||
|
'open_fill_timestamp': ANY,
|
||||||
'is_open': ANY,
|
'is_open': ANY,
|
||||||
'fee_open': ANY,
|
'fee_open': ANY,
|
||||||
'fee_open_cost': ANY,
|
'fee_open_cost': ANY,
|
||||||
|
|
|
@ -180,7 +180,9 @@ def test_api_auth():
|
||||||
|
|
||||||
def test_api_ws_auth(botclient):
|
def test_api_ws_auth(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
def url(token): return f"/api/v1/message/ws?token={token}"
|
|
||||||
|
def url(token):
|
||||||
|
return f"/api/v1/message/ws?token={token}"
|
||||||
|
|
||||||
bad_token = "bad-ws_token"
|
bad_token = "bad-ws_token"
|
||||||
with pytest.raises(WebSocketDisconnect):
|
with pytest.raises(WebSocketDisconnect):
|
||||||
|
@ -1165,6 +1167,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
|
'open_fill_date': ANY,
|
||||||
|
'open_fill_timestamp': ANY,
|
||||||
'open_rate': 0.123,
|
'open_rate': 0.123,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'ETH',
|
'base_currency': 'ETH',
|
||||||
|
@ -1368,6 +1372,8 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||||
'close_rate': 0.265441,
|
'close_rate': 0.265441,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
|
'open_fill_date': ANY,
|
||||||
|
'open_fill_timestamp': ANY,
|
||||||
'open_rate': 0.245441,
|
'open_rate': 0.245441,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'ETH',
|
'base_currency': 'ETH',
|
||||||
|
|
|
@ -1507,7 +1507,7 @@ async def test_telegram_entry_tag_performance_handle(
|
||||||
await telegram._enter_tag_performance(update=update, context=context)
|
await telegram._enter_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TEST1\t3.987 USDT (5.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '`TEST1\t3.987 USDT (5.00%) (1)`' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
context.args = ['XRP/USDT']
|
context.args = ['XRP/USDT']
|
||||||
await telegram._enter_tag_performance(update=update, context=context)
|
await telegram._enter_tag_performance(update=update, context=context)
|
||||||
|
@ -1538,7 +1538,7 @@ async def test_telegram_exit_reason_performance_handle(
|
||||||
await telegram._exit_reason_performance(update=update, context=context)
|
await telegram._exit_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>roi\t2.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '`roi\t2.842 USDT (10.00%) (1)`' in msg_mock.call_args_list[0][0][0]
|
||||||
context.args = ['XRP/USDT']
|
context.args = ['XRP/USDT']
|
||||||
|
|
||||||
await telegram._exit_reason_performance(update=update, context=context)
|
await telegram._exit_reason_performance(update=update, context=context)
|
||||||
|
@ -1570,7 +1570,7 @@ async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ti
|
||||||
await telegram._mix_tag_performance(update=update, context=context)
|
await telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert ('<code>TEST3 roi\t2.842 USDT (10.00%) (1)</code>'
|
assert ('`TEST3 roi\t2.842 USDT (10.00%) (1)`'
|
||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
context.args = ['XRP/USDT']
|
context.args = ['XRP/USDT']
|
||||||
|
|
|
@ -3,8 +3,8 @@ from datetime import datetime, timedelta, timezone
|
||||||
import pytest
|
import pytest
|
||||||
import time_machine
|
import time_machine
|
||||||
|
|
||||||
from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def, dt_utc,
|
from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def,
|
||||||
format_date, format_ms_time, shorten_date)
|
dt_ts_none, dt_utc, format_date, format_ms_time, shorten_date)
|
||||||
|
|
||||||
|
|
||||||
def test_dt_now():
|
def test_dt_now():
|
||||||
|
@ -29,6 +29,13 @@ def test_dt_ts_def():
|
||||||
assert dt_ts_def(datetime(2023, 5, 5, tzinfo=timezone.utc), 123) == 1683244800000
|
assert dt_ts_def(datetime(2023, 5, 5, tzinfo=timezone.utc), 123) == 1683244800000
|
||||||
|
|
||||||
|
|
||||||
|
def test_dt_ts_none():
|
||||||
|
assert dt_ts_none(None) is None
|
||||||
|
assert dt_ts_none(None) is None
|
||||||
|
assert dt_ts_none(datetime(2023, 5, 5, tzinfo=timezone.utc)) == 1683244800000
|
||||||
|
assert dt_ts_none(datetime(2023, 5, 5, tzinfo=timezone.utc)) == 1683244800000
|
||||||
|
|
||||||
|
|
||||||
def test_dt_utc():
|
def test_dt_utc():
|
||||||
assert dt_utc(2023, 5, 5) == datetime(2023, 5, 5, tzinfo=timezone.utc)
|
assert dt_utc(2023, 5, 5) == datetime(2023, 5, 5, tzinfo=timezone.utc)
|
||||||
assert dt_utc(2023, 5, 5, 0, 0, 0, 555500) == datetime(2023, 5, 5, 0, 0, 0, 555500,
|
assert dt_utc(2023, 5, 5, 0, 0, 0, 555500) == datetime(2023, 5, 5, 0, 0, 0, 555500,
|
||||||
|
|
|
@ -5,7 +5,7 @@ from freqtrade.util import PeriodicCache
|
||||||
|
|
||||||
def test_ttl_cache():
|
def test_ttl_cache():
|
||||||
|
|
||||||
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
with time_machine.travel("2021-09-01 05:00:00 +00:00", tick=False) as t:
|
||||||
|
|
||||||
cache = PeriodicCache(5, ttl=60)
|
cache = PeriodicCache(5, ttl=60)
|
||||||
cache1h = PeriodicCache(5, ttl=3600)
|
cache1h = PeriodicCache(5, ttl=3600)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user