mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 01:21:11 +00:00
Merge pull request #9348 from freqtrade/new_release
New release 2023.10
This commit is contained in:
commit
f142abfb76
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -108,7 +108,7 @@ jobs:
|
|||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
ruff check --output-format=github .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
|
@ -217,7 +217,7 @@ jobs:
|
|||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
ruff check --output-format=github .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
|
@ -287,7 +287,7 @@ jobs:
|
|||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
ruff check --output-format=github .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
|
|
|
@ -15,10 +15,10 @@ repos:
|
|||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.6
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.31.0.4
|
||||
- types-requests==2.31.0.10
|
||||
- types-tabulate==0.9.0.3
|
||||
- types-python-dateutil==2.8.19.14
|
||||
- SQLAlchemy==2.0.21
|
||||
- SQLAlchemy==2.0.22
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
@ -30,7 +30,7 @@ repos:
|
|||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.270'
|
||||
rev: 'v0.1.1'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ WORKDIR /freqtrade
|
|||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libopenblas-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip \
|
||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||
|
|
|
@ -31,9 +31,9 @@ optional arguments:
|
|||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--data-format-ohlcv {json,jsongz,hdf5}
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
(default: `feather`).
|
||||
--max-open-trades INT
|
||||
Override the value of the `max_open_trades`
|
||||
configuration setting.
|
||||
|
|
|
@ -136,6 +136,43 @@ Freqtrade will not attempt to change these settings.
|
|||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||
|
||||
To speed up downloading, you can download the [trades zip files](https://support.kraken.com/hc/en-us/articles/360047543791-Downloadable-historical-market-data-time-and-sales-) kraken provides.
|
||||
These are usually updated once per quarter. Freqtrade expects these files to be placed in `user_data/data/kraken/trades_csv`.
|
||||
|
||||
A structure as follows can make sense if using incremental files, with the "full" history in one directory, and incremental files in different directories.
|
||||
The assumption for this mode is that the data is downloaded and unzipped keeping filenames as they are.
|
||||
Duplicate content will be ignored (based on timestamp) - though the assumption is that there is no gap in the data.
|
||||
|
||||
This means, if your "full" history ends in Q4 2022 - then both incremental updates Q1 2023 and Q2 2023 are available.
|
||||
Not having this will lead to incomplete data, and therefore invalid results while using the data.
|
||||
|
||||
```
|
||||
└── trades_csv
|
||||
├── Kraken_full_history
|
||||
│ ├── BCHEUR.csv
|
||||
│ └── XBTEUR.csv
|
||||
├── Kraken_Trading_History_Q1_2023
|
||||
│ ├── BCHEUR.csv
|
||||
│ └── XBTEUR.csv
|
||||
└── Kraken_Trading_History_Q2_2023
|
||||
├── BCHEUR.csv
|
||||
└── XBTEUR.csv
|
||||
```
|
||||
|
||||
You can convert these files into freqtrade files:
|
||||
|
||||
``` bash
|
||||
freqtrade convert-trade-data --exchange kraken --format-from kraken_csv --format-to feather
|
||||
# Convert trade data to different ohlcv timeframes
|
||||
freqtrade trades-to-ohlcv -p BTC/EUR BCH/EUR --exchange kraken -t 1m 5m 15m 1h
|
||||
```
|
||||
|
||||
The converted data also makes downloading data possible, and will start the download after the latest loaded trade.
|
||||
|
||||
``` bash
|
||||
freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
||||
```
|
||||
|
||||
!!! Warning "Downloading data from kraken"
|
||||
Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine.
|
||||
It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient.
|
||||
|
|
|
@ -40,11 +40,41 @@ usage: freqtrade recursive-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||
[--startup-candle STARTUP_CANDLES [STARTUP_CANDLES ...]]
|
||||
|
||||
optional arguments:
|
||||
-p PAIR, --pairs PAIR
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `feather`).
|
||||
-p PAIR, --pairs PAIR
|
||||
Limit command to this pair.
|
||||
--startup-candle STARTUP_CANDLE [STARTUP_CANDLE ...]
|
||||
--startup-candle STARTUP_CANDLE [STARTUP_CANDLE ...]
|
||||
Provide a space-separated list of startup_candle_count to
|
||||
be checked. Default : `199 399 499 999 1999`.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
Strategy arguments:
|
||||
-s NAME, --strategy NAME
|
||||
Specify strategy class name which will be used by the
|
||||
bot.
|
||||
--strategy-path PATH Specify additional strategy lookup path.
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
```
|
||||
|
||||
### Why are odd-numbered default startup candles used?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
markdown==3.4.4
|
||||
markdown==3.5
|
||||
mkdocs==1.5.3
|
||||
mkdocs-material==9.4.1
|
||||
mkdocs-material==9.4.6
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.3
|
||||
pymdown-extensions==10.3.1
|
||||
jinja2==3.1.2
|
||||
|
|
|
@ -1008,6 +1008,10 @@ The following lists some common patterns which should be avoided to prevent frus
|
|||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
|
||||
!!! Tip "Identifying problems"
|
||||
You may also want to check the 2 helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||
Please treat them as what they are - helpers to identify most common problems. A negative result of each does not guarantee that there's none of the above errors included.
|
||||
|
||||
### Colliding signals
|
||||
|
||||
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" Freqtrade bot """
|
||||
__version__ = '2023.9'
|
||||
__version__ = '2023.10'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -65,8 +65,8 @@ ARGS_BUILD_CONFIG = ["config"]
|
|||
|
||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||
|
||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
@ -268,7 +268,7 @@ class Arguments:
|
|||
parents=[_common_parser],
|
||||
)
|
||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
||||
self._build_args(optionlist=ARGS_CONVERT_DATA_TRADES, parser=convert_trade_data_cmd)
|
||||
|
||||
# Add trades-to-ohlcv subcommand
|
||||
convert_trade_data_cmd = subparsers.add_parser(
|
||||
|
|
|
@ -421,6 +421,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||
'desired timeframe as specified as --timeframes/-t.',
|
||||
action='store_true',
|
||||
),
|
||||
"format_from_trades": Arg(
|
||||
'--format-from',
|
||||
help='Source format for data conversion.',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS + ['kraken_csv'],
|
||||
required=True,
|
||||
),
|
||||
"format_from": Arg(
|
||||
'--format-from',
|
||||
help='Source format for data conversion.',
|
||||
|
|
|
@ -85,7 +85,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||
erase=args['erase'])
|
||||
else:
|
||||
convert_trades_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
convert_from=args['format_from_trades'], convert_to=args['format_to'],
|
||||
erase=args['erase'])
|
||||
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
|||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
||||
from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFunctions
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
LookaheadAnalysisSubFunctions.start(config)
|
||||
|
@ -152,7 +152,7 @@ def start_recursive_analysis(args: Dict[str, Any]) -> None:
|
|||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.recursive_analysis_helpers import RecursiveAnalysisSubFunctions
|
||||
from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
RecursiveAnalysisSubFunctions.start(config)
|
||||
|
|
|
@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange
|
|||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||
Config, TradeList)
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -127,6 +128,16 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er
|
|||
:param convert_to: Target format
|
||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||
"""
|
||||
if convert_from == 'kraken_csv':
|
||||
if config['exchange']['name'] != 'kraken':
|
||||
raise OperationalException(
|
||||
'Converting from csv is only supported for kraken.'
|
||||
'Please refer to the documentation for details about this special mode.'
|
||||
)
|
||||
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
||||
import_kraken_trades_from_csv(config, convert_to)
|
||||
return
|
||||
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
trg = get_datahandler(config['datadir'], convert_to)
|
||||
|
|
70
freqtrade/data/converter/trade_converter_kraken.py
Normal file
70
freqtrade/data/converter/trade_converter_kraken.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
|
||||
from freqtrade.data.converter.trade_converter import (trades_convert_types,
|
||||
trades_df_remove_duplicates)
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KRAKEN_CSV_TRADE_COLUMNS = ['timestamp', 'price', 'amount']
|
||||
|
||||
|
||||
def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||
"""
|
||||
Import kraken trades from csv
|
||||
"""
|
||||
if config['exchange']['name'] != 'kraken':
|
||||
raise OperationalException('This function is only for the kraken exchange.')
|
||||
|
||||
datadir: Path = config['datadir']
|
||||
data_handler = get_datahandler(datadir, data_format=convert_to)
|
||||
|
||||
tradesdir: Path = config['datadir'] / 'trades_csv'
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
# iterate through directories in this directory
|
||||
data_symbols = {p.stem for p in tradesdir.rglob('*.csv')}
|
||||
|
||||
# create pair/filename mapping
|
||||
markets = {
|
||||
(m['symbol'], m['altname']) for m in exchange.markets.values()
|
||||
if m.get('altname') in data_symbols
|
||||
}
|
||||
logger.info(f"Found csv files for {', '.join(data_symbols)}.")
|
||||
|
||||
for pair, name in markets:
|
||||
dfs = []
|
||||
# Load and combine all csv files for this pair
|
||||
for f in tradesdir.rglob(f"{name}.csv"):
|
||||
df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS)
|
||||
dfs.append(df)
|
||||
|
||||
# Load existing trades data
|
||||
if not dfs:
|
||||
# edgecase, can only happen if the file was deleted between the above glob and here
|
||||
logger.info(f"No data found for pair {pair}")
|
||||
continue
|
||||
|
||||
trades = pd.concat(dfs, ignore_index=True)
|
||||
|
||||
trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3
|
||||
trades.loc[:, 'cost'] = trades['price'] * trades['amount']
|
||||
for col in DEFAULT_TRADES_COLUMNS:
|
||||
if col not in trades.columns:
|
||||
trades[col] = ''
|
||||
|
||||
trades = trades[DEFAULT_TRADES_COLUMNS]
|
||||
trades = trades_convert_types(trades)
|
||||
|
||||
trades_df = trades_df_remove_duplicates(trades)
|
||||
logger.info(f"{pair}: {len(trades_df)} trades, from "
|
||||
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
|
||||
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}")
|
||||
|
||||
data_handler.trades_store(pair, trades_df)
|
|
@ -123,10 +123,14 @@ class Binance(Exchange):
|
|||
|
||||
def funding_fee_cutoff(self, open_date: datetime):
|
||||
"""
|
||||
Funding fees are only charged at full hours (usually every 4-8h).
|
||||
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||
On binance, this cutoff is 15s.
|
||||
https://github.com/freqtrade/freqtrade/pull/5779#discussion_r740175931
|
||||
:param open_date: The open date for a trade
|
||||
:return: The cutoff open time for when a funding fee is charged
|
||||
:return: True if the date falls on a full hour, False otherwise
|
||||
"""
|
||||
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
|
||||
return open_date.minute == 0 and open_date.second < 15
|
||||
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ import ccxt
|
|||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||
|
@ -202,8 +202,11 @@ class Bybit(Exchange):
|
|||
"""
|
||||
# Bybit does not provide "applied" funding fees per position.
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
return self._fetch_and_calculate_funding_fees(
|
||||
pair, amount, is_short, open_date)
|
||||
try:
|
||||
return self._fetch_and_calculate_funding_fees(
|
||||
pair, amount, is_short, open_date)
|
||||
except ExchangeError:
|
||||
logger.warning(f"Could not update funding fees for {pair}.")
|
||||
return 0.0
|
||||
|
||||
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
|
||||
|
|
|
@ -2279,6 +2279,7 @@ class Exchange:
|
|||
|
||||
from_id = t[-1][1]
|
||||
else:
|
||||
logger.debug("Stopping as no more trades were returned.")
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||
|
@ -2304,6 +2305,11 @@ class Exchange:
|
|||
try:
|
||||
t = await self._async_fetch_trades(pair, since=since)
|
||||
if t:
|
||||
# No more trades to download available at the exchange,
|
||||
# So we repeatedly get the same trade over and over again.
|
||||
if since == t[-1][0] and len(t) == 1:
|
||||
logger.debug("Stopping because no more trades are available.")
|
||||
break
|
||||
since = t[-1][0]
|
||||
trades.extend(t)
|
||||
# Reached the end of the defined-download period
|
||||
|
@ -2312,6 +2318,7 @@ class Exchange:
|
|||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
||||
break
|
||||
else:
|
||||
logger.debug("Stopping as no more trades were returned.")
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||
|
@ -2653,12 +2660,14 @@ class Exchange:
|
|||
"""
|
||||
return 0.0
|
||||
|
||||
def funding_fee_cutoff(self, open_date: datetime):
|
||||
def funding_fee_cutoff(self, open_date: datetime) -> bool:
|
||||
"""
|
||||
Funding fees are only charged at full hours (usually every 4-8h).
|
||||
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||
:param open_date: The open date for a trade
|
||||
:return: The cutoff open time for when a funding fee is charged
|
||||
:return: True if the date falls on a full hour, False otherwise
|
||||
"""
|
||||
return open_date.minute > 0 or open_date.second > 0
|
||||
return open_date.minute == 0 and open_date.second == 0
|
||||
|
||||
@retrier
|
||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
||||
|
@ -2706,15 +2715,16 @@ class Exchange:
|
|||
"""
|
||||
|
||||
if self.funding_fee_cutoff(open_date):
|
||||
open_date += timedelta(hours=1)
|
||||
# Shift back to 1h candle to avoid missing funding fees
|
||||
# Only really relevant for trades very close to the full hour
|
||||
open_date = timeframe_to_prev_date('1h', open_date)
|
||||
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
||||
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
||||
self._ft_has['mark_ohlcv_timeframe'])
|
||||
|
||||
if not close_date:
|
||||
close_date = datetime.now(timezone.utc)
|
||||
open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
||||
# close_timestamp = int(close_date.timestamp()) * 1000
|
||||
since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
||||
|
||||
mark_comb: PairWithTimeframe = (
|
||||
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
||||
|
@ -2722,7 +2732,7 @@ class Exchange:
|
|||
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||
candle_histories = self.refresh_latest_ohlcv(
|
||||
[mark_comb, funding_comb],
|
||||
since_ms=open_timestamp,
|
||||
since_ms=since_ms,
|
||||
cache=False,
|
||||
drop_incomplete=False,
|
||||
)
|
||||
|
@ -2733,8 +2743,7 @@ class Exchange:
|
|||
except KeyError:
|
||||
raise ExchangeError("Could not find funding rates.") from None
|
||||
|
||||
funding_mark_rates = self.combine_funding_and_mark(
|
||||
funding_rates=funding_rates, mark_rates=mark_rates)
|
||||
funding_mark_rates = self.combine_funding_and_mark(funding_rates, mark_rates)
|
||||
|
||||
return self.calculate_funding_fees(
|
||||
funding_mark_rates,
|
||||
|
@ -2781,7 +2790,7 @@ class Exchange:
|
|||
amount: float,
|
||||
is_short: bool,
|
||||
open_date: datetime,
|
||||
close_date: Optional[datetime] = None,
|
||||
close_date: datetime,
|
||||
time_in_ratio: Optional[float] = None
|
||||
) -> float:
|
||||
"""
|
||||
|
@ -2797,8 +2806,8 @@ class Exchange:
|
|||
fees: float = 0
|
||||
|
||||
if not df.empty:
|
||||
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
||||
fees = sum(df['open_fund'] * df['open_mark'] * amount)
|
||||
df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
||||
fees = sum(df1['open_fund'] * df1['open_mark'] * amount)
|
||||
|
||||
# Negate fees for longs as funding_fees expects it this way based on live endpoints.
|
||||
return fees if is_short else -fees
|
||||
|
@ -2813,17 +2822,19 @@ class Exchange:
|
|||
:param amount: Trade amount
|
||||
:param open_date: Open date of the trade
|
||||
:return: funding fee since open_date
|
||||
:raises: ExchangeError if something goes wrong.
|
||||
"""
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
if self._config['dry_run']:
|
||||
funding_fees = self._fetch_and_calculate_funding_fees(
|
||||
pair, amount, is_short, open_date)
|
||||
else:
|
||||
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
||||
return funding_fees
|
||||
else:
|
||||
return 0.0
|
||||
try:
|
||||
if self._config['dry_run']:
|
||||
funding_fees = self._fetch_and_calculate_funding_fees(
|
||||
pair, amount, is_short, open_date)
|
||||
else:
|
||||
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
||||
return funding_fees
|
||||
except ExchangeError:
|
||||
logger.warning(f"Could not update funding fees for {pair}.")
|
||||
|
||||
return 0.0
|
||||
|
||||
def get_liquidation_price(
|
||||
self,
|
||||
|
|
|
@ -195,7 +195,7 @@ class Kraken(Exchange):
|
|||
amount: float,
|
||||
is_short: bool,
|
||||
open_date: datetime,
|
||||
close_date: Optional[datetime] = None,
|
||||
close_date: datetime,
|
||||
time_in_ratio: Optional[float] = None
|
||||
) -> float:
|
||||
"""
|
||||
|
|
|
@ -159,7 +159,7 @@ class BaseEnvironment(gym.Env):
|
|||
function is designed for tracking incremented objects,
|
||||
events, actions inside the training environment.
|
||||
For example, a user can call this to track the
|
||||
frequency of occurence of an `is_valid` call in
|
||||
frequency of occurrence of an `is_valid` call in
|
||||
their `calculate_reward()`:
|
||||
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
|
|
|
@ -296,8 +296,7 @@ class FreqaiDataDrawer:
|
|||
f"for more than {len(dataframe.index)} candles.")
|
||||
|
||||
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys())
|
||||
# remove last row because we will append that later in append_model_predictions()
|
||||
df_concat = df_concat.iloc[:-1]
|
||||
|
||||
# any missing values will get zeroed out so users can see the exact
|
||||
# downtime in FreqUI
|
||||
df_concat = df_concat.fillna(0)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Type
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
import torch as th
|
||||
from stable_baselines3.common.callbacks import ProgressBarCallback
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||
|
@ -73,19 +74,27 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||
'trained agent.')
|
||||
model = self.dd.model_dictionary[dk.pair]
|
||||
model.set_env(self.train_env)
|
||||
callbacks: List[Any] = [self.eval_callback, self.tensorboard_callback]
|
||||
progressbar_callback: Optional[ProgressBarCallback] = None
|
||||
if self.rl_config.get('progress_bar', False):
|
||||
progressbar_callback = ProgressBarCallback()
|
||||
callbacks.insert(0, progressbar_callback)
|
||||
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=[self.eval_callback, self.tensorboard_callback],
|
||||
progress_bar=self.rl_config.get('progress_bar', False)
|
||||
)
|
||||
try:
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=callbacks,
|
||||
)
|
||||
finally:
|
||||
if progressbar_callback:
|
||||
progressbar_callback.on_training_end()
|
||||
|
||||
if Path(dk.data_path / "best_model.zip").is_file():
|
||||
logger.info('Callback found a best model.')
|
||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||
return best_model
|
||||
|
||||
logger.info('Couldnt find best model, using final model instead.')
|
||||
logger.info("Couldn't find best model, using final model instead.")
|
||||
|
||||
return model
|
||||
|
||||
|
|
|
@ -49,7 +49,13 @@ class TensorboardCallback(BaseCallback):
|
|||
local_info = self.locals["infos"][0]
|
||||
if self.training_env is None:
|
||||
return True
|
||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||
|
||||
if hasattr(self.training_env, 'envs'):
|
||||
tensorboard_metrics = self.training_env.envs[0].unwrapped.tensorboard_metrics
|
||||
|
||||
else:
|
||||
# For RL-multiproc - usage of [0] might need to be evaluated
|
||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||
|
||||
for metric in local_info:
|
||||
if metric not in ["episode", "terminal_observation"]:
|
||||
|
|
|
@ -132,7 +132,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
# TODO: This would be more efficient if scheduled in utc time, and performed at each
|
||||
# TODO: funding interval, specified by funding_fee_times on the exchange classes
|
||||
for time_slot in range(0, 24):
|
||||
for minutes in [0, 15, 30, 45]:
|
||||
for minutes in [1, 31]:
|
||||
t = str(time(time_slot, minutes, 2))
|
||||
self._schedule.every().day.at(t).do(update)
|
||||
self.last_process: Optional[datetime] = None
|
||||
|
@ -199,6 +199,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
# Only update open orders on startup
|
||||
# This will update the database after the initial migration
|
||||
self.startup_update_open_orders()
|
||||
self.update_funding_fees()
|
||||
|
||||
def process(self) -> None:
|
||||
"""
|
||||
|
@ -312,22 +313,19 @@ class FreqtradeBot(LoggingMixin):
|
|||
open_trades = Trade.get_open_trade_count()
|
||||
return max(0, self.config['max_open_trades'] - open_trades)
|
||||
|
||||
def update_funding_fees(self):
|
||||
def update_funding_fees(self) -> None:
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
trades = Trade.get_open_trades()
|
||||
try:
|
||||
for trade in trades:
|
||||
funding_fees = self.exchange.get_funding_fees(
|
||||
trades: List[Trade] = Trade.get_open_trades()
|
||||
for trade in trades:
|
||||
trade.set_funding_fees(
|
||||
self.exchange.get_funding_fees(
|
||||
pair=trade.pair,
|
||||
amount=trade.amount,
|
||||
is_short=trade.is_short,
|
||||
open_date=trade.date_last_filled_utc
|
||||
)
|
||||
trade.funding_fees = funding_fees
|
||||
except ExchangeError:
|
||||
logger.warning("Could not update funding fees for open trades.")
|
||||
open_date=trade.date_last_filled_utc)
|
||||
)
|
||||
|
||||
def startup_backpopulate_precision(self):
|
||||
def startup_backpopulate_precision(self) -> None:
|
||||
|
||||
trades = Trade.get_trades([Trade.contract_size.is_(None)])
|
||||
for trade in trades:
|
||||
|
@ -374,17 +372,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
fo = order.to_ccxt_object()
|
||||
fo['status'] = 'canceled'
|
||||
self.handle_cancel_order(
|
||||
fo, order.order_id, order.trade,
|
||||
constants.CANCEL_REASON['TIMEOUT']
|
||||
fo, order, order.trade, constants.CANCEL_REASON['TIMEOUT']
|
||||
)
|
||||
|
||||
except ExchangeError as e:
|
||||
|
||||
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._schedule.run_pending()
|
||||
|
||||
def update_trades_without_assigned_fees(self) -> None:
|
||||
"""
|
||||
Update closed trades without close fees assigned.
|
||||
|
@ -749,6 +743,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:param stake_amount: amount of stake-currency for the pair
|
||||
:return: True if a buy order is created, false if it fails.
|
||||
:raise: DependencyException or it's subclasses like ExchangeError.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['entry']
|
||||
|
||||
|
@ -835,14 +830,15 @@ class FreqtradeBot(LoggingMixin):
|
|||
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||
open_date = datetime.now(timezone.utc)
|
||||
|
||||
funding_fees = self.exchange.get_funding_fees(
|
||||
pair=pair,
|
||||
amount=amount + trade.amount if trade else amount,
|
||||
is_short=is_short,
|
||||
open_date=trade.date_last_filled_utc if trade else open_date
|
||||
)
|
||||
|
||||
# This is a new trade
|
||||
if trade is None:
|
||||
funding_fees = 0.0
|
||||
try:
|
||||
funding_fees = self.exchange.get_funding_fees(
|
||||
pair=pair, amount=amount, is_short=is_short, open_date=open_date)
|
||||
except ExchangeError:
|
||||
logger.warning("Could not find funding fee.")
|
||||
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
|
@ -878,6 +874,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
trade.is_open = True
|
||||
trade.fee_open_currency = None
|
||||
trade.open_rate_requested = enter_limit_requested
|
||||
trade.set_funding_fees(funding_fees)
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
trade.recalc_trade_from_orders()
|
||||
|
@ -1087,7 +1084,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
trades_closed = 0
|
||||
for trade in trades:
|
||||
|
||||
if not trade.has_open_orders and not self.wallets.check_exit_amount(trade):
|
||||
if (
|
||||
not trade.has_open_orders
|
||||
and not trade.stoploss_order_id
|
||||
and not self.wallets.check_exit_amount(trade)
|
||||
):
|
||||
logger.warning(
|
||||
f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
|
||||
'Trying to recover.')
|
||||
|
@ -1331,6 +1332,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
:return: None
|
||||
"""
|
||||
for trade in Trade.get_open_trades():
|
||||
open_order: Order
|
||||
for open_order in trade.open_orders:
|
||||
try:
|
||||
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
|
||||
|
@ -1351,22 +1353,23 @@ class FreqtradeBot(LoggingMixin):
|
|||
)
|
||||
):
|
||||
self.handle_cancel_order(
|
||||
order, open_order.order_id, trade, constants.CANCEL_REASON['TIMEOUT']
|
||||
order, open_order, trade, constants.CANCEL_REASON['TIMEOUT']
|
||||
)
|
||||
else:
|
||||
self.replace_order(order, open_order, trade)
|
||||
|
||||
def handle_cancel_order(self, order: Dict, order_id: str, trade: Trade, reason: str) -> None:
|
||||
def handle_cancel_order(self, order: Dict, order_obj: Order, trade: Trade, reason: str) -> None:
|
||||
"""
|
||||
Check if current analyzed order timed out and cancel if necessary.
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
:param order_obj: Order object from the database.
|
||||
:param trade: Trade object.
|
||||
:return: None
|
||||
"""
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, order_id, reason)
|
||||
self.handle_cancel_enter(trade, order, order_obj, reason)
|
||||
else:
|
||||
canceled = self.handle_cancel_exit(trade, order, order_id, reason)
|
||||
canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
canceled_count = trade.get_canceled_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
if (canceled and max_timeouts > 0 and canceled_count >= max_timeouts):
|
||||
|
@ -1431,7 +1434,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
trade=trade, order=order_obj, pair=trade.pair,
|
||||
current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
|
||||
current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag,
|
||||
side=trade.entry_side)
|
||||
side=trade.trade_direction)
|
||||
|
||||
replacing = True
|
||||
cancel_reason = constants.CANCEL_REASON['REPLACE']
|
||||
|
@ -1440,7 +1443,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
||||
if order_obj.price != adjusted_entry_price:
|
||||
# cancel existing order if new price is supplied or None
|
||||
res = self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason,
|
||||
res = self.handle_cancel_enter(trade, order, order_obj, cancel_reason,
|
||||
replacing=replacing)
|
||||
if not res:
|
||||
self.replace_order_failed(
|
||||
|
@ -1448,15 +1451,21 @@ class FreqtradeBot(LoggingMixin):
|
|||
return
|
||||
if adjusted_entry_price:
|
||||
# place new order only if new price is supplied
|
||||
if not self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
|
||||
price=adjusted_entry_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode='replace',
|
||||
):
|
||||
try:
|
||||
if not self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
|
||||
price=adjusted_entry_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode='replace',
|
||||
):
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not replace order for {trade}.")
|
||||
except DependencyException as exception:
|
||||
logger.warning(
|
||||
f'Unable to replace order for {trade.pair}: {exception}')
|
||||
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
||||
|
||||
def cancel_all_open_orders(self) -> None:
|
||||
|
@ -1475,29 +1484,28 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(
|
||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
)
|
||||
|
||||
elif order['side'] == trade.exit_side:
|
||||
self.handle_cancel_exit(
|
||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
)
|
||||
Trade.commit()
|
||||
|
||||
def handle_cancel_enter(
|
||||
self, trade: Trade, order: Dict, order_id: str,
|
||||
self, trade: Trade, order: Dict, order_obj: Order,
|
||||
reason: str, replacing: Optional[bool] = False
|
||||
) -> bool:
|
||||
"""
|
||||
entry cancel - cancel order
|
||||
:param order_obj: Order object from the database.
|
||||
:param replacing: Replacing order - prevent trade deletion.
|
||||
:return: True if trade was fully cancelled
|
||||
"""
|
||||
was_trade_fully_canceled = False
|
||||
order_id = order_obj.order_id
|
||||
side = trade.entry_side.capitalize()
|
||||
if not trade.has_open_orders:
|
||||
logger.warning(f"No open order for {trade}.")
|
||||
return False
|
||||
|
||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||
|
@ -1510,8 +1518,8 @@ class FreqtradeBot(LoggingMixin):
|
|||
f"Order {order_id} for {trade.pair} not cancelled, "
|
||||
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||
return False
|
||||
corder = self.exchange.cancel_order_with_result(order_id, trade.pair,
|
||||
trade.amount)
|
||||
corder = self.exchange.cancel_order_with_result(order_id, trade.pair, trade.amount)
|
||||
order_obj.ft_cancel_reason = reason
|
||||
# if replacing, retry fetching the order 3 times if the status is not what we need
|
||||
if replacing:
|
||||
retry_count = 0
|
||||
|
@ -1532,9 +1540,10 @@ class FreqtradeBot(LoggingMixin):
|
|||
else:
|
||||
# Order was cancelled already, so we can reuse the existing dict
|
||||
corder = order
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
if order_obj.ft_cancel_reason is None:
|
||||
order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
|
||||
logger.info(f'{side} order {reason} for {trade}.')
|
||||
logger.info(f'{side} order {order_obj.ft_cancel_reason} for {trade}.')
|
||||
|
||||
# Using filled to determine the filled amount
|
||||
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
||||
|
@ -1547,7 +1556,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
if open_order_count < 1 and trade.nr_of_successful_entries == 0 and not replacing:
|
||||
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
|
||||
trade.delete()
|
||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||
order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||
else:
|
||||
self.update_trade_state(trade, order_id, corder)
|
||||
logger.info(f'{side} Order timeout for {trade}.')
|
||||
|
@ -1557,21 +1566,21 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.update_trade_state(trade, order_id, corder)
|
||||
|
||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
|
||||
reason=reason)
|
||||
reason=order_obj.ft_cancel_reason)
|
||||
return was_trade_fully_canceled
|
||||
|
||||
def handle_cancel_exit(
|
||||
self, trade: Trade, order: Dict, order_id: str,
|
||||
reason: str
|
||||
self, trade: Trade, order: Dict, order_obj: Order, reason: str
|
||||
) -> bool:
|
||||
"""
|
||||
exit order cancel - cancel order and update trade
|
||||
:return: True if exit order was cancelled, false otherwise
|
||||
"""
|
||||
order_id = order_obj.order_id
|
||||
cancelled = False
|
||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
|
@ -1596,7 +1605,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
sub_trade=trade.amount != order['amount']
|
||||
)
|
||||
return False
|
||||
|
||||
order_obj.ft_cancel_reason = reason
|
||||
try:
|
||||
order = self.exchange.cancel_order_with_result(
|
||||
order['id'], trade.pair, trade.amount)
|
||||
|
@ -1615,19 +1624,22 @@ class FreqtradeBot(LoggingMixin):
|
|||
trade.exit_reason = exit_reason_prev
|
||||
cancelled = True
|
||||
else:
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
if order_obj.ft_cancel_reason is None:
|
||||
order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
trade.exit_reason = None
|
||||
|
||||
self.update_trade_state(trade, order['id'], order)
|
||||
|
||||
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||
logger.info(
|
||||
f'{trade.exit_side.capitalize()} order {order_obj.ft_cancel_reason} for {trade}.')
|
||||
trade.close_rate = None
|
||||
trade.close_rate_requested = None
|
||||
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['exit'],
|
||||
reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
|
||||
reason=order_obj.ft_cancel_reason, order_id=order['id'],
|
||||
sub_trade=trade.amount != order['amount']
|
||||
)
|
||||
return cancelled
|
||||
|
||||
|
@ -1679,15 +1691,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
:param exit_check: CheckTuple with signal and reason
|
||||
:return: True if it succeeds False
|
||||
"""
|
||||
try:
|
||||
trade.funding_fees = self.exchange.get_funding_fees(
|
||||
trade.set_funding_fees(
|
||||
self.exchange.get_funding_fees(
|
||||
pair=trade.pair,
|
||||
amount=trade.amount,
|
||||
is_short=trade.is_short,
|
||||
open_date=trade.date_last_filled_utc,
|
||||
)
|
||||
except ExchangeError:
|
||||
logger.warning("Could not update funding fee.")
|
||||
open_date=trade.date_last_filled_utc)
|
||||
)
|
||||
|
||||
exit_type = 'exit'
|
||||
exit_reason = exit_tag or exit_check.exit_reason
|
||||
|
@ -1910,7 +1920,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
if self.exchange.check_order_canceled_empty(order):
|
||||
# Trade has been cancelled on exchange
|
||||
# Handling of this will happen in check_handle_timedout.
|
||||
# Handling of this will happen in handle_cancel_order.
|
||||
return True
|
||||
|
||||
order_obj_or_none = trade.select_order_by_order_id(order_id)
|
||||
|
|
|
@ -8,15 +8,13 @@ logger = logging.getLogger(__name__)
|
|||
def set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||
"""
|
||||
Set the logging level for third party libraries
|
||||
:param verbosity: Verbosity level. amount of `-v` passed to the command line
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger("urllib3").setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
for logger_name in ('requests', 'urllib3', 'httpcore'):
|
||||
logging.getLogger(logger_name).setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if verbosity <= 2 else logging.DEBUG
|
||||
)
|
||||
|
|
0
freqtrade/optimize/analysis/__init__.py
Normal file
0
freqtrade/optimize/analysis/__init__.py
Normal file
|
@ -7,7 +7,7 @@ import pandas as pd
|
|||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.lookahead_analysis import LookaheadAnalysis
|
||||
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||
self.dict_recursive[indicator][part.startup_candle] = f"{diff:.3f}%"
|
||||
|
||||
else:
|
||||
logger.info("No difference found. Stop the process.")
|
||||
logger.info("No variance on indicator(s) found due to recursive formula.")
|
||||
break
|
||||
|
||||
# For lookahead bias check
|
||||
|
@ -100,7 +100,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||
# logger.info("part value {:.5f}".format(values_diff_other))
|
||||
|
||||
else:
|
||||
logger.info("No lookahead bias on indicators found. Stop the process.")
|
||||
logger.info("No lookahead bias on indicators found.")
|
||||
|
||||
def prepare_data(self, varholder: VarHolder, pairs_to_load: List[DataFrame]):
|
||||
|
||||
|
@ -120,6 +120,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
||||
|
||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||
self.exchange = backtesting.exchange
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
|
@ -5,7 +5,7 @@ from typing import Any, Dict, List
|
|||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.recursive_analysis import RecursiveAnalysis
|
||||
from freqtrade.optimize.analysis.recursive import RecursiveAnalysis
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
|
||||
|
@ -31,10 +31,13 @@ class RecursiveAnalysisSubFunctions:
|
|||
temp_data.append(values.get(int(candle), '-'))
|
||||
data.append(temp_data)
|
||||
|
||||
from tabulate import tabulate
|
||||
table = tabulate(data, headers=headers, tablefmt="orgtbl")
|
||||
print(table)
|
||||
return table, headers, data
|
||||
if len(data) > 0:
|
||||
from tabulate import tabulate
|
||||
table = tabulate(data, headers=headers, tablefmt="orgtbl")
|
||||
print(table)
|
||||
return table, headers, data
|
||||
|
||||
return None, None, data
|
||||
|
||||
@staticmethod
|
||||
def calculate_config_overrides(config: Config):
|
||||
|
@ -81,8 +84,7 @@ class RecursiveAnalysisSubFunctions:
|
|||
if not (strategy_list := config.get('strategy_list', [])):
|
||||
if config.get('strategy') is None:
|
||||
raise OperationalException(
|
||||
"No Strategy specified. Please specify a strategy via --strategy or "
|
||||
"--strategy-list"
|
||||
"No Strategy specified. Please specify a strategy via --strategy"
|
||||
)
|
||||
strategy_list = [config['strategy']]
|
||||
|
||||
|
@ -100,7 +102,5 @@ class RecursiveAnalysisSubFunctions:
|
|||
RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances(
|
||||
RecursiveAnalysis_instances)
|
||||
else:
|
||||
logger.error("There were no strategies specified neither through "
|
||||
"--strategy nor through "
|
||||
"--strategy-list "
|
||||
logger.error("There was no strategy specified through --strategy "
|
||||
"or timeframe was not specified.")
|
|
@ -525,10 +525,10 @@ class Backtesting:
|
|||
# This should not be reached...
|
||||
return row[OPEN_IDX]
|
||||
|
||||
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
|
||||
) -> LocalTrade:
|
||||
def _get_adjust_trade_entry_for_candle(
|
||||
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||
) -> LocalTrade:
|
||||
current_rate = row[OPEN_IDX]
|
||||
current_date = row[DATE_IDX].to_pydatetime()
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||
|
@ -536,7 +536,7 @@ class Backtesting:
|
|||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None, supress_error=True)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=current_date, current_rate=current_rate,
|
||||
current_time=current_time, current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
max_stake=min(max_stake, stake_available),
|
||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||
|
@ -569,10 +569,10 @@ class Backtesting:
|
|||
# Remaining stake is too low to be sold.
|
||||
return trade
|
||||
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
|
||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount)
|
||||
if pos_trade is not None:
|
||||
order = pos_trade.orders[-1]
|
||||
if self._try_close_open_order(order, trade, current_date, row):
|
||||
if self._try_close_open_order(order, trade, current_time, row):
|
||||
trade.recalc_trade_from_orders()
|
||||
self.wallets.update()
|
||||
return pos_trade
|
||||
|
@ -615,11 +615,11 @@ class Backtesting:
|
|||
|
||||
def _get_exit_for_signal(
|
||||
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
||||
current_time: datetime,
|
||||
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
if exit_.exit_flag:
|
||||
trade.close_date = exit_candle_time
|
||||
trade.close_date = current_time
|
||||
exit_reason = exit_.exit_reason
|
||||
amount_ = amount if amount is not None else trade.amount
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
|
@ -647,10 +647,10 @@ class Backtesting:
|
|||
default_retval=close_rate)(
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=exit_candle_time,
|
||||
current_time=current_time,
|
||||
proposed_rate=close_rate, current_profit=current_profit,
|
||||
exit_tag=exit_reason)
|
||||
if rate != close_rate:
|
||||
if rate is not None and rate != close_rate:
|
||||
close_rate = price_to_precision(rate, trade.price_precision,
|
||||
self.precision_mode)
|
||||
# We can't place orders lower than current low.
|
||||
|
@ -673,7 +673,7 @@ class Backtesting:
|
|||
time_in_force=time_in_force,
|
||||
sell_reason=exit_reason, # deprecated
|
||||
exit_reason=exit_reason,
|
||||
current_time=exit_candle_time)):
|
||||
current_time=current_time)):
|
||||
return None
|
||||
|
||||
trade.exit_reason = exit_reason
|
||||
|
@ -714,21 +714,24 @@ class Backtesting:
|
|||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
def _check_trade_exit(
|
||||
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||
) -> Optional[LocalTrade]:
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||
self.futures_data[trade.pair],
|
||||
amount=trade.amount,
|
||||
is_short=trade.is_short,
|
||||
open_date=trade.date_last_filled_utc,
|
||||
close_date=exit_candle_time,
|
||||
trade.set_funding_fees(
|
||||
self.exchange.calculate_funding_fees(
|
||||
self.futures_data[trade.pair],
|
||||
amount=trade.amount,
|
||||
is_short=trade.is_short,
|
||||
open_date=trade.date_last_filled_utc,
|
||||
close_date=current_time
|
||||
)
|
||||
)
|
||||
|
||||
# Check if we need to adjust our current positions
|
||||
if self.strategy.position_adjustment_enable:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
|
||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||
|
@ -738,7 +741,7 @@ class Backtesting:
|
|||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||
)
|
||||
for exit_ in exits:
|
||||
t = self._get_exit_for_signal(trade, row, exit_)
|
||||
t = self._get_exit_for_signal(trade, row, exit_, current_time)
|
||||
if t:
|
||||
return t
|
||||
return None
|
||||
|
@ -760,7 +763,7 @@ class Backtesting:
|
|||
) # default value is the open rate
|
||||
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
|
||||
# which freqtrade does not support in live.
|
||||
if new_rate != propose_rate:
|
||||
if new_rate is not None and new_rate != propose_rate:
|
||||
propose_rate = price_to_precision(new_rate, price_precision,
|
||||
self.precision_mode)
|
||||
if direction == "short":
|
||||
|
@ -1145,7 +1148,7 @@ class Backtesting:
|
|||
|
||||
# 4. Create exit orders (if any)
|
||||
if not trade.has_open_orders:
|
||||
self._check_trade_exit(trade, row) # Place exit order if necessary
|
||||
self._check_trade_exit(trade, row, current_time) # Place exit order if necessary
|
||||
|
||||
# 5. Process exit orders.
|
||||
order = trade.select_order(trade.exit_side, is_open=True)
|
||||
|
|
|
@ -115,6 +115,7 @@ def migrate_trades_and_orders_table(
|
|||
# Futures Properties
|
||||
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
||||
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
|
||||
funding_fee_running = get_column_def(cols, 'funding_fee_running', 'null')
|
||||
max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount')
|
||||
|
||||
# If ticker-interval existed use that, else null.
|
||||
|
@ -163,7 +164,7 @@ def migrate_trades_and_orders_table(
|
|||
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
|
||||
timeframe, open_trade_value, close_profit_abs,
|
||||
trading_mode, leverage, liquidation_price, is_short,
|
||||
interest_rate, funding_fees, realized_profit,
|
||||
interest_rate, funding_fees, funding_fee_running, realized_profit,
|
||||
amount_precision, price_precision, precision_mode, contract_size,
|
||||
max_stake_amount
|
||||
)
|
||||
|
@ -192,7 +193,8 @@ def migrate_trades_and_orders_table(
|
|||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
|
||||
{is_short} is_short, {interest_rate} interest_rate,
|
||||
{funding_fees} funding_fees, {realized_profit} realized_profit,
|
||||
{funding_fees} funding_fees, {funding_fee_running} funding_fee_running,
|
||||
{realized_profit} realized_profit,
|
||||
{amount_precision} amount_precision, {price_precision} price_precision,
|
||||
{precision_mode} precision_mode, {contract_size} contract_size,
|
||||
{max_stake_amount} max_stake_amount
|
||||
|
@ -220,6 +222,7 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
|||
funding_fee = get_column_def(cols_order, 'funding_fee', '0.0')
|
||||
ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
|
||||
ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
|
||||
ft_cancel_reason = get_column_def(cols_order, 'ft_cancel_reason', 'null')
|
||||
|
||||
# sqlite does not support literals for booleans
|
||||
with engine.begin() as connection:
|
||||
|
@ -227,13 +230,13 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
|||
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
||||
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee,
|
||||
ft_amount, ft_price
|
||||
ft_amount, ft_price, ft_cancel_reason
|
||||
)
|
||||
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
||||
cost, {stop_price} stop_price, order_date, order_filled_date,
|
||||
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee,
|
||||
{ft_amount} ft_amount, {ft_price} ft_price
|
||||
{ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason
|
||||
from {table_back_name}
|
||||
"""))
|
||||
|
||||
|
@ -328,8 +331,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'funding_fee')):
|
||||
migrating = False
|
||||
# if not has_column(cols_orders, 'ft_price'):
|
||||
if not has_column(cols_trades, 'is_stop_loss_trailing'):
|
||||
# if not has_column(cols_orders, 'ft_cancel_reason'):
|
||||
if not has_column(cols_trades, 'funding_fee_running'):
|
||||
migrating = True
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
|
|
|
@ -68,6 +68,7 @@ class Order(ModelBase):
|
|||
ft_is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
|
||||
ft_amount: Mapped[float] = mapped_column(Float(), nullable=False)
|
||||
ft_price: Mapped[float] = mapped_column(Float(), nullable=False)
|
||||
ft_cancel_reason: Mapped[str] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True)
|
||||
|
||||
order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
|
@ -173,10 +174,6 @@ class Order(ModelBase):
|
|||
self.ft_is_open = True
|
||||
if self.status in NON_OPEN_EXCHANGE_STATES:
|
||||
self.ft_is_open = False
|
||||
if self.trade:
|
||||
# Assign funding fee up to this point
|
||||
# (represents the funding fee since the last order)
|
||||
self.funding_fee = self.trade.funding_fees
|
||||
if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date:
|
||||
self.order_filled_date = dt_from_ts(
|
||||
safe_value_fallback(order, 'lastTradeTimestamp', default_value=dt_ts())
|
||||
|
@ -237,6 +234,7 @@ class Order(ModelBase):
|
|||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
'ft_fee_base': self.ft_fee_base,
|
||||
'funding_fee': self.funding_fee,
|
||||
})
|
||||
return resp
|
||||
|
||||
|
@ -248,7 +246,8 @@ class Order(ModelBase):
|
|||
self.ft_is_open = False
|
||||
# Assign funding fees to Order.
|
||||
# Assumes backtesting will use date_last_filled_utc to calculate future funding fees.
|
||||
self.funding_fee = trade.funding_fees
|
||||
self.funding_fee = trade.funding_fee_running
|
||||
trade.funding_fee_running = 0.0
|
||||
|
||||
if (self.ft_order_side == trade.entry_side and self.price):
|
||||
trade.open_rate = self.price
|
||||
|
@ -395,6 +394,9 @@ class LocalTrade:
|
|||
|
||||
# Futures properties
|
||||
funding_fees: Optional[float] = None
|
||||
# Used to keep running funding fees - between the last filled order and now
|
||||
# Shall not be used for calculations!
|
||||
funding_fee_running: Optional[float] = None
|
||||
|
||||
@property
|
||||
def stoploss_or_liquidation(self) -> float:
|
||||
|
@ -534,6 +536,7 @@ class LocalTrade:
|
|||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
self.recalc_open_trade_value()
|
||||
self.orders = []
|
||||
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading requires param interest_rate on trades")
|
||||
|
@ -660,6 +663,16 @@ class LocalTrade:
|
|||
return
|
||||
self.liquidation_price = liquidation_price
|
||||
|
||||
def set_funding_fees(self, funding_fee: float) -> None:
|
||||
"""
|
||||
Assign funding fees to Trade.
|
||||
"""
|
||||
if funding_fee is None:
|
||||
return
|
||||
self.funding_fee_running = funding_fee
|
||||
prior_funding_fees = sum([o.funding_fee for o in self.orders if o.funding_fee])
|
||||
self.funding_fees = prior_funding_fees + funding_fee
|
||||
|
||||
def __set_stop_loss(self, stop_loss: float, percent: float):
|
||||
"""
|
||||
Method used internally to set self.stop_loss.
|
||||
|
@ -740,6 +753,10 @@ class LocalTrade:
|
|||
return
|
||||
|
||||
logger.info(f'Updating trade (id={self.id}) ...')
|
||||
if order.ft_order_side != 'stoploss':
|
||||
order.funding_fee = self.funding_fee_running
|
||||
# Reset running funding fees
|
||||
self.funding_fee_running = 0.0
|
||||
|
||||
if order.ft_order_side == self.entry_side:
|
||||
# Update open rate and actual amount
|
||||
|
@ -1285,6 +1302,99 @@ class LocalTrade:
|
|||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Self:
|
||||
"""
|
||||
Create a Trade instance from a json string.
|
||||
|
||||
Used for debugging purposes - please keep.
|
||||
:param json_str: json string to parse
|
||||
:return: Trade instance
|
||||
"""
|
||||
import rapidjson
|
||||
data = rapidjson.loads(json_str)
|
||||
trade = cls(
|
||||
__FROM_JSON=True,
|
||||
id=data["trade_id"],
|
||||
pair=data["pair"],
|
||||
base_currency=data["base_currency"],
|
||||
stake_currency=data["quote_currency"],
|
||||
is_open=data["is_open"],
|
||||
exchange=data["exchange"],
|
||||
amount=data["amount"],
|
||||
amount_requested=data["amount_requested"],
|
||||
stake_amount=data["stake_amount"],
|
||||
strategy=data["strategy"],
|
||||
enter_tag=data["enter_tag"],
|
||||
timeframe=data["timeframe"],
|
||||
fee_open=data["fee_open"],
|
||||
fee_open_cost=data["fee_open_cost"],
|
||||
fee_open_currency=data["fee_open_currency"],
|
||||
fee_close=data["fee_close"],
|
||||
fee_close_cost=data["fee_close_cost"],
|
||||
fee_close_currency=data["fee_close_currency"],
|
||||
open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
|
||||
open_rate=data["open_rate"],
|
||||
open_rate_requested=data["open_rate_requested"],
|
||||
open_trade_value=data["open_trade_value"],
|
||||
close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
|
||||
if data["close_timestamp"] else None),
|
||||
realized_profit=data["realized_profit"],
|
||||
close_rate=data["close_rate"],
|
||||
close_rate_requested=data["close_rate_requested"],
|
||||
close_profit=data["close_profit"],
|
||||
close_profit_abs=data["close_profit_abs"],
|
||||
exit_reason=data["exit_reason"],
|
||||
exit_order_status=data["exit_order_status"],
|
||||
stop_loss=data["stop_loss_abs"],
|
||||
stop_loss_pct=data["stop_loss_ratio"],
|
||||
stoploss_order_id=data["stoploss_order_id"],
|
||||
stoploss_last_update=(
|
||||
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
|
||||
tz=timezone.utc)
|
||||
if data["stoploss_last_update_timestamp"] else None),
|
||||
initial_stop_loss=data["initial_stop_loss_abs"],
|
||||
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
|
||||
min_rate=data["min_rate"],
|
||||
max_rate=data["max_rate"],
|
||||
leverage=data["leverage"],
|
||||
interest_rate=data["interest_rate"],
|
||||
liquidation_price=data["liquidation_price"],
|
||||
is_short=data["is_short"],
|
||||
trading_mode=data["trading_mode"],
|
||||
funding_fees=data["funding_fees"],
|
||||
amount_precision=data.get('amount_precision', None),
|
||||
price_precision=data.get('price_precision', None),
|
||||
precision_mode=data.get('precision_mode', None),
|
||||
contract_size=data.get('contract_size', None),
|
||||
)
|
||||
for order in data["orders"]:
|
||||
|
||||
order_obj = Order(
|
||||
amount=order["amount"],
|
||||
ft_amount=order["amount"],
|
||||
ft_order_side=order["ft_order_side"],
|
||||
ft_pair=order["pair"],
|
||||
ft_is_open=order["is_open"],
|
||||
order_id=order["order_id"],
|
||||
status=order["status"],
|
||||
average=order["average"],
|
||||
cost=order["cost"],
|
||||
filled=order["filled"],
|
||||
order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
|
||||
order_filled_date=(datetime.fromtimestamp(
|
||||
order["order_filled_timestamp"] // 1000, tz=timezone.utc)
|
||||
if order["order_filled_timestamp"] else None),
|
||||
order_type=order["order_type"],
|
||||
price=order["price"],
|
||||
ft_price=order["price"],
|
||||
remaining=order["remaining"],
|
||||
funding_fee=order.get("funding_fee", None),
|
||||
)
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
return trade
|
||||
|
||||
|
||||
class Trade(ModelBase, LocalTrade):
|
||||
"""
|
||||
|
@ -1388,6 +1498,8 @@ class Trade(ModelBase, LocalTrade):
|
|||
# Futures properties
|
||||
funding_fees: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True, default=None) # type: ignore
|
||||
funding_fee_running: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True, default=None) # type: ignore
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from_json = kwargs.pop('__FROM_JSON', None)
|
||||
|
@ -1728,95 +1840,3 @@ class Trade(ModelBase, LocalTrade):
|
|||
Order.status == 'closed'
|
||||
)).scalar_one()
|
||||
return trading_volume
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Self:
|
||||
"""
|
||||
Create a Trade instance from a json string.
|
||||
|
||||
Used for debugging purposes - please keep.
|
||||
:param json_str: json string to parse
|
||||
:return: Trade instance
|
||||
"""
|
||||
import rapidjson
|
||||
data = rapidjson.loads(json_str)
|
||||
trade = cls(
|
||||
__FROM_JSON=True,
|
||||
id=data["trade_id"],
|
||||
pair=data["pair"],
|
||||
base_currency=data["base_currency"],
|
||||
stake_currency=data["quote_currency"],
|
||||
is_open=data["is_open"],
|
||||
exchange=data["exchange"],
|
||||
amount=data["amount"],
|
||||
amount_requested=data["amount_requested"],
|
||||
stake_amount=data["stake_amount"],
|
||||
strategy=data["strategy"],
|
||||
enter_tag=data["enter_tag"],
|
||||
timeframe=data["timeframe"],
|
||||
fee_open=data["fee_open"],
|
||||
fee_open_cost=data["fee_open_cost"],
|
||||
fee_open_currency=data["fee_open_currency"],
|
||||
fee_close=data["fee_close"],
|
||||
fee_close_cost=data["fee_close_cost"],
|
||||
fee_close_currency=data["fee_close_currency"],
|
||||
open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
|
||||
open_rate=data["open_rate"],
|
||||
open_rate_requested=data["open_rate_requested"],
|
||||
open_trade_value=data["open_trade_value"],
|
||||
close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
|
||||
if data["close_timestamp"] else None),
|
||||
realized_profit=data["realized_profit"],
|
||||
close_rate=data["close_rate"],
|
||||
close_rate_requested=data["close_rate_requested"],
|
||||
close_profit=data["close_profit"],
|
||||
close_profit_abs=data["close_profit_abs"],
|
||||
exit_reason=data["exit_reason"],
|
||||
exit_order_status=data["exit_order_status"],
|
||||
stop_loss=data["stop_loss_abs"],
|
||||
stop_loss_pct=data["stop_loss_ratio"],
|
||||
stoploss_order_id=data["stoploss_order_id"],
|
||||
stoploss_last_update=(
|
||||
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
|
||||
tz=timezone.utc)
|
||||
if data["stoploss_last_update_timestamp"] else None),
|
||||
initial_stop_loss=data["initial_stop_loss_abs"],
|
||||
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
|
||||
min_rate=data["min_rate"],
|
||||
max_rate=data["max_rate"],
|
||||
leverage=data["leverage"],
|
||||
interest_rate=data["interest_rate"],
|
||||
liquidation_price=data["liquidation_price"],
|
||||
is_short=data["is_short"],
|
||||
trading_mode=data["trading_mode"],
|
||||
funding_fees=data["funding_fees"],
|
||||
amount_precision=data.get('amount_precision', None),
|
||||
price_precision=data.get('price_precision', None),
|
||||
precision_mode=data.get('precision_mode', None),
|
||||
contract_size=data.get('contract_size', None),
|
||||
)
|
||||
for order in data["orders"]:
|
||||
|
||||
order_obj = Order(
|
||||
amount=order["amount"],
|
||||
ft_amount=order["amount"],
|
||||
ft_order_side=order["ft_order_side"],
|
||||
ft_pair=order["pair"],
|
||||
ft_is_open=order["is_open"],
|
||||
order_id=order["order_id"],
|
||||
status=order["status"],
|
||||
average=order["average"],
|
||||
cost=order["cost"],
|
||||
filled=order["filled"],
|
||||
order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
|
||||
order_filled_date=(datetime.fromtimestamp(
|
||||
order["order_filled_timestamp"] // 1000, tz=timezone.utc)
|
||||
if order["order_filled_timestamp"] else None),
|
||||
order_type=order["order_type"],
|
||||
price=order["price"],
|
||||
ft_price=order["price"],
|
||||
remaining=order["remaining"],
|
||||
)
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
return trade
|
||||
|
|
|
@ -27,6 +27,7 @@ coingecko_mapping = {
|
|||
'usdt': 'tether',
|
||||
'busd': 'binance-usd',
|
||||
'tusd': 'true-usd',
|
||||
'usdc': 'usd-coin',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -795,14 +795,14 @@ class RPC:
|
|||
|
||||
if order['side'] == trade.entry_side:
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT'])
|
||||
trade, order, oo, CANCEL_REASON['FORCE_EXIT'])
|
||||
trade_entry_cancelation_res['cancel_state'] = fully_canceled
|
||||
trade_entry_cancelation_registry.append(trade_entry_cancelation_res)
|
||||
|
||||
if order['side'] == trade.exit_side:
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_exit(
|
||||
trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT'])
|
||||
trade, order, oo, CANCEL_REASON['FORCE_EXIT'])
|
||||
|
||||
if all(tocr['cancel_state'] is False for tocr in trade_entry_cancelation_registry):
|
||||
if trade.has_open_orders:
|
||||
|
@ -955,7 +955,7 @@ class RPC:
|
|||
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
|
||||
raise RPCException("Order not found.")
|
||||
self._freqtrade.handle_cancel_order(
|
||||
order, open_order.order_id, trade, CANCEL_REASON['USER_CANCEL'])
|
||||
order, open_order, trade, CANCEL_REASON['USER_CANCEL'])
|
||||
Trade.commit()
|
||||
|
||||
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
|
||||
|
|
|
@ -1244,10 +1244,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
and trade.liquidation_price <= (high or current_rate)
|
||||
and trade.is_short)
|
||||
|
||||
if (liq_higher_long or liq_lower_short):
|
||||
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
|
||||
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
|
||||
|
||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||
# regular stoploss handling.
|
||||
|
@ -1268,6 +1264,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
|
||||
return ExitCheckTuple(exit_type=exit_type)
|
||||
|
||||
if (liq_higher_long or liq_lower_short):
|
||||
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
|
||||
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
|
||||
|
||||
return ExitCheckTuple(exit_type=ExitType.NONE)
|
||||
|
||||
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
|
||||
|
|
2
freqtrade/vendor/qtpylib/indicators.py
vendored
2
freqtrade/vendor/qtpylib/indicators.py
vendored
|
@ -226,7 +226,7 @@ def crossed(series1, series2, direction=None):
|
|||
series1.shift(1) >= series2.shift(1)))
|
||||
|
||||
if direction is None:
|
||||
return above or below
|
||||
return above | below
|
||||
|
||||
return above if direction == "above" else below
|
||||
|
||||
|
|
|
@ -82,6 +82,11 @@ extend-select = [
|
|||
# "TCH", # flake8-type-checking
|
||||
"PTH", # flake8-use-pathlib
|
||||
]
|
||||
extend-ignore = [
|
||||
"E241", # Multiple spaces after comma
|
||||
"E272", # Multiple spaces before keyword
|
||||
"E221", # Multiple spaces before operator
|
||||
]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 12
|
||||
|
|
|
@ -7,24 +7,24 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.291
|
||||
mypy==1.5.1
|
||||
pre-commit==3.4.0
|
||||
ruff==0.1.1
|
||||
mypy==1.6.1
|
||||
pre-commit==3.5.0
|
||||
pytest==7.4.2
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-mock==3.11.1
|
||||
pytest-mock==3.12.0
|
||||
pytest-random-order==1.1.0
|
||||
isort==5.12.0
|
||||
# For datetime mocking
|
||||
time-machine==2.13.0
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.8.0
|
||||
nbconvert==7.9.2
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.3.0.6
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.31.0.4
|
||||
types-requests==2.31.0.10
|
||||
types-tabulate==0.9.0.3
|
||||
types-python-dateutil==2.8.19.14
|
||||
|
|
|
@ -8,5 +8,5 @@ joblib==1.3.2
|
|||
catboost==1.2.2; 'arm' not in platform_machine
|
||||
lightgbm==4.1.0
|
||||
xgboost==2.0.0
|
||||
tensorboard==2.14.0
|
||||
tensorboard==2.15.0
|
||||
datasieve==0.1.7
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.11.2
|
||||
scipy==1.11.3
|
||||
scikit-learn==1.1.3
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.12.4
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
numpy==1.26.0; platform_machine != 'armv7l'
|
||||
numpy==1.25.2; platform_machine == 'armv7l'
|
||||
numpy==1.26.1; platform_machine != 'armv7l'
|
||||
pandas==2.0.3
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==4.0.105
|
||||
cryptography==41.0.3
|
||||
aiohttp==3.8.5
|
||||
SQLAlchemy==2.0.21
|
||||
python-telegram-bot==20.5
|
||||
ccxt==4.1.22
|
||||
cryptography==41.0.4
|
||||
aiohttp==3.8.6
|
||||
SQLAlchemy==2.0.22
|
||||
python-telegram-bot==20.6
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
arrow==1.2.3
|
||||
arrow==1.3.0
|
||||
cachetools==5.3.1
|
||||
requests==2.31.0
|
||||
urllib3==2.0.5
|
||||
urllib3==2.0.7
|
||||
jsonschema==4.19.1
|
||||
TA-Lib==0.4.28
|
||||
technical==1.4.0
|
||||
|
@ -23,27 +23,27 @@ jinja2==3.1.2
|
|||
tables==3.8.0
|
||||
blosc==1.11.1
|
||||
joblib==1.3.2
|
||||
rich==13.5.3
|
||||
rich==13.6.0
|
||||
pyarrow==13.0.0; platform_machine != 'armv7l'
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.5
|
||||
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.11
|
||||
python-rapidjson==1.12
|
||||
# Properly format api responses
|
||||
orjson==3.9.7
|
||||
orjson==3.9.9
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.103.1
|
||||
pydantic==2.3.0
|
||||
fastapi==0.104.0
|
||||
pydantic==2.4.2
|
||||
uvicorn==0.23.2
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.2.1
|
||||
psutil==5.9.5
|
||||
psutil==5.9.6
|
||||
|
||||
# Support for colorized terminal output
|
||||
colorama==0.4.6
|
||||
|
@ -54,11 +54,11 @@ prompt-toolkit==3.0.36
|
|||
python-dateutil==2.8.2
|
||||
|
||||
#Futures
|
||||
schedule==1.2.0
|
||||
schedule==1.2.1
|
||||
|
||||
#WS Messages
|
||||
websockets==11.0.3
|
||||
websockets==12.0
|
||||
janus==1.0.0
|
||||
|
||||
ast-comments==1.1.0
|
||||
packaging==23.1
|
||||
ast-comments==1.1.2
|
||||
packaging==23.2
|
||||
|
|
52
tests/data/test_trade_converter_kraken.py
Normal file
52
tests/data/test_trade_converter_kraken.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copytree
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from tests.conftest import EXMS, log_has, log_has_re, patch_exchange
|
||||
|
||||
|
||||
def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf_usdt, mocker):
|
||||
with pytest.raises(OperationalException, match="This function is only for the kraken exchange"):
|
||||
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
||||
|
||||
default_conf_usdt['exchange']['name'] = 'kraken'
|
||||
|
||||
patch_exchange(mocker, id='kraken')
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={
|
||||
'BCH/EUR': {'symbol': 'BCH/EUR', 'id': 'BCHEUR', 'altname': 'BCHEUR'},
|
||||
}))
|
||||
tmpdir1 = Path(tmpdir)
|
||||
dstfile = tmpdir1 / 'BCH_EUR-trades.feather'
|
||||
assert not dstfile.is_file()
|
||||
default_conf_usdt['datadir'] = tmpdir1
|
||||
# There's 2 files in this tree, containing a total of 2 days.
|
||||
# tests/testdata/kraken/
|
||||
# └── trades_csv
|
||||
# ├── BCHEUR.csv <-- 2023-01-01
|
||||
# └── incremental_q2
|
||||
# └── BCHEUR.csv <-- 2023-01-02
|
||||
|
||||
copytree(testdatadir / 'kraken/trades_csv', tmpdir1 / 'trades_csv')
|
||||
|
||||
import_kraken_trades_from_csv(default_conf_usdt, 'feather')
|
||||
assert log_has("Found csv files for BCHEUR.", caplog)
|
||||
assert log_has_re(r"BCH/EUR: 340 trades.* 2023-01-01.* 2023-01-02.*", caplog)
|
||||
|
||||
assert dstfile.is_file()
|
||||
|
||||
dh = get_datahandler(tmpdir1, 'feather')
|
||||
trades = dh.trades_load('BCH_EUR')
|
||||
assert len(trades) == 340
|
||||
|
||||
assert trades['date'].min().to_pydatetime() == datetime(2023, 1, 1, 0, 3, 56,
|
||||
tzinfo=timezone.utc)
|
||||
assert trades['date'].max().to_pydatetime() == datetime(2023, 1, 2, 23, 17, 3,
|
||||
tzinfo=timezone.utc)
|
||||
# ID is not filled
|
||||
assert len(trades.loc[trades['id'] != '']) == 0
|
|
@ -3737,6 +3737,18 @@ def test_calculate_backoff(retrycount, max_retries, expected):
|
|||
assert calculate_backoff(retrycount, max_retries) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_funding_fees(default_conf_usdt, mocker, exchange_name, caplog):
|
||||
now = datetime.now(timezone.utc)
|
||||
default_conf_usdt['trading_mode'] = 'futures'
|
||||
default_conf_usdt['margin_mode'] = 'isolated'
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name)
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock(side_effect=ExchangeError)
|
||||
assert exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now) == 0.0
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
assert log_has("Could not update funding fees for BTC/USDT:USDT.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", ['binance'])
|
||||
def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
|
@ -4075,7 +4087,10 @@ def test_combine_funding_and_mark(
|
|||
('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||
('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.00066479999),
|
||||
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.00091409999),
|
||||
('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||
# :01 must be rounded down.
|
||||
('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.00091409999),
|
||||
('binance', 0, 2, "2021-08-31 23:58:00", "2021-09-01 08:00:00", 30.0, -0.00091409999),
|
||||
('binance', 0, 2, "2021-09-01 00:10:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
|
||||
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937),
|
||||
# ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289),
|
||||
|
@ -4191,7 +4206,7 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
|
|||
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
||||
mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['4h', '8h']))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
|
||||
d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||
d1 = datetime.strptime("2021-08-31 23:00:01 +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||
|
||||
time_machine.move_to("2021-09-01 08:00:00 +00:00")
|
||||
funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, True, d1)
|
||||
|
|
|
@ -179,10 +179,9 @@ def test_set_initial_return_values(mocker, freqai_conf):
|
|||
hist_pred_df = freqai.dd.historic_predictions[pair]
|
||||
model_return_df = freqai.dd.model_return_values[pair]
|
||||
|
||||
assert (hist_pred_df['date_pred'].iloc[-1] ==
|
||||
pd.Timestamp(end_x_plus_5) - pd.Timedelta(days=1))
|
||||
assert hist_pred_df['date_pred'].iloc[-1] == pd.Timestamp(end_x_plus_5)
|
||||
assert 'date_pred' in hist_pred_df.columns
|
||||
assert hist_pred_df.shape[0] == 7 # Total rows: 5 from historic and 2 new zeros
|
||||
assert hist_pred_df.shape[0] == 8
|
||||
|
||||
# compare values in model_return_df with hist_pred_df
|
||||
assert (model_return_df["value"].values ==
|
||||
|
@ -234,9 +233,9 @@ def test_set_initial_return_values_warning(mocker, freqai_conf):
|
|||
hist_pred_df = freqai.dd.historic_predictions[pair]
|
||||
model_return_df = freqai.dd.model_return_values[pair]
|
||||
|
||||
assert hist_pred_df['date_pred'].iloc[-1] == pd.Timestamp(end_x_plus_5) - pd.Timedelta(days=1)
|
||||
assert hist_pred_df['date_pred'].iloc[-1] == pd.Timestamp(end_x_plus_5)
|
||||
assert 'date_pred' in hist_pred_df.columns
|
||||
assert hist_pred_df.shape[0] == 9 # Total rows: 5 from historic and 4 new zeros
|
||||
assert hist_pred_df.shape[0] == 10
|
||||
|
||||
# compare values in model_return_df with hist_pred_df
|
||||
assert (model_return_df["value"].values == hist_pred_df.tail(
|
||||
|
|
|
@ -665,7 +665,7 @@ def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
|
|||
]
|
||||
|
||||
# No data available.
|
||||
res = backtesting._check_trade_exit(trade, row_sell)
|
||||
res = backtesting._check_trade_exit(trade, row_sell, row_sell[0].to_pydatetime())
|
||||
assert res is not None
|
||||
assert res.exit_reason == ExitType.ROI.value
|
||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||
|
@ -678,7 +678,7 @@ def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
|
|||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||
|
||||
res = backtesting._check_trade_exit(trade, row)
|
||||
res = backtesting._check_trade_exit(trade, row, row[0].to_pydatetime())
|
||||
assert res is None
|
||||
|
||||
|
||||
|
@ -1006,7 +1006,7 @@ def test_backtest_one_detail_futures_funding_fees(
|
|||
assert t.nr_of_successful_entries >= 6
|
||||
# Funding fees will vary depending on the number of adjustment orders
|
||||
# That number is a lot higher with detail data.
|
||||
assert -20 < t.funding_fees < -0.1
|
||||
assert -1.81 < t.funding_fees < -0.1
|
||||
|
||||
|
||||
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
||||
|
|
|
@ -133,6 +133,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
]
|
||||
backtesting.strategy.leverage = MagicMock(return_value=leverage)
|
||||
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
||||
current_time = row[0].to_pydatetime()
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 100.0
|
||||
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
||||
|
@ -140,7 +141,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
assert pytest.approx(trade.liquidation_price) == (0.10278333 if leverage == 1 else 1.2122249)
|
||||
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 100.0
|
||||
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
||||
|
@ -148,7 +149,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
# Increase position by 100
|
||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=100)
|
||||
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 200.0
|
||||
|
@ -159,7 +160,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
# Reduce by more than amount - no change to trade.
|
||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-500)
|
||||
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 200.0
|
||||
|
@ -170,7 +171,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
|
||||
# Reduce position by 50
|
||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 100.0
|
||||
|
@ -182,7 +183,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||
|
||||
# Adjust below minimum
|
||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-99)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
|
||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||
|
||||
assert trade
|
||||
assert pytest.approx(trade.stake_amount) == 100.0
|
||||
|
|
|
@ -8,8 +8,8 @@ import pytest
|
|||
from freqtrade.commands.optimize_commands import start_lookahead_analysis
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.lookahead_analysis import Analysis, LookaheadAnalysis
|
||||
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
||||
from freqtrade.optimize.analysis.lookahead import Analysis, LookaheadAnalysis
|
||||
from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFunctions
|
||||
from tests.conftest import EXMS, get_args, log_has_re, patch_exchange
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ def test_start_lookahead_analysis(mocker):
|
|||
single_mock = MagicMock()
|
||||
text_table_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.lookahead_analysis_helpers.LookaheadAnalysisSubFunctions',
|
||||
'freqtrade.optimize.analysis.lookahead_helpers.LookaheadAnalysisSubFunctions',
|
||||
initialize_single_lookahead_analysis=single_mock,
|
||||
text_table_lookahead_analysis_instances=text_table_mock,
|
||||
)
|
||||
|
@ -117,7 +117,7 @@ def test_lookahead_helper_start(lookahead_conf, mocker) -> None:
|
|||
single_mock = MagicMock()
|
||||
text_table_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.lookahead_analysis_helpers.LookaheadAnalysisSubFunctions',
|
||||
'freqtrade.optimize.analysis.lookahead_helpers.LookaheadAnalysisSubFunctions',
|
||||
initialize_single_lookahead_analysis=single_mock,
|
||||
text_table_lookahead_analysis_instances=text_table_mock,
|
||||
)
|
||||
|
@ -324,7 +324,7 @@ def test_initialize_single_lookahead_analysis(lookahead_conf, mocker, caplog):
|
|||
|
||||
lookahead_conf['timeframe'] = '5m'
|
||||
lookahead_conf['timerange'] = '20180119-20180122'
|
||||
start_mock = mocker.patch('freqtrade.optimize.lookahead_analysis.LookaheadAnalysis.start')
|
||||
start_mock = mocker.patch('freqtrade.optimize.analysis.lookahead.LookaheadAnalysis.start')
|
||||
strategy_obj = {
|
||||
'name': "strategy_test_v3_with_lookahead_bias",
|
||||
'location': Path(lookahead_conf['strategy_path'], f"{lookahead_conf['strategy']}.py")
|
||||
|
|
|
@ -8,8 +8,8 @@ import pytest
|
|||
from freqtrade.commands.optimize_commands import start_recursive_analysis
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.recursive_analysis import RecursiveAnalysis
|
||||
from freqtrade.optimize.recursive_analysis_helpers import RecursiveAnalysisSubFunctions
|
||||
from freqtrade.optimize.analysis.recursive import RecursiveAnalysis
|
||||
from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions
|
||||
from tests.conftest import get_args, log_has_re, patch_exchange
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ def test_start_recursive_analysis(mocker):
|
|||
single_mock = MagicMock()
|
||||
text_table_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.recursive_analysis_helpers.RecursiveAnalysisSubFunctions',
|
||||
'freqtrade.optimize.analysis.recursive_helpers.RecursiveAnalysisSubFunctions',
|
||||
initialize_single_recursive_analysis=single_mock,
|
||||
text_table_recursive_analysis_instances=text_table_mock,
|
||||
)
|
||||
|
@ -83,7 +83,7 @@ def test_recursive_helper_start(recursive_conf, mocker) -> None:
|
|||
single_mock = MagicMock()
|
||||
text_table_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.recursive_analysis_helpers.RecursiveAnalysisSubFunctions',
|
||||
'freqtrade.optimize.analysis.recursive_helpers.RecursiveAnalysisSubFunctions',
|
||||
initialize_single_recursive_analysis=single_mock,
|
||||
text_table_recursive_analysis_instances=text_table_mock,
|
||||
)
|
||||
|
@ -133,7 +133,7 @@ def test_initialize_single_recursive_analysis(recursive_conf, mocker, caplog):
|
|||
|
||||
recursive_conf['timeframe'] = '5m'
|
||||
recursive_conf['timerange'] = '20180119-20180122'
|
||||
start_mock = mocker.patch('freqtrade.optimize.recursive_analysis.RecursiveAnalysis.start')
|
||||
start_mock = mocker.patch('freqtrade.optimize.analysis.recursive.RecursiveAnalysis.start')
|
||||
strategy_obj = {
|
||||
'name': "strategy_test_v3_recursive_issue",
|
||||
'location': Path(recursive_conf['strategy_path'], f"{recursive_conf['strategy']}.py")
|
||||
|
|
|
@ -583,7 +583,7 @@ def test_calc_open_close_trade_price(
|
|||
oobj.update_from_ccxt_object(entry_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.funding_fees = funding_fees
|
||||
trade.funding_fee_running = funding_fees
|
||||
|
||||
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side)
|
||||
oobj._trade_live = trade
|
||||
|
@ -591,7 +591,9 @@ def test_calc_open_close_trade_price(
|
|||
trade.update_trade(oobj)
|
||||
|
||||
assert trade.is_open is False
|
||||
# Funding fees transfer from funding_fee_running to funding_Fees
|
||||
assert trade.funding_fees == funding_fees
|
||||
assert trade.orders[-1].funding_fee == funding_fees
|
||||
|
||||
assert pytest.approx(trade._calc_open_trade_value(trade.amount, trade.open_rate)) == open_value
|
||||
assert pytest.approx(trade.calc_close_trade_value(trade.close_rate)) == close_value
|
||||
|
@ -2094,11 +2096,10 @@ def test_Trade_object_idem():
|
|||
'get_enter_tag_performance',
|
||||
'get_mix_tag_performance',
|
||||
'get_trading_volume',
|
||||
'from_json',
|
||||
'validate_string_len',
|
||||
)
|
||||
EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count',
|
||||
'total_profit')
|
||||
'total_profit', 'from_json',)
|
||||
|
||||
# Parent (LocalTrade) should have the same attributes
|
||||
for item in trade:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.persistence.trade_model import Trade
|
||||
from freqtrade.persistence.trade_model import LocalTrade, Trade
|
||||
from tests.conftest import create_mock_trades_usdt
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
|
@ -170,7 +172,8 @@ def test_trade_fromjson():
|
|||
"order_filled_date": "2022-10-18 09:45:22",
|
||||
"order_type": "market",
|
||||
"price": 0.2592,
|
||||
"remaining": 0.0
|
||||
"remaining": 0.0,
|
||||
"funding_fee": -0.055
|
||||
}
|
||||
]
|
||||
}"""
|
||||
|
@ -192,3 +195,72 @@ def test_trade_fromjson():
|
|||
last_o = trade.orders[-1]
|
||||
assert last_o.order_filled_utc == datetime(2022, 10, 18, 9, 45, 22, tzinfo=timezone.utc)
|
||||
assert isinstance(last_o.order_date, datetime)
|
||||
assert last_o.funding_fee == -0.055
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_trade_serialize_load_back(fee):
|
||||
|
||||
create_mock_trades_usdt(fee, None)
|
||||
|
||||
t = Trade.get_trades([Trade.id == 1]).first()
|
||||
assert t.id == 1
|
||||
t.funding_fees = 0.025
|
||||
t.orders[0].funding_fee = 0.0125
|
||||
assert len(t.orders) == 2
|
||||
Trade.commit()
|
||||
|
||||
tjson = t.to_json(False)
|
||||
assert isinstance(tjson, dict)
|
||||
trade_string = json.dumps(tjson)
|
||||
trade = Trade.from_json(trade_string)
|
||||
|
||||
assert trade.id == t.id
|
||||
assert trade.funding_fees == t.funding_fees
|
||||
assert len(trade.orders) == len(t.orders)
|
||||
assert trade.orders[0].funding_fee == t.orders[0].funding_fee
|
||||
excluded = [
|
||||
'trade_id', 'quote_currency', 'open_timestamp', 'close_timestamp',
|
||||
'realized_profit_ratio', 'close_profit_pct',
|
||||
'trade_duration_s', 'trade_duration',
|
||||
'profit_ratio', 'profit_pct', 'profit_abs', 'stop_loss_abs',
|
||||
'initial_stop_loss_abs',
|
||||
'orders',
|
||||
]
|
||||
failed = []
|
||||
# Ensure all attributes written can be read.
|
||||
for obj, value in tjson.items():
|
||||
if obj in excluded:
|
||||
continue
|
||||
tattr = getattr(trade, obj, None)
|
||||
if isinstance(tattr, datetime):
|
||||
tattr = tattr.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if tattr != value:
|
||||
failed.append((obj, tattr, value))
|
||||
|
||||
assert tjson.get('trade_id') == trade.id
|
||||
assert tjson.get('quote_currency') == trade.stake_currency
|
||||
assert tjson.get('stop_loss_abs') == trade.stop_loss
|
||||
assert tjson.get('initial_stop_loss_abs') == trade.initial_stop_loss
|
||||
|
||||
excluded_o = [
|
||||
'order_filled_timestamp', 'ft_is_entry', 'pair', 'is_open', 'order_timestamp',
|
||||
]
|
||||
order_obj = trade.orders[0]
|
||||
for obj, value in tjson['orders'][0].items():
|
||||
if obj in excluded_o:
|
||||
continue
|
||||
tattr = getattr(order_obj, obj, None)
|
||||
if isinstance(tattr, datetime):
|
||||
tattr = tattr.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if tattr != value:
|
||||
failed.append((obj, tattr, value))
|
||||
|
||||
assert tjson['orders'][0]['pair'] == order_obj.ft_pair
|
||||
assert not failed
|
||||
|
||||
trade2 = LocalTrade.from_json(trade_string)
|
||||
assert len(trade2.orders) == len(t.orders)
|
||||
|
||||
trade3 = LocalTrade.from_json(trade_string)
|
||||
assert len(trade3.orders) == len(t.orders)
|
||||
|
|
|
@ -99,6 +99,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True, 'ft_fee_base': None,
|
||||
'funding_fee': ANY,
|
||||
}],
|
||||
}
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
|
|
@ -422,7 +422,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
|
|||
# enable custom stoploss, expected after 1st call, expected after 2nd call
|
||||
(0.2, 0.9, ExitType.NONE, None, False, False, 0.3, 0.9, ExitType.NONE, None),
|
||||
(0.2, 0.9, ExitType.NONE, None, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None),
|
||||
(0.2, 0.9, ExitType.NONE, 0.8, False, False, -0.2, 0.9, ExitType.LIQUIDATION, None),
|
||||
(0.2, 0.9, ExitType.NONE, 0.92, False, False, -0.09, 0.9, ExitType.LIQUIDATION, None),
|
||||
(0.2, 1.14, ExitType.NONE, None, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS,
|
||||
None),
|
||||
(0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None),
|
||||
|
|
|
@ -559,7 +559,6 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
|
|||
fetch_ticker=ticker_usdt,
|
||||
create_order=MagicMock(return_value=limit_buy_order_usdt_open),
|
||||
get_fee=fee,
|
||||
get_funding_fees=MagicMock(side_effect=ExchangeError()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
patch_get_signal(freqtrade)
|
||||
|
@ -567,7 +566,6 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
|
|||
# Create 2 existing trades
|
||||
freqtrade.execute_entry('ETH/USDT', default_conf_usdt['stake_amount'])
|
||||
freqtrade.execute_entry('NEO/BTC', default_conf_usdt['stake_amount'])
|
||||
assert log_has("Could not find funding fee.", caplog)
|
||||
|
||||
assert len(Trade.get_open_trades()) == 2
|
||||
# Change order_id for new orders
|
||||
|
@ -2899,6 +2897,51 @@ def test_adjust_entry_replace_fail(
|
|||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_adjust_entry_replace_fail_create_order(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
limit_sell_order_old, fee, mocker, caplog, is_short
|
||||
) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
old_order = limit_sell_order_old if is_short else limit_buy_order_old
|
||||
old_order['id'] = open_trade.open_orders[0].order_id
|
||||
limit_entry_cancel = deepcopy(old_order)
|
||||
limit_entry_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_entry_cancel)
|
||||
fetch_order_mock = MagicMock(return_value=old_order)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
fetch_order=fetch_order_mock,
|
||||
cancel_order_with_result=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.sleep')
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_entry',
|
||||
side_effect=DependencyException())
|
||||
|
||||
open_trade.is_short = is_short
|
||||
Trade.session.add(open_trade)
|
||||
Trade.commit()
|
||||
|
||||
# Timeout to not interfere
|
||||
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
|
||||
|
||||
# Attempt replace order - which fails
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=12234)
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade)
|
||||
.where(Trade.is_open.is_(True))
|
||||
).all()
|
||||
|
||||
assert len(trades) == 0
|
||||
assert len(Order.session.scalars(select(Order)).all()) == 0
|
||||
assert fetch_order_mock.call_count == 1
|
||||
assert log_has_re(
|
||||
r"Could not replace order for.*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_adjust_entry_maintain_replace(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
|
@ -3398,20 +3441,20 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
|||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
caplog.clear()
|
||||
l_order['filled'] = 0.01
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog)
|
||||
|
||||
caplog.clear()
|
||||
cancel_order_mock.reset_mock()
|
||||
l_order['filled'] = 2
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
# Order remained open for some reason (cancel failed)
|
||||
|
@ -3419,12 +3462,12 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
|||
cancel_order_mock = MagicMock(return_value=cancel_entry_order)
|
||||
|
||||
mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||
# min_pair_stake empty should not crash
|
||||
mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=None)
|
||||
assert not freqtrade.handle_cancel_enter(
|
||||
trade, limit_order[entry_side(is_short)], trade.open_orders_ids[0], reason
|
||||
trade, limit_order[entry_side(is_short)], trade.open_orders[0], reason
|
||||
)
|
||||
|
||||
# Retry ...
|
||||
|
@ -3435,7 +3478,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
|||
co_mock = mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=cbo)
|
||||
fo_mock = mocker.patch(f'{EXMS}.fetch_order', return_value=cbo)
|
||||
assert not freqtrade.handle_cancel_enter(
|
||||
trade, cbo, cbo['id'], reason, replacing=True
|
||||
trade, cbo, trade.open_orders[0], reason, replacing=True
|
||||
)
|
||||
assert co_mock.call_count == 1
|
||||
assert fo_mock.call_count == 3
|
||||
|
@ -3460,7 +3503,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
|
|||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
assert freqtrade.handle_cancel_enter(
|
||||
trade, limit_buy_order_canceled_empty, trade.open_orders_ids[0], reason
|
||||
trade, limit_buy_order_canceled_empty, trade.open_orders[0], reason
|
||||
)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re(
|
||||
|
@ -3498,7 +3541,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
|
@ -3506,7 +3549,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||
order = deepcopy(l_order)
|
||||
order['status'] = 'canceled'
|
||||
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
|
||||
|
@ -3587,8 +3630,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
|||
'amount': 1,
|
||||
'status': "open"}
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
order_obj = trade.open_orders[-1]
|
||||
send_msg_mock.reset_mock()
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert trade.close_rate is None
|
||||
|
@ -3600,14 +3644,14 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
|||
# Partial exit - below exit threshold
|
||||
order['amount'] = amount * leverage
|
||||
order['filled'] = amount * 0.99 * leverage
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
# Assert cancel_order was not called (callcount remains unchanged)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||
|
@ -3619,7 +3663,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
|||
send_msg_mock.reset_mock()
|
||||
|
||||
order['filled'] = amount * 0.5 * leverage
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED'])
|
||||
|
@ -3635,13 +3679,14 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
|||
|
||||
# TODO: should not be magicmock
|
||||
trade = MagicMock()
|
||||
order_id = '125'
|
||||
order_obj = MagicMock()
|
||||
order_obj.order_id = '125'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
order = {'remaining': 1,
|
||||
'id': '125',
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order_id, reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
|
||||
# mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
|
||||
# assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
|
@ -4161,7 +4206,6 @@ def test_execute_trade_exit_market_order(
|
|||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
get_funding_fees=MagicMock(side_effect=ExchangeError()),
|
||||
)
|
||||
patch_whitelist(mocker, default_conf_usdt)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
|
@ -4187,7 +4231,6 @@ def test_execute_trade_exit_market_order(
|
|||
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
|
||||
exit_check=ExitCheckTuple(exit_type=ExitType.ROI)
|
||||
)
|
||||
assert log_has("Could not update funding fee.", caplog)
|
||||
|
||||
assert not trade.is_open
|
||||
assert pytest.approx(trade.close_profit) == profit_ratio
|
||||
|
@ -5874,16 +5917,17 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None:
|
|||
@pytest.mark.parametrize('trading_mode,calls,t1,t2', [
|
||||
('spot', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
|
||||
('margin', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
|
||||
('futures', 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
|
||||
('futures', 32, "2021-08-31 23:59:59", "2021-09-01 08:00:01"),
|
||||
('futures', 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"),
|
||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
|
||||
('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
|
||||
('futures', 15, "2021-09-01 00:01:02", "2021-09-01 08:00:01"),
|
||||
('futures', 16, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
|
||||
('futures', 16, "2021-08-31 23:59:59", "2021-09-01 08:00:01"),
|
||||
('futures', 16, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
|
||||
('futures', 16, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
|
||||
('futures', 16, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
|
||||
('futures', 16, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
|
||||
('futures', 17, "2021-08-31 23:59:59", "2021-09-01 08:01:05"),
|
||||
('futures', 17, "2021-08-31 23:59:59", "2021-09-01 08:01:06"),
|
||||
('futures', 17, "2021-08-31 23:59:59", "2021-09-01 08:01:07"),
|
||||
('futures', 17, "2021-08-31 23:59:58", "2021-09-01 08:01:07"),
|
||||
])
|
||||
def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, time_machine,
|
||||
t1, t2):
|
||||
|
@ -5933,7 +5977,7 @@ def test_update_funding_fees(
|
|||
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
|
||||
"""
|
||||
# SETUP
|
||||
time_machine.move_to("2021-09-01 00:00:00 +00:00")
|
||||
time_machine.move_to("2021-09-01 00:00:16 +00:00")
|
||||
|
||||
open_order = limit_order_open[entry_side(is_short)]
|
||||
open_exit_order = limit_order_open[exit_side(is_short)]
|
||||
|
|
148
tests/testdata/kraken/trades_csv/BCHEUR.csv
vendored
Normal file
148
tests/testdata/kraken/trades_csv/BCHEUR.csv
vendored
Normal file
|
@ -0,0 +1,148 @@
|
|||
1672531436,90.540000,1.10448420
|
||||
1672531471,90.450000,1.00000000
|
||||
1672531647,90.360000,0.28600000
|
||||
1672533909,90.000000,0.05000000
|
||||
1672533909,90.000000,0.55555555
|
||||
1672533909,90.000000,0.05000000
|
||||
1672539535,90.160000,5.15191003
|
||||
1672539535,90.170000,6.19997081
|
||||
1672541045,90.170000,0.17270970
|
||||
1672541045,90.170000,0.81109774
|
||||
1672547372,89.940000,0.51535378
|
||||
1672549388,89.820000,0.10000000
|
||||
1672552963,90.020000,0.49279466
|
||||
1672556400,90.020000,0.02188895
|
||||
1672556899,89.880000,0.95129174
|
||||
1672559791,90.040000,0.11100000
|
||||
1672562703,90.130000,5.00000000
|
||||
1672563176,90.040000,0.11106175
|
||||
1672568240,90.260000,0.49146128
|
||||
1672568359,90.270000,2.00000000
|
||||
1672568359,90.270000,15.00000000
|
||||
1672568359,90.280000,15.00000000
|
||||
1672568359,90.290000,8.00000000
|
||||
1672568359,90.310000,1.10000000
|
||||
1672568359,90.310000,5.00000000
|
||||
1672568359,90.320000,7.24209194
|
||||
1672568359,90.320000,0.33774286
|
||||
1672568359,90.340000,7.70694586
|
||||
1672568359,90.350000,1.00000000
|
||||
1672568359,90.360000,1.00000000
|
||||
1672568359,90.360000,3.44290939
|
||||
1672568359,90.370000,1.00000000
|
||||
1672568359,90.370000,1.49359404
|
||||
1672568359,90.380000,1.00000000
|
||||
1672568359,90.390000,1.00000000
|
||||
1672568359,90.390000,0.65058415
|
||||
1672568359,90.390000,3.38801189
|
||||
1672568359,90.420000,3.42212570
|
||||
1672568359,90.440000,5.15202596
|
||||
1672568359,90.450000,3.38991070
|
||||
1672568359,90.450000,0.19329203
|
||||
1672568359,90.460000,21.52896864
|
||||
1672568526,90.250000,0.28048094
|
||||
1672569016,90.340000,5.95581833
|
||||
1672569115,90.340000,6.00000000
|
||||
1672570237,90.310000,0.14993569
|
||||
1672571388,90.370000,0.21230004
|
||||
1672571631,90.370000,0.74619835
|
||||
1672574412,90.400000,0.12438419
|
||||
1672576112,90.400000,0.53000000
|
||||
1672577367,90.400000,0.47000000
|
||||
1672577367,90.370000,0.10698000
|
||||
1672579099,90.360000,0.49091739
|
||||
1672580071,90.310000,0.29699158
|
||||
1672580225,90.300000,9.00000000
|
||||
1672581264,90.280000,0.17823568
|
||||
1672581264,90.280000,0.49008938
|
||||
1672581589,90.250000,4.00000000
|
||||
1672582660,90.250000,0.17506004
|
||||
1672582660,90.250000,3.75716335
|
||||
1672585445,90.180000,0.17600000
|
||||
1672585589,90.180000,0.05426674
|
||||
1672586068,90.190000,0.20000000
|
||||
1672587250,90.120000,1.33484000
|
||||
1672587559,90.100000,0.21882576
|
||||
1672587585,90.100000,0.28075194
|
||||
1672588454,90.160000,0.19694290
|
||||
1672588454,90.170000,0.92488128
|
||||
1672589636,90.080000,0.29193475
|
||||
1672590151,90.150000,14.08171960
|
||||
1672590566,90.120000,1.01627000
|
||||
1672590888,90.270000,4.25070441
|
||||
1672590952,90.250000,0.14121710
|
||||
1672590952,90.250000,1.98437057
|
||||
1672591010,90.200000,0.34152350
|
||||
1672591010,90.190000,9.00575666
|
||||
1672591132,90.300000,0.89294468
|
||||
1672591380,90.310000,0.10531140
|
||||
1672591380,90.320000,0.72237064
|
||||
1672591390,90.260000,0.17076175
|
||||
1672591390,90.250000,0.93726662
|
||||
1672591626,90.280000,0.13803658
|
||||
1672591626,90.270000,1.39856040
|
||||
1672592025,90.330000,0.18053494
|
||||
1672592350,90.340000,3.82317350
|
||||
1672592350,90.360000,5.66081594
|
||||
1672592350,90.370000,0.23660665
|
||||
1672592350,90.380000,0.89618051
|
||||
1672592398,90.370000,1.04662011
|
||||
1672592444,90.360000,0.12419225
|
||||
1672593227,90.360000,1.96390112
|
||||
1672599481,90.710000,0.05520000
|
||||
1672599657,90.710000,39.94480000
|
||||
1672600545,90.720000,1.09096000
|
||||
1672600545,90.720000,39.99904000
|
||||
1672601235,90.790000,0.11022927
|
||||
1672601584,90.790000,0.11897764
|
||||
1672601584,90.800000,0.16215666
|
||||
1672602875,90.830000,1.00000000
|
||||
1672604100,90.820000,0.53216284
|
||||
1672604129,90.820000,0.05503577
|
||||
1672604219,90.810000,0.07234981
|
||||
1672607019,90.920000,2.00000000
|
||||
1672607019,90.920000,40.00000000
|
||||
1672607019,90.920000,8.00000000
|
||||
1672607067,90.930000,2.00000000
|
||||
1672607067,90.930000,8.00000000
|
||||
1672607067,90.930000,21.35000000
|
||||
1672607076,90.910000,5.50000000
|
||||
1672607196,90.910000,2.00000000
|
||||
1672607196,90.910000,0.30030000
|
||||
1672607310,90.890000,0.96144343
|
||||
1672607310,90.900000,0.50292482
|
||||
1672607703,90.760000,1.00000000
|
||||
1672607703,90.750000,3.52448259
|
||||
1672607703,90.750000,0.15295233
|
||||
1672607703,90.740000,0.82256508
|
||||
1672608691,90.790000,0.19963741
|
||||
1672608691,90.810000,0.31977406
|
||||
1672609051,90.820000,0.38000000
|
||||
1672609580,90.780000,0.24004396
|
||||
1672609580,90.770000,5.25995604
|
||||
1672609695,90.740000,0.98519900
|
||||
1672609697,90.740000,0.14629976
|
||||
1672609698,90.740000,0.10000000
|
||||
1672610001,90.740000,0.21984068
|
||||
1672610001,90.740000,0.08898963
|
||||
1672610507,90.760000,0.35960205
|
||||
1672610527,90.760000,0.74039795
|
||||
1672610527,90.760000,0.11030671
|
||||
1672610527,90.760000,1.82902226
|
||||
1672611326,90.680000,0.08710000
|
||||
1672612351,90.680000,1.18193004
|
||||
1672612351,90.660000,0.11985422
|
||||
1672612460,90.640000,0.22000000
|
||||
1672612460,90.600000,0.24680297
|
||||
1672612460,90.590000,0.65058415
|
||||
1672612460,90.590000,1.10000000
|
||||
1672612460,90.570000,2.78261288
|
||||
1672612485,90.520000,2.00000000
|
||||
1672612485,90.530000,1.10000000
|
||||
1672612485,90.530000,5.00000000
|
||||
1672613044,90.520000,1.08661000
|
||||
1672613471,90.550000,0.22739075
|
||||
1672613471,90.560000,0.92169701
|
||||
1672613957,90.490000,0.50000000
|
||||
1672614482,90.520000,0.19893239
|
||||
1672615187,90.530000,1.00000000
|
|
486
tests/testdata/kraken/trades_csv/incremental_q2/BCHEUR.csv
vendored
Normal file
486
tests/testdata/kraken/trades_csv/incremental_q2/BCHEUR.csv
vendored
Normal file
|
@ -0,0 +1,486 @@
|
|||
1672620536,90.070000,1.10000000
|
||||
1672621569,90.000000,0.05000000
|
||||
1672621572,89.900000,0.16781327
|
||||
1672621572,89.880000,3.21796979
|
||||
1672621574,89.680000,0.15038625
|
||||
1672621591,89.500000,0.40223000
|
||||
1672621591,89.500000,0.00000463
|
||||
1672621591,89.460000,0.42312537
|
||||
1672621591,89.460000,0.00001340
|
||||
1672621591,89.450000,0.27462660
|
||||
1672621591,89.450000,1.55812912
|
||||
1672621592,89.430000,3.49563000
|
||||
1672621592,89.430000,0.00000245
|
||||
1672621592,89.400000,3.24020755
|
||||
1672621593,89.400000,0.00000579
|
||||
1672621593,89.340000,3.39843000
|
||||
1672621593,89.340000,0.00000367
|
||||
1672621594,89.310000,3.46879000
|
||||
1672621594,89.310000,0.00000151
|
||||
1672621594,89.300000,0.23606849
|
||||
1672621594,89.300000,0.00000151
|
||||
1672621611,89.510000,0.05201346
|
||||
1672623048,89.400000,0.05000000
|
||||
1672623487,89.340000,3.29746218
|
||||
1672624539,89.480000,0.13947779
|
||||
1672630508,89.970000,0.48616536
|
||||
1672633444,89.830000,0.36077173
|
||||
1672633444,89.840000,0.18222827
|
||||
1672635717,90.070000,0.15020808
|
||||
1672638127,90.170000,0.11950000
|
||||
1672638790,90.250000,2.08482000
|
||||
1672639521,90.250000,0.00000100
|
||||
1672639521,90.350000,0.16503183
|
||||
1672639521,90.360000,0.10887277
|
||||
1672641041,90.420000,0.33000000
|
||||
1672643661,90.290000,0.16644000
|
||||
1672643664,90.290000,0.00000378
|
||||
1672643677,90.900000,0.27611440
|
||||
1672643677,90.910000,3.55887567
|
||||
1672643677,90.930000,1.94112433
|
||||
1672643681,90.930000,1.45287534
|
||||
1672643683,91.000000,0.26528000
|
||||
1672643683,91.000000,0.27938000
|
||||
1672643698,91.100000,1.02000000
|
||||
1672643698,91.090000,1.10000000
|
||||
1672643698,91.080000,9.37501459
|
||||
1672643698,91.080000,10.00000000
|
||||
1672643698,91.080000,30.20498538
|
||||
1672643740,91.050000,2.00000000
|
||||
1672643740,91.050000,1.25174226
|
||||
1672643740,91.100000,0.21657000
|
||||
1672643742,91.140000,3.41661000
|
||||
1672643742,91.140000,0.00000339
|
||||
1672643745,91.300000,0.22559000
|
||||
1672643745,91.300000,0.20927000
|
||||
1672643754,91.530000,1.46349415
|
||||
1672643754,91.500000,0.05000000
|
||||
1672643755,91.500000,0.19820000
|
||||
1672643755,91.500000,0.23323000
|
||||
1672643755,91.400000,0.25276000
|
||||
1672643755,91.400000,0.24522000
|
||||
1672643755,91.360000,0.17059000
|
||||
1672643762,91.590000,3.38609591
|
||||
1672643762,91.590000,1.70666409
|
||||
1672643762,91.590000,5.00245000
|
||||
1672643763,91.590000,0.00000222
|
||||
1672643763,91.620000,3.28019778
|
||||
1672643764,91.620000,0.00001175
|
||||
1672643764,91.680000,3.35131000
|
||||
1672643765,91.680000,0.00000053
|
||||
1672643765,91.710000,1.99999947
|
||||
1672643765,91.710000,1.45873628
|
||||
1672643780,91.800000,0.24421000
|
||||
1672643781,91.700000,0.24097000
|
||||
1672643786,91.500000,0.24123000
|
||||
1672643786,91.500000,0.23718000
|
||||
1672643917,91.440000,0.09961400
|
||||
1672643954,91.460000,1.46349415
|
||||
1672644454,91.420000,1.10000000
|
||||
1672644454,91.410000,0.90000000
|
||||
1672644817,91.290000,0.97023360
|
||||
1672644888,91.370000,5.00000000
|
||||
1672645505,91.500000,0.55869071
|
||||
1672645505,91.500000,0.20130929
|
||||
1672646731,91.680000,0.10000000
|
||||
1672647152,91.840000,1.10000000
|
||||
1672647152,91.840000,0.72640000
|
||||
1672647284,91.900000,0.10000000
|
||||
1672647284,92.000000,0.10000000
|
||||
1672647294,92.100000,0.07400739
|
||||
1672647331,92.100000,0.02599261
|
||||
1672647331,92.200000,0.10000000
|
||||
1672647408,92.300000,0.10000000
|
||||
1672647447,92.400000,0.10000000
|
||||
1672647452,92.530000,1.10000000
|
||||
1672647452,92.530000,7.07935280
|
||||
1672647452,92.520000,1.82064720
|
||||
1672647453,92.530000,7.08005995
|
||||
1672647453,92.520000,2.91994005
|
||||
1672647453,92.520000,1.10000000
|
||||
1672647453,92.520000,7.08106089
|
||||
1672647453,92.520000,0.11257595
|
||||
1672647453,92.520000,1.70636316
|
||||
1672647453,92.520000,1.61618244
|
||||
1672647977,92.290000,1.10000000
|
||||
1672647977,92.280000,6.10788595
|
||||
1672648596,92.600000,0.28769588
|
||||
1672649352,92.520000,1.00000000
|
||||
1672649722,92.360000,0.43190135
|
||||
1672649722,92.350000,0.56809865
|
||||
1672650340,92.430000,1.10000000
|
||||
1672650340,92.440000,0.17885343
|
||||
1672650340,92.450000,0.63696354
|
||||
1672651165,92.390000,40.00000000
|
||||
1672651596,92.380000,1.00000000
|
||||
1672651664,92.390000,2.00754507
|
||||
1672651716,92.380000,1.00000000
|
||||
1672651727,92.390000,1.40528155
|
||||
1672651746,92.380000,0.10934211
|
||||
1672651746,92.370000,0.14252789
|
||||
1672651865,92.410000,0.10000000
|
||||
1672651866,92.410000,0.10000000
|
||||
1672651922,92.420000,1.02836794
|
||||
1672651981,92.410000,0.33805200
|
||||
1672651982,92.410000,2.96775605
|
||||
1672652034,92.400000,40.00000000
|
||||
1672652230,92.410000,3.00000003
|
||||
1672652230,92.410000,40.00000000
|
||||
1672652462,92.560000,4.72637791
|
||||
1672652785,92.540000,0.26371093
|
||||
1672652785,92.550000,0.06028907
|
||||
1672653191,92.380000,0.11300328
|
||||
1672653191,92.370000,0.08699672
|
||||
1672654246,92.480000,1.10000000
|
||||
1672654246,92.480000,0.18835710
|
||||
1672654246,92.480000,0.71723426
|
||||
1672654327,92.420000,1.10000000
|
||||
1672654327,92.430000,0.90624217
|
||||
1672654405,92.400000,0.71000000
|
||||
1672654405,92.420000,1.10000000
|
||||
1672654405,92.430000,0.19732780
|
||||
1672654469,92.400000,0.44000000
|
||||
1672654469,92.410000,0.18835711
|
||||
1672654469,92.420000,1.10000000
|
||||
1672654469,92.420000,0.27897069
|
||||
1672654503,92.420000,1.56537046
|
||||
1672654551,92.370000,2.00000000
|
||||
1672654551,92.380000,0.00732780
|
||||
1672656132,92.360000,0.51441726
|
||||
1672656132,92.350000,0.07117974
|
||||
1672656562,92.460000,1.09777201
|
||||
1672656822,92.540000,0.25720863
|
||||
1672656822,92.550000,0.17503688
|
||||
1672657382,92.410000,0.88037274
|
||||
1672657843,92.300000,0.52109000
|
||||
1672659479,92.450000,0.86375500
|
||||
1672659527,92.450000,0.95973892
|
||||
1672659842,92.380000,0.10000000
|
||||
1672659844,92.380000,0.28000000
|
||||
1672659858,92.380000,0.40000000
|
||||
1672659860,92.380000,0.59000000
|
||||
1672659860,92.380000,0.21230377
|
||||
1672659860,92.380000,0.19581295
|
||||
1672659860,92.370000,0.05188328
|
||||
1672659880,92.440000,0.41651079
|
||||
1672659880,92.450000,1.58233921
|
||||
1672660744,92.620000,0.20825539
|
||||
1672660744,92.620000,2.00000000
|
||||
1672660744,92.620000,0.81325838
|
||||
1672660786,92.540000,0.40620484
|
||||
1672660786,92.530000,0.50322423
|
||||
1672661316,92.760000,3.00000000
|
||||
1672661316,92.760000,0.51083000
|
||||
1672661316,92.760000,0.00000750
|
||||
1672661763,92.880000,0.40600000
|
||||
1672662004,92.680000,0.63474730
|
||||
1672662021,92.610000,0.99999999
|
||||
1672662056,92.670000,0.51391586
|
||||
1672662831,92.640000,0.20718714
|
||||
1672662831,92.630000,0.19281286
|
||||
1672663146,92.780000,0.20678545
|
||||
1672664406,92.660000,0.20698630
|
||||
1672664406,92.650000,0.09207754
|
||||
1672665304,92.600000,0.27096400
|
||||
1672665644,92.560000,0.10349315
|
||||
1672665644,92.550000,0.50000000
|
||||
1672665644,92.550000,1.10000000
|
||||
1672665644,92.540000,2.29650685
|
||||
1672665885,92.680000,0.25647281
|
||||
1672666383,92.750000,1.10000000
|
||||
1672666383,92.770000,0.33443428
|
||||
1672666383,92.780000,1.61158033
|
||||
1672666724,92.620000,0.26415235
|
||||
1672666724,92.600000,0.00975325
|
||||
1672667271,92.530000,0.21512394
|
||||
1672667271,92.520000,3.44492536
|
||||
1672667271,92.520000,1.10000000
|
||||
1672667271,92.500000,13.00000000
|
||||
1672667271,92.500000,4.17971070
|
||||
1672667820,92.490000,0.90149481
|
||||
1672668419,92.600000,0.05179685
|
||||
1672668761,92.600000,0.20000000
|
||||
1672668970,92.600000,1.00000000
|
||||
1672668970,92.600000,0.36870115
|
||||
1672669497,92.870000,0.27986569
|
||||
1672669497,92.920000,0.23423604
|
||||
1672670020,92.780000,0.24749482
|
||||
1672670020,92.780000,0.22946945
|
||||
1672670088,92.860000,0.39066985
|
||||
1672670088,92.870000,0.60802221
|
||||
1672670162,92.800000,0.19533493
|
||||
1672670162,92.810000,1.57768714
|
||||
1672671482,92.970000,0.05000000
|
||||
1672671482,92.990000,0.25805040
|
||||
1672671482,93.000000,0.05500000
|
||||
1672671482,93.000000,1.17150537
|
||||
1672671484,93.030000,1.90000000
|
||||
1672671484,93.050000,0.10000000
|
||||
1672671484,93.050000,0.09533493
|
||||
1672671484,93.100000,5.50000000
|
||||
1672671597,93.210000,0.28559636
|
||||
1672671922,93.210000,0.06224165
|
||||
1672672258,93.190000,0.05360162
|
||||
1672672715,93.440000,0.35848855
|
||||
1672673021,93.390000,0.24603359
|
||||
1672673644,93.500000,1.25128947
|
||||
1672673926,93.370000,0.10000000
|
||||
1672673926,93.370000,0.10000000
|
||||
1672674245,93.570000,3.00000000
|
||||
1672674245,93.570000,0.27582332
|
||||
1672674251,93.600000,3.00000000
|
||||
1672674251,93.600000,0.27021894
|
||||
1672674269,93.620000,0.05000000
|
||||
1672674278,93.690000,0.16020297
|
||||
1672674928,93.870000,16.00000000
|
||||
1672674928,93.870000,0.42229400
|
||||
1672674928,93.880000,3.00000000
|
||||
1672674928,93.880000,0.83625840
|
||||
1672675263,93.920000,0.15897954
|
||||
1672675354,93.790000,0.05541000
|
||||
1672675420,93.750000,3.42716504
|
||||
1672675420,93.750000,1.10000000
|
||||
1672675420,93.730000,0.30071309
|
||||
1672675420,93.730000,0.26523930
|
||||
1672675420,93.720000,3.43747306
|
||||
1672675420,93.720000,2.00000000
|
||||
1672675420,93.700000,2.00000000
|
||||
1672675420,93.690000,3.50480003
|
||||
1672675420,93.690000,17.74436742
|
||||
1672675420,93.690000,2.53044755
|
||||
1672675420,93.690000,5.59187611
|
||||
1672675420,93.690000,1.10269129
|
||||
1672675420,93.690000,5.00000000
|
||||
1672675420,93.690000,10.00000000
|
||||
1672675420,93.690000,1.99522711
|
||||
1672677602,93.900000,0.33635572
|
||||
1672677602,93.890000,0.02213283
|
||||
1672677724,93.960000,0.20921217
|
||||
1672677724,93.970000,4.01815113
|
||||
1672677724,93.980000,1.10000000
|
||||
1672677724,93.980000,4.99304515
|
||||
1672678203,93.960000,0.53112491
|
||||
1672678563,93.780000,1.06849022
|
||||
1672678711,93.780000,0.25694338
|
||||
1672678807,93.720000,0.21340200
|
||||
1672678854,93.710000,0.20162914
|
||||
1672678854,93.710000,1.00949171
|
||||
1672678856,93.710000,1.48404486
|
||||
1672678996,93.660000,0.21353800
|
||||
1672679012,93.660000,0.21353800
|
||||
1672679028,93.660000,0.21356100
|
||||
1672679037,93.670000,0.11009220
|
||||
1672679037,93.660000,1.78643900
|
||||
1672679037,93.660000,3.52200393
|
||||
1672679042,93.660000,0.21353800
|
||||
1672679057,93.660000,0.21353800
|
||||
1672679072,93.670000,0.21351600
|
||||
1672679087,93.650000,0.11692327
|
||||
1672679087,93.650000,0.09663773
|
||||
1672679094,93.660000,4.95970000
|
||||
1672679104,93.660000,0.21356100
|
||||
1672679118,93.660000,0.21353800
|
||||
1672679135,93.650000,0.11692328
|
||||
1672679135,93.650000,0.09663772
|
||||
1672679151,93.650000,0.21356100
|
||||
1672679166,93.620000,0.21358400
|
||||
1672679180,93.630000,0.21360700
|
||||
1672679195,93.620000,0.21363000
|
||||
1672679212,93.620000,0.21363000
|
||||
1672679225,93.620000,0.21363000
|
||||
1672679240,93.620000,0.21363000
|
||||
1672679256,93.620000,0.21363000
|
||||
1672679273,93.630000,0.21360700
|
||||
1672679286,93.630000,0.21360700
|
||||
1672679303,93.630000,0.21360700
|
||||
1672679315,93.630000,1.39400000
|
||||
1672679318,93.640000,0.21360700
|
||||
1672679332,93.640000,0.21358400
|
||||
1672679342,93.650000,40.00000000
|
||||
1672679356,93.640000,0.21358400
|
||||
1672679370,93.640000,0.21358400
|
||||
1672679386,93.640000,0.21358400
|
||||
1672679402,93.640000,0.21358400
|
||||
1672679416,93.640000,0.21358400
|
||||
1672679432,93.640000,0.21358400
|
||||
1672679448,93.640000,0.21358400
|
||||
1672679463,93.640000,0.21358400
|
||||
1672679477,93.640000,0.21358400
|
||||
1672679494,93.620000,0.21365200
|
||||
1672679510,93.620000,0.21363000
|
||||
1672679525,93.620000,0.21363000
|
||||
1672679540,93.630000,0.21363000
|
||||
1672679554,93.630000,0.21360700
|
||||
1672679570,93.630000,0.21360700
|
||||
1672679585,93.630000,0.21360700
|
||||
1672679600,93.630000,0.21360700
|
||||
1672679615,93.630000,0.21360700
|
||||
1672679632,93.630000,0.21360700
|
||||
1672679646,93.620000,0.21363000
|
||||
1672679661,93.630000,0.21360700
|
||||
1672679676,93.630000,0.21360700
|
||||
1672679691,93.630000,0.21360700
|
||||
1672679707,93.620000,0.21363000
|
||||
1672679726,93.620000,0.21363000
|
||||
1672679741,93.620000,0.21360700
|
||||
1672679757,93.620000,0.21363000
|
||||
1672679778,93.640000,0.21360700
|
||||
1672679794,93.660000,0.21360700
|
||||
1672679809,93.680000,0.21349300
|
||||
1672679821,93.700000,0.21340200
|
||||
1672679821,93.700000,0.21353800
|
||||
1672679821,93.700000,0.21353800
|
||||
1672679821,93.700000,0.21356100
|
||||
1672679821,93.700000,0.21353800
|
||||
1672679821,93.700000,0.21353800
|
||||
1672679821,93.700000,0.21351600
|
||||
1672679821,93.700000,0.21356100
|
||||
1672679821,93.700000,0.21356100
|
||||
1672679821,93.700000,0.21353800
|
||||
1672679821,93.700000,0.21356100
|
||||
1672679821,93.700000,0.21356100
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21358400
|
||||
1672679821,93.700000,0.21365200
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21363000
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21360700
|
||||
1672679821,93.700000,0.21349300
|
||||
1672679829,93.690000,0.21349300
|
||||
1672679943,93.730000,0.10000000
|
||||
1672679943,93.740000,0.35573142
|
||||
1672680124,93.760000,0.41611649
|
||||
1672680124,93.770000,1.10000000
|
||||
1672680124,93.770000,1.00289427
|
||||
1672680599,93.710000,40.00000000
|
||||
1672680599,93.710000,40.00000000
|
||||
1672681203,93.760000,0.30635902
|
||||
1672681203,93.770000,0.68274662
|
||||
1672681262,93.710000,1.01899965
|
||||
1672681304,93.700000,40.00000000
|
||||
1672681323,93.750000,0.57938646
|
||||
1672681386,93.750000,1.48381671
|
||||
1672681458,93.750000,0.01067000
|
||||
1672681669,93.630000,40.00000000
|
||||
1672682646,93.650000,0.17545586
|
||||
1672682646,93.660000,2.04572869
|
||||
1672682700,93.660000,0.43630185
|
||||
1672683062,93.520000,0.21349300
|
||||
1672683333,93.400000,40.00000000
|
||||
1672683540,93.370000,0.37323978
|
||||
1672683540,93.360000,0.78018965
|
||||
1672683664,93.400000,40.00000000
|
||||
1672683664,93.400000,40.00000000
|
||||
1672683912,93.360000,40.00000000
|
||||
1672684035,93.380000,0.10000000
|
||||
1672684681,93.400000,40.00000000
|
||||
1672684681,93.400000,5.00000000
|
||||
1672684895,93.450000,0.28232122
|
||||
1672684895,93.450000,0.16798232
|
||||
1672684895,93.460000,2.74269478
|
||||
1672686428,93.590000,0.32054706
|
||||
1672686593,93.610000,0.18606342
|
||||
1672686593,93.600000,1.10000000
|
||||
1672686593,93.600000,0.64000000
|
||||
1672686593,93.600000,2.00000000
|
||||
1672686593,93.600000,0.56906893
|
||||
1672686763,93.580000,1.10000000
|
||||
1672686763,93.570000,0.18606343
|
||||
1672686763,93.560000,3.00000000
|
||||
1672686763,93.550000,6.66393657
|
||||
1672686853,93.600000,1.04000000
|
||||
1672686853,93.580000,0.31000000
|
||||
1672687010,93.660000,0.51441726
|
||||
1672687010,93.670000,0.49536629
|
||||
1672687144,93.660000,0.25720863
|
||||
1672687144,93.670000,0.13025611
|
||||
1672687924,93.380000,1.38703774
|
||||
1672688043,93.450000,1.10000000
|
||||
1672688043,93.470000,9.62310000
|
||||
1672688163,93.450000,0.12860432
|
||||
1672688163,93.460000,0.74330839
|
||||
1672688463,93.450000,0.12860432
|
||||
1672688463,93.460000,2.43888543
|
||||
1672689094,93.500000,0.48000000
|
||||
1672689094,93.500000,1.52000000
|
||||
1672689122,93.500000,0.69746509
|
||||
1672689605,93.380000,0.85913567
|
||||
1672690623,93.360000,1.55373756
|
||||
1672690623,93.380000,0.97334211
|
||||
1672690731,93.280000,0.37258400
|
||||
1672691284,93.510000,1.10000000
|
||||
1672691284,93.520000,0.17017217
|
||||
1672691284,93.530000,1.86482783
|
||||
1672693176,93.340000,0.08092000
|
||||
1672693262,93.400000,0.43291938
|
||||
1672694405,93.280000,0.40741109
|
||||
1672694828,93.200000,0.41321135
|
||||
1672694828,93.200000,0.12328865
|
||||
1672695290,93.240000,0.20660567
|
||||
1672695290,93.230000,1.10000000
|
||||
1672695290,93.220000,3.00000000
|
||||
1672695290,93.210000,3.44792866
|
||||
1672695290,93.200000,3.00000000
|
||||
1672695290,93.200000,1.34083632
|
||||
1672695290,93.200000,2.52411935
|
||||
1672695567,93.230000,0.09530603
|
||||
1672695997,93.310000,0.10280546
|
||||
1672696328,93.270000,0.10705255
|
||||
1672696328,93.260000,0.00294745
|
||||
1672697520,93.390000,0.38943251
|
||||
1672697520,93.400000,1.10000000
|
||||
1672697520,93.400000,0.03084631
|
||||
1672697584,93.400000,0.21413300
|
||||
1672697656,93.400000,0.21413300
|
||||
1672697671,93.400000,0.21314117
|
||||
1672698111,93.500000,0.34000000
|
||||
1672698111,93.500000,0.26617473
|
||||
1672698111,93.510000,3.39382527
|
||||
1672698284,93.430000,0.38132990
|
||||
1672698284,93.420000,1.10000000
|
||||
1672698284,93.400000,12.34766434
|
||||
1672698922,93.220000,0.11677000
|
||||
1672699624,93.220000,0.32375231
|
||||
1672699624,93.230000,1.10000000
|
||||
1672699624,93.230000,0.50000000
|
||||
1672699624,93.240000,0.07624769
|
||||
1672701005,93.000000,0.34305825
|
||||
1672701389,93.180000,0.26786932
|
||||
1672701423,93.180000,1.00000000
|
||||
1672701423,93.190000,1.10000000
|
||||
1672701423,93.190000,0.44235389
|
|
Loading…
Reference in New Issue
Block a user