Merge branch 'develop' into pr/cyberjunky/6615
This commit is contained in:
commit
2653d83fee
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: '4.0.1'
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: 'v0.942'
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: '5.10.1'
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
# stages: [push]
|
@ -182,6 +182,7 @@
|
|||||||
"disable_dataframe_checks": false,
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "SampleStrategy",
|
"strategy": "SampleStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
|
"add_config_files": [],
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "jsongz"
|
||||||
}
|
}
|
||||||
|
@ -53,14 +53,33 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
|||||||
|
|
||||||
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
||||||
|
|
||||||
|
You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file.
|
||||||
|
This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands.
|
||||||
|
|
||||||
!!! Tip "Use multiple configuration files to keep secrets secret"
|
!!! Tip "Use multiple configuration files to keep secrets secret"
|
||||||
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
|
You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself.
|
||||||
|
|
||||||
|
``` json title="user_data/config.json"
|
||||||
|
"add_config_files": [
|
||||||
|
"config-private.json"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trade --config user_data/config.json <...>
|
||||||
|
```
|
||||||
|
|
||||||
|
The 2nd file should only specify what you intend to override.
|
||||||
|
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
||||||
|
|
||||||
|
For one-off commands, you can also use the below syntax by specifying multiple "--config" parameters.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>
|
||||||
```
|
```
|
||||||
The 2nd file should only specify what you intend to override.
|
|
||||||
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
This is equivalent to the example above - but `config-private.json` is specified as cli argument.
|
||||||
|
|
||||||
|
|
||||||
## Configuration parameters
|
## Configuration parameters
|
||||||
|
|
||||||
@ -175,6 +194,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
|
||||||
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
||||||
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
||||||
|
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
||||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||||
|
@ -26,6 +26,9 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
|
|||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
|
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
||||||
|
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
||||||
|
|
||||||
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
|
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Devcontainer setup
|
### Devcontainer setup
|
||||||
|
@ -96,7 +96,7 @@ Strategy arguments:
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade plot-dataframe -p BTC/ETH
|
freqtrade plot-dataframe -p BTC/ETH --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
||||||
@ -107,9 +107,6 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
|||||||
Specify custom indicators.
|
Specify custom indicators.
|
||||||
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
|
freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
|
||||||
```
|
```
|
||||||
|
@ -88,10 +88,11 @@ Allows to define custom exit signals, indicating that specified position should
|
|||||||
|
|
||||||
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
|
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
|
||||||
|
|
||||||
Using custom_exit() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
Using `custom_exit()` signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Returning a (none-empty) `string` or `True` from this method is equal to setting exit signal on a candle at specified time. This method is not called when exit signal is set already, or if exit signals are disabled (`use_exit_signal=False` or `exit_profit_only=True` while profit is below `exit_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
Returning a (none-empty) `string` or `True` from this method is equal to setting exit signal on a candle at specified time. This method is not called when exit signal is set already, or if exit signals are disabled (`use_exit_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||||
|
`custom_exit()` will ignore `exit_profit_only`, and will always be called unless `use_exit_signal=False`, even if there is a new enter signal.
|
||||||
|
|
||||||
An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
|
An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ You can use the quick summary as checklist. Please refer to the detailed section
|
|||||||
|
|
||||||
## Quick summary / migration checklist
|
## Quick summary / migration checklist
|
||||||
|
|
||||||
Note : `force_exit`, `force_enter`, `emergency_exit` are changed to `force_exit`, `force_enter`, `emergency_exit` respectively.
|
Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `force_enter`, `emergency_exit` respectively.
|
||||||
|
|
||||||
* Strategy methods:
|
* Strategy methods:
|
||||||
* [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend)
|
* [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend)
|
||||||
@ -145,6 +145,9 @@ Please refer to the [Strategy documentation](strategy-customization.md#exit-sign
|
|||||||
|
|
||||||
### `custom_sell`
|
### `custom_sell`
|
||||||
|
|
||||||
|
`custom_sell` has been renamed to `custom_exit`.
|
||||||
|
It's now also being called for every iteration, independent of current profit and `exit_profit_only` settings.
|
||||||
|
|
||||||
``` python hl_lines="2"
|
``` python hl_lines="2"
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
@ -75,19 +75,41 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_from_files(files: List[str]) -> Dict[str, Any]:
|
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recursively load configuration files if specified.
|
||||||
|
Sub-files are assumed to be relative to the initial config.
|
||||||
|
"""
|
||||||
config: Dict[str, Any] = {}
|
config: Dict[str, Any] = {}
|
||||||
|
if level > 5:
|
||||||
|
raise OperationalException("Config loop detected.")
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
return deepcopy(MINIMAL_CONFIG)
|
return deepcopy(MINIMAL_CONFIG)
|
||||||
|
files_loaded = []
|
||||||
# We expect here a list of config filenames
|
# We expect here a list of config filenames
|
||||||
for path in files:
|
for filename in files:
|
||||||
logger.info(f'Using config: {path} ...')
|
logger.info(f'Using config: {filename} ...')
|
||||||
# Merge config options, overwriting old values
|
if filename == '-':
|
||||||
config = deep_merge_dicts(load_config_file(path), config)
|
# Immediately load stdin and return
|
||||||
|
return load_config_file(filename)
|
||||||
|
file = Path(filename)
|
||||||
|
if base_path:
|
||||||
|
# Prepend basepath to allow for relative assignments
|
||||||
|
file = base_path / file
|
||||||
|
|
||||||
config['config_files'] = files
|
config_tmp = load_config_file(str(file))
|
||||||
|
if 'add_config_files' in config_tmp:
|
||||||
|
config_sub = load_from_files(
|
||||||
|
config_tmp['add_config_files'], file.resolve().parent, level + 1)
|
||||||
|
files_loaded.extend(config_sub.get('config_files', []))
|
||||||
|
deep_merge_dicts(config_sub, config_tmp)
|
||||||
|
|
||||||
|
files_loaded.insert(0, str(file))
|
||||||
|
|
||||||
|
# Merge config options, overwriting prior values
|
||||||
|
config = deep_merge_dicts(config_tmp, config)
|
||||||
|
|
||||||
|
config['config_files'] = files_loaded
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -91,15 +91,14 @@ SUPPORTED_FIAT = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MINIMAL_CONFIG = {
|
MINIMAL_CONFIG = {
|
||||||
'stake_currency': '',
|
"stake_currency": "",
|
||||||
'dry_run': True,
|
"dry_run": True,
|
||||||
'exchange': {
|
"exchange": {
|
||||||
'name': '',
|
"name": "",
|
||||||
'key': '',
|
"key": "",
|
||||||
'secret': '',
|
"secret": "",
|
||||||
'pair_whitelist': [],
|
"pair_whitelist": [],
|
||||||
'ccxt_async_config': {
|
"ccxt_async_config": {
|
||||||
'enableRateLimit': True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,15 +341,11 @@ class Exchange:
|
|||||||
return sorted(set([x['quote'] for _, x in markets.items()]))
|
return sorted(set([x['quote'] for _, x in markets.items()]))
|
||||||
|
|
||||||
def get_pair_quote_currency(self, pair: str) -> str:
|
def get_pair_quote_currency(self, pair: str) -> str:
|
||||||
"""
|
""" Return a pair's quote currency (base/quote:settlement) """
|
||||||
Return a pair's quote currency
|
|
||||||
"""
|
|
||||||
return self.markets.get(pair, {}).get('quote', '')
|
return self.markets.get(pair, {}).get('quote', '')
|
||||||
|
|
||||||
def get_pair_base_currency(self, pair: str) -> str:
|
def get_pair_base_currency(self, pair: str) -> str:
|
||||||
"""
|
""" Return a pair's base currency (base/quote:settlement) """
|
||||||
Return a pair's base currency
|
|
||||||
"""
|
|
||||||
return self.markets.get(pair, {}).get('base', '')
|
return self.markets.get(pair, {}).get('base', '')
|
||||||
|
|
||||||
def market_is_future(self, market: Dict[str, Any]) -> bool:
|
def market_is_future(self, market: Dict[str, Any]) -> bool:
|
||||||
|
@ -598,6 +598,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
pair, price, stake_amount, trade_side, enter_tag, trade)
|
pair, price, stake_amount, trade_side, enter_tag, trade)
|
||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
|
logger.info(f"No stake amount to enter a trade for {pair}.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if pos_adjust:
|
if pos_adjust:
|
||||||
@ -675,6 +676,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
|
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
open_date = datetime.now(timezone.utc)
|
open_date = datetime.now(timezone.utc)
|
||||||
funding_fees = self.exchange.get_funding_fees(
|
funding_fees = self.exchange.get_funding_fees(
|
||||||
pair=pair, amount=amount, is_short=is_short, open_date=open_date)
|
pair=pair, amount=amount, is_short=is_short, open_date=open_date)
|
||||||
@ -682,6 +684,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if trade is None:
|
if trade is None:
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
|
base_currency=base_currency,
|
||||||
|
stake_currency=self.config['stake_currency'],
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
|
@ -726,6 +726,7 @@ class Backtesting:
|
|||||||
|
|
||||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
|
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
amount = round((stake_amount / propose_rate) * leverage, 8)
|
amount = round((stake_amount / propose_rate) * leverage, 8)
|
||||||
is_short = (direction == 'short')
|
is_short = (direction == 'short')
|
||||||
# Necessary for Margin trading. Disabled until support is enabled.
|
# Necessary for Margin trading. Disabled until support is enabled.
|
||||||
@ -738,6 +739,8 @@ class Backtesting:
|
|||||||
id=self.trade_id_counter,
|
id=self.trade_id_counter,
|
||||||
open_order_id=self.order_id_counter,
|
open_order_id=self.order_id_counter,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
|
base_currency=base_currency,
|
||||||
|
stake_currency=self.config['stake_currency'],
|
||||||
open_rate=propose_rate,
|
open_rate=propose_rate,
|
||||||
open_rate_requested=propose_rate,
|
open_rate_requested=propose_rate,
|
||||||
open_date=current_time,
|
open_date=current_time,
|
||||||
|
@ -3,6 +3,8 @@ from typing import List
|
|||||||
|
|
||||||
from sqlalchemy import inspect, text
|
from sqlalchemy import inspect, text
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -58,6 +60,8 @@ def migrate_trades_and_orders_table(
|
|||||||
decl_base, inspector, engine,
|
decl_base, inspector, engine,
|
||||||
trade_back_name: str, cols: List,
|
trade_back_name: str, cols: List,
|
||||||
order_back_name: str, cols_order: List):
|
order_back_name: str, cols_order: List):
|
||||||
|
base_currency = get_column_def(cols, 'base_currency', 'null')
|
||||||
|
stake_currency = get_column_def(cols, 'stake_currency', 'null')
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
|
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
|
||||||
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
|
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
|
||||||
@ -130,7 +134,7 @@ def migrate_trades_and_orders_table(
|
|||||||
# Copy data back - following the correct schema
|
# Copy data back - following the correct schema
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(f"""insert into trades
|
connection.execute(text(f"""insert into trades
|
||||||
(id, exchange, pair, is_open,
|
(id, exchange, pair, base_currency, stake_currency, is_open,
|
||||||
fee_open, fee_open_cost, fee_open_currency,
|
fee_open, fee_open_cost, fee_open_currency,
|
||||||
fee_close, fee_close_cost, fee_close_currency, open_rate,
|
fee_close, fee_close_cost, fee_close_currency, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
@ -142,7 +146,8 @@ def migrate_trades_and_orders_table(
|
|||||||
trading_mode, leverage, liquidation_price, is_short,
|
trading_mode, leverage, liquidation_price, is_short,
|
||||||
interest_rate, funding_fees
|
interest_rate, funding_fees
|
||||||
)
|
)
|
||||||
select id, lower(exchange), pair,
|
select id, lower(exchange), pair, {base_currency} base_currency,
|
||||||
|
{stake_currency} stake_currency,
|
||||||
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
||||||
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
|
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
|
||||||
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
|
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
|
||||||
@ -173,23 +178,6 @@ def migrate_trades_and_orders_table(
|
|||||||
set_sequence_ids(engine, order_id, trade_id)
|
set_sequence_ids(engine, order_id, trade_id)
|
||||||
|
|
||||||
|
|
||||||
def migrate_open_orders_to_trades(engine):
|
|
||||||
with engine.begin() as connection:
|
|
||||||
connection.execute(text("""
|
|
||||||
insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
|
|
||||||
select id ft_trade_id, pair ft_pair, open_order_id,
|
|
||||||
case when close_rate_requested is null then 'buy'
|
|
||||||
else 'sell' end ft_order_side, 1 ft_is_open
|
|
||||||
from trades
|
|
||||||
where open_order_id is not null
|
|
||||||
union all
|
|
||||||
select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
|
|
||||||
'stoploss' ft_order_side, 1 ft_is_open
|
|
||||||
from trades
|
|
||||||
where stoploss_order_id is not null
|
|
||||||
"""))
|
|
||||||
|
|
||||||
|
|
||||||
def drop_orders_table(engine, table_back_name: str):
|
def drop_orders_table(engine, table_back_name: str):
|
||||||
# Drop and recreate orders table as backup
|
# Drop and recreate orders table as backup
|
||||||
# This drops foreign keys, too.
|
# This drops foreign keys, too.
|
||||||
@ -230,7 +218,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
"""
|
"""
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
|
|
||||||
cols = inspector.get_columns('trades')
|
cols_trades = inspector.get_columns('trades')
|
||||||
cols_orders = inspector.get_columns('orders')
|
cols_orders = inspector.get_columns('orders')
|
||||||
tabs = get_table_names_for_table(inspector, 'trades')
|
tabs = get_table_names_for_table(inspector, 'trades')
|
||||||
table_back_name = get_backup_name(tabs, 'trades_bak')
|
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||||
@ -241,13 +229,17 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
# Migrates both trades and orders table!
|
# Migrates both trades and orders table!
|
||||||
# if ('orders' not in previous_tables
|
# if ('orders' not in previous_tables
|
||||||
# or not has_column(cols_orders, 'leverage')):
|
# or not has_column(cols_orders, 'leverage')):
|
||||||
if not has_column(cols, 'exit_order_status'):
|
if not has_column(cols_trades, 'base_currency'):
|
||||||
logger.info(f"Running database migration for trades - "
|
logger.info(f"Running database migration for trades - "
|
||||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||||
migrate_trades_and_orders_table(
|
migrate_trades_and_orders_table(
|
||||||
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders)
|
decl_base, inspector, engine, table_back_name, cols_trades,
|
||||||
|
order_table_bak_name, cols_orders)
|
||||||
|
|
||||||
if 'orders' not in previous_tables and 'trades' in previous_tables:
|
if 'orders' not in previous_tables and 'trades' in previous_tables:
|
||||||
logger.info('Moving open orders to Orders table.')
|
raise OperationalException(
|
||||||
migrate_open_orders_to_trades(engine)
|
"Your database seems to be very old. "
|
||||||
|
"Please update to freqtrade 2022.3 to migrate this database or "
|
||||||
|
"start with a fresh database.")
|
||||||
|
|
||||||
set_sqlite_to_wal(engine)
|
set_sqlite_to_wal(engine)
|
||||||
|
@ -279,6 +279,8 @@ class LocalTrade():
|
|||||||
|
|
||||||
exchange: str = ''
|
exchange: str = ''
|
||||||
pair: str = ''
|
pair: str = ''
|
||||||
|
base_currency: str = ''
|
||||||
|
stake_currency: str = ''
|
||||||
is_open: bool = True
|
is_open: bool = True
|
||||||
fee_open: float = 0.0
|
fee_open: float = 0.0
|
||||||
fee_open_cost: Optional[float] = None
|
fee_open_cost: Optional[float] = None
|
||||||
@ -397,6 +399,26 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
return "long"
|
return "long"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def safe_base_currency(self) -> str:
|
||||||
|
"""
|
||||||
|
Compatibility layer for asset - which can be empty for old trades.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.base_currency or self.pair.split('/')[0]
|
||||||
|
except IndexError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def safe_quote_currency(self) -> str:
|
||||||
|
"""
|
||||||
|
Compatibility layer for asset - which can be empty for old trades.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.stake_currency or self.pair.split('/')[1].split(':')[0]
|
||||||
|
except IndexError:
|
||||||
|
return ''
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
@ -423,6 +445,8 @@ class LocalTrade():
|
|||||||
return {
|
return {
|
||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
'pair': self.pair,
|
'pair': self.pair,
|
||||||
|
'base_currency': self.safe_base_currency,
|
||||||
|
'quote_currency': self.safe_quote_currency,
|
||||||
'is_open': self.is_open,
|
'is_open': self.is_open,
|
||||||
'exchange': self.exchange,
|
'exchange': self.exchange,
|
||||||
'amount': round(self.amount, 8),
|
'amount': round(self.amount, 8),
|
||||||
@ -1051,6 +1075,8 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
|
|
||||||
exchange = Column(String(25), nullable=False)
|
exchange = Column(String(25), nullable=False)
|
||||||
pair = Column(String(25), nullable=False, index=True)
|
pair = Column(String(25), nullable=False, index=True)
|
||||||
|
base_currency = Column(String(25), nullable=True)
|
||||||
|
stake_currency = Column(String(25), nullable=True)
|
||||||
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||||
fee_open = Column(Float, nullable=False, default=0.0)
|
fee_open = Column(Float, nullable=False, default=0.0)
|
||||||
fee_open_cost = Column(Float, nullable=True)
|
fee_open_cost = Column(Float, nullable=True)
|
||||||
|
@ -203,6 +203,8 @@ class OrderSchema(BaseModel):
|
|||||||
class TradeSchema(BaseModel):
|
class TradeSchema(BaseModel):
|
||||||
trade_id: int
|
trade_id: int
|
||||||
pair: str
|
pair: str
|
||||||
|
base_currency: str
|
||||||
|
quote_currency: str
|
||||||
is_open: bool
|
is_open: bool
|
||||||
is_short: bool
|
is_short: bool
|
||||||
exchange: str
|
exchange: str
|
||||||
|
@ -197,7 +197,6 @@ class RPC:
|
|||||||
|
|
||||||
trade_dict = trade.to_json()
|
trade_dict = trade.to_json()
|
||||||
trade_dict.update(dict(
|
trade_dict.update(dict(
|
||||||
base_currency=self._freqtrade.config['stake_currency'],
|
|
||||||
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
current_profit=current_profit, # Deprecated
|
current_profit=current_profit, # Deprecated
|
||||||
@ -223,6 +222,7 @@ class RPC:
|
|||||||
def _rpc_status_table(self, stake_currency: str,
|
def _rpc_status_table(self, stake_currency: str,
|
||||||
fiat_display_currency: str) -> Tuple[List, List, float]:
|
fiat_display_currency: str) -> Tuple[List, List, float]:
|
||||||
trades: List[Trade] = Trade.get_open_trades()
|
trades: List[Trade] = Trade.get_open_trades()
|
||||||
|
nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
|
||||||
if not trades:
|
if not trades:
|
||||||
raise RPCException('no active trade')
|
raise RPCException('no active trade')
|
||||||
else:
|
else:
|
||||||
@ -237,7 +237,7 @@ class RPC:
|
|||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
trade_profit = trade.calc_profit(current_rate)
|
trade_profit = trade.calc_profit(current_rate)
|
||||||
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
||||||
direction_str = 'S' if trade.is_short else 'L'
|
direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
|
||||||
if self._fiat_converter:
|
if self._fiat_converter:
|
||||||
fiat_profit = self._fiat_converter.convert_amount(
|
fiat_profit = self._fiat_converter.convert_amount(
|
||||||
trade_profit,
|
trade_profit,
|
||||||
@ -267,7 +267,11 @@ class RPC:
|
|||||||
if self._fiat_converter:
|
if self._fiat_converter:
|
||||||
profitcol += " (" + fiat_display_currency + ")"
|
profitcol += " (" + fiat_display_currency + ")"
|
||||||
|
|
||||||
columns = ['ID L/S', 'Pair', 'Since', profitcol]
|
columns = [
|
||||||
|
'ID L/S' if nonspot else 'ID',
|
||||||
|
'Pair',
|
||||||
|
'Since',
|
||||||
|
profitcol]
|
||||||
if self._config.get('position_adjustment_enable', False):
|
if self._config.get('position_adjustment_enable', False):
|
||||||
columns.append('# Entries')
|
columns.append('# Entries')
|
||||||
return trades_list, columns, fiat_profit_sum
|
return trades_list, columns, fiat_profit_sum
|
||||||
@ -791,7 +795,7 @@ class RPC:
|
|||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
else:
|
else:
|
||||||
return None
|
raise RPCException(f'Failed to enter position for {pair}.')
|
||||||
|
|
||||||
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
|
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
|
||||||
"""
|
"""
|
||||||
|
@ -509,7 +509,7 @@ class Telegram(RPCHandler):
|
|||||||
lines.append("*Open Order:* `{open_order}`")
|
lines.append("*Open Order:* `{open_order}`")
|
||||||
|
|
||||||
lines_detail = self._prepare_entry_details(
|
lines_detail = self._prepare_entry_details(
|
||||||
r['orders'], r['base_currency'], r['is_open'])
|
r['orders'], r['quote_currency'], r['is_open'])
|
||||||
lines.extend(lines_detail if lines_detail else "")
|
lines.extend(lines_detail if lines_detail else "")
|
||||||
|
|
||||||
# Filter empty lines using list-comprehension
|
# Filter empty lines using list-comprehension
|
||||||
|
@ -881,14 +881,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
current_rate = rate
|
current_rate = rate
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
|
||||||
if (self.exit_profit_only and current_profit <= self.exit_profit_offset):
|
if self.use_exit_signal:
|
||||||
# exit_profit_only and profit doesn't reach the offset - ignore sell signal
|
if exit_ and not enter:
|
||||||
pass
|
|
||||||
elif self.use_exit_signal and not enter:
|
|
||||||
if exit_:
|
|
||||||
exit_signal = ExitType.EXIT_SIGNAL
|
exit_signal = ExitType.EXIT_SIGNAL
|
||||||
else:
|
else:
|
||||||
trade_type = "exit_short" if trade.is_short else "sell"
|
|
||||||
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||||
pair=trade.pair, trade=trade, current_time=current_time,
|
pair=trade.pair, trade=trade, current_time=current_time,
|
||||||
current_rate=current_rate, current_profit=current_profit)
|
current_rate=current_rate, current_profit=current_profit)
|
||||||
@ -896,13 +892,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
exit_signal = ExitType.CUSTOM_EXIT
|
exit_signal = ExitType.CUSTOM_EXIT
|
||||||
if isinstance(custom_reason, str):
|
if isinstance(custom_reason, str):
|
||||||
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
||||||
logger.warning(f'Custom {trade_type} reason returned from '
|
logger.warning(f'Custom exit reason returned from '
|
||||||
f'custom_exit is too long and was trimmed'
|
f'custom_exit is too long and was trimmed'
|
||||||
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
||||||
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
||||||
else:
|
else:
|
||||||
custom_reason = None
|
custom_reason = None
|
||||||
if exit_signal in (ExitType.CUSTOM_EXIT, ExitType.EXIT_SIGNAL):
|
if (
|
||||||
|
exit_signal == ExitType.CUSTOM_EXIT
|
||||||
|
or (exit_signal == ExitType.EXIT_SIGNAL
|
||||||
|
and (not self.exit_profit_only or current_profit > self.exit_profit_offset))
|
||||||
|
):
|
||||||
logger.debug(f"{trade.pair} - Sell signal received. "
|
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||||
f"exit_type=ExitType.{exit_signal.name}" +
|
f"exit_type=ExitType.{exit_signal.name}" +
|
||||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||||
|
@ -7,6 +7,7 @@ coveralls==3.3.1
|
|||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.6.0
|
flake8-tidy-imports==4.6.0
|
||||||
mypy==0.942
|
mypy==0.942
|
||||||
|
pre-commit==2.18.1
|
||||||
pytest==7.1.1
|
pytest==7.1.1
|
||||||
pytest-asyncio==0.18.3
|
pytest-asyncio==0.18.3
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
|
8
setup.sh
8
setup.sh
@ -51,6 +51,7 @@ function updateenv() {
|
|||||||
echo "pip install in-progress. Please wait..."
|
echo "pip install in-progress. Please wait..."
|
||||||
${PYTHON} -m pip install --upgrade pip
|
${PYTHON} -m pip install --upgrade pip
|
||||||
read -p "Do you want to install dependencies for dev [y/N]? "
|
read -p "Do you want to install dependencies for dev [y/N]? "
|
||||||
|
dev=$REPLY
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
REQUIREMENTS=requirements-dev.txt
|
REQUIREMENTS=requirements-dev.txt
|
||||||
@ -88,6 +89,13 @@ function updateenv() {
|
|||||||
fi
|
fi
|
||||||
echo "pip install completed"
|
echo "pip install completed"
|
||||||
echo
|
echo
|
||||||
|
if [[ $dev =~ ^[Yy]$ ]]; then
|
||||||
|
${PYTHON} -m pre-commit install
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed installing pre-commit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
|
@ -52,7 +52,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
assert results[0] == {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'ETH',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
'is_open': ANY,
|
'is_open': ANY,
|
||||||
@ -135,7 +136,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
assert results[0] == {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'ETH',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
'is_open': ANY,
|
'is_open': ANY,
|
||||||
@ -1230,8 +1232,8 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
|||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
pair = 'TKN/BTC'
|
pair = 'TKN/BTC'
|
||||||
|
with pytest.raises(RPCException, match=r"Failed to enter position for TKN/BTC."):
|
||||||
trade = rpc._rpc_force_entry(pair, None)
|
trade = rpc._rpc_force_entry(pair, None)
|
||||||
assert trade is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_force_entry_stopped(mocker, default_conf) -> None:
|
def test_rpc_force_entry_stopped(mocker, default_conf) -> None:
|
||||||
|
@ -931,6 +931,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'open_order': None,
|
'open_order': None,
|
||||||
'open_rate': 0.123,
|
'open_rate': 0.123,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
|
'base_currency': 'ETH',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stop_loss_abs': ANY,
|
'stop_loss_abs': ANY,
|
||||||
'stop_loss_pct': ANY,
|
'stop_loss_pct': ANY,
|
||||||
@ -1097,7 +1099,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
|
|
||||||
# Test creating trade
|
# Test creating trade
|
||||||
fbuy_mock = MagicMock(return_value=Trade(
|
fbuy_mock = MagicMock(return_value=Trade(
|
||||||
pair='ETH/ETH',
|
pair='ETH/BTC',
|
||||||
amount=1,
|
amount=1,
|
||||||
amount_requested=1,
|
amount_requested=1,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
@ -1130,7 +1132,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
'open_rate': 0.245441,
|
'open_rate': 0.245441,
|
||||||
'pair': 'ETH/ETH',
|
'pair': 'ETH/BTC',
|
||||||
|
'base_currency': 'ETH',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'stake_amount': 1,
|
'stake_amount': 1,
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
|
@ -184,7 +184,8 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
_rpc_trade_status=MagicMock(return_value=[{
|
_rpc_trade_status=MagicMock(return_value=[{
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'ETH',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'open_date': arrow.utcnow(),
|
'open_date': arrow.utcnow(),
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
@ -398,8 +399,8 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
||||||
|
|
||||||
assert int(fields[0]) == 1
|
assert int(fields[0]) == 1
|
||||||
assert 'L' in fields[1]
|
# assert 'L' in fields[1]
|
||||||
assert 'ETH/BTC' in fields[2]
|
assert 'ETH/BTC' in fields[1]
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -1253,7 +1254,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
|
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
|
||||||
assert keyboard[-1][0].text == "Cancel"
|
assert keyboard[-1][0].text == "Cancel"
|
||||||
|
|
||||||
assert keyboard[1][0].callback_data == 'force_exit__2 L'
|
assert keyboard[1][0].callback_data == 'force_exit__2 '
|
||||||
update = MagicMock()
|
update = MagicMock()
|
||||||
update.callback_query = MagicMock()
|
update.callback_query = MagicMock()
|
||||||
update.callback_query.data = keyboard[1][0].callback_data
|
update.callback_query.data = keyboard[1][0].callback_data
|
||||||
|
@ -523,7 +523,7 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
|
|||||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
assert res.exit_type == ExitType.CUSTOM_EXIT
|
||||||
assert res.exit_flag is True
|
assert res.exit_flag is True
|
||||||
assert res.exit_reason == 'h' * 64
|
assert res.exit_reason == 'h' * 64
|
||||||
assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog)
|
assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('side', TRADE_SIDES)
|
@pytest.mark.parametrize('side', TRADE_SIDES)
|
||||||
|
@ -18,7 +18,8 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti
|
|||||||
process_removed_setting,
|
process_removed_setting,
|
||||||
process_temporary_deprecated_settings)
|
process_temporary_deprecated_settings)
|
||||||
from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict
|
from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict
|
||||||
from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range
|
from freqtrade.configuration.load_config import (load_config_file, load_file, load_from_files,
|
||||||
|
log_config_error_range)
|
||||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -206,6 +207,32 @@ def test_from_config(default_conf, mocker, caplog) -> None:
|
|||||||
assert isinstance(validated_conf['user_data_dir'], Path)
|
assert isinstance(validated_conf['user_data_dir'], Path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_recursive_files(testdatadir) -> None:
|
||||||
|
files = testdatadir / "testconfigs/testconfig.json"
|
||||||
|
|
||||||
|
conf = Configuration.from_files([files])
|
||||||
|
|
||||||
|
assert conf
|
||||||
|
# Exchange comes from "the first config"
|
||||||
|
assert conf['exchange']
|
||||||
|
# Pricing comes from the 2nd config
|
||||||
|
assert conf['entry_pricing']
|
||||||
|
assert conf['entry_pricing']['price_side'] == "same"
|
||||||
|
assert conf['exit_pricing']
|
||||||
|
# The other key comes from pricing2, which is imported by pricing.json
|
||||||
|
assert conf['exit_pricing']['price_side'] == "other"
|
||||||
|
|
||||||
|
assert len(conf['config_files']) == 4
|
||||||
|
assert 'testconfig.json' in conf['config_files'][0]
|
||||||
|
assert 'test_pricing_conf.json' in conf['config_files'][1]
|
||||||
|
assert 'test_base_config.json' in conf['config_files'][2]
|
||||||
|
assert 'test_pricing2_conf.json' in conf['config_files'][3]
|
||||||
|
|
||||||
|
files = testdatadir / "testconfigs/recursive.json"
|
||||||
|
with pytest.raises(OperationalException, match="Config loop detected."):
|
||||||
|
load_from_files([files])
|
||||||
|
|
||||||
|
|
||||||
def test_print_config(default_conf, mocker, caplog) -> None:
|
def test_print_config(default_conf, mocker, caplog) -> None:
|
||||||
conf1 = deepcopy(default_conf)
|
conf1 = deepcopy(default_conf)
|
||||||
# Delete non-json elements from default_conf
|
# Delete non-json elements from default_conf
|
||||||
|
@ -3663,6 +3663,7 @@ def test_exit_profit_only(
|
|||||||
})
|
})
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
|
freqtrade.strategy.custom_exit = MagicMock(return_value=None)
|
||||||
if exit_type == ExitType.EXIT_SIGNAL.value:
|
if exit_type == ExitType.EXIT_SIGNAL.value:
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
else:
|
else:
|
||||||
@ -3671,10 +3672,15 @@ def test_exit_profit_only(
|
|||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.is_short = is_short
|
assert trade.is_short == is_short
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
|
if profit_only:
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
# Custom-exit is called
|
||||||
|
freqtrade.strategy.custom_exit.call_count == 1
|
||||||
|
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short)
|
patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short)
|
||||||
assert freqtrade.handle_trade(trade) is handle_first
|
assert freqtrade.handle_trade(trade) is handle_first
|
||||||
|
|
||||||
|
@ -1209,6 +1209,27 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);"""
|
);"""
|
||||||
|
create_table_order = """CREATE TABLE orders (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
ft_trade_id INTEGER,
|
||||||
|
ft_order_side VARCHAR(25) NOT NULL,
|
||||||
|
ft_pair VARCHAR(25) NOT NULL,
|
||||||
|
ft_is_open BOOLEAN NOT NULL,
|
||||||
|
order_id VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(255),
|
||||||
|
symbol VARCHAR(25),
|
||||||
|
order_type VARCHAR(50),
|
||||||
|
side VARCHAR(25),
|
||||||
|
price FLOAT,
|
||||||
|
amount FLOAT,
|
||||||
|
filled FLOAT,
|
||||||
|
remaining FLOAT,
|
||||||
|
cost FLOAT,
|
||||||
|
order_date DATETIME,
|
||||||
|
order_filled_date DATETIME,
|
||||||
|
order_update_date DATETIME,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);"""
|
||||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||||
open_rate, stake_amount, amount, open_date,
|
open_rate, stake_amount, amount, open_date,
|
||||||
stop_loss, initial_stop_loss, max_rate, ticker_interval,
|
stop_loss, initial_stop_loss, max_rate, ticker_interval,
|
||||||
@ -1222,15 +1243,66 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
stake=default_conf.get("stake_amount"),
|
stake=default_conf.get("stake_amount"),
|
||||||
amount=amount
|
amount=amount
|
||||||
)
|
)
|
||||||
|
insert_orders = f"""
|
||||||
|
insert into orders (
|
||||||
|
ft_trade_id,
|
||||||
|
ft_order_side,
|
||||||
|
ft_pair,
|
||||||
|
ft_is_open,
|
||||||
|
order_id,
|
||||||
|
status,
|
||||||
|
symbol,
|
||||||
|
order_type,
|
||||||
|
side,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
filled,
|
||||||
|
remaining,
|
||||||
|
cost)
|
||||||
|
values (
|
||||||
|
1,
|
||||||
|
'buy',
|
||||||
|
'ETC/BTC',
|
||||||
|
0,
|
||||||
|
'buy_order',
|
||||||
|
'closed',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'stoploss',
|
||||||
|
'ETC/BTC',
|
||||||
|
0,
|
||||||
|
'stop_order_id222',
|
||||||
|
'closed',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'sell',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
)
|
||||||
|
"""
|
||||||
engine = create_engine('sqlite://')
|
engine = create_engine('sqlite://')
|
||||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(create_table_old))
|
connection.execute(text(create_table_old))
|
||||||
|
connection.execute(text(create_table_order))
|
||||||
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
||||||
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
||||||
connection.execute(text(insert_table_old))
|
connection.execute(text(insert_table_old))
|
||||||
|
connection.execute(text(insert_orders))
|
||||||
|
|
||||||
# fake previous backup
|
# fake previous backup
|
||||||
connection.execute(text("create table trades_bak as select * from trades"))
|
connection.execute(text("create table trades_bak as select * from trades"))
|
||||||
@ -1267,8 +1339,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
assert trade.open_trade_value == trade._calc_open_trade_value()
|
assert trade.open_trade_value == trade._calc_open_trade_value()
|
||||||
assert trade.close_profit_abs is None
|
assert trade.close_profit_abs is None
|
||||||
|
|
||||||
assert log_has("Moving open orders to Orders table.", caplog)
|
orders = trade.orders
|
||||||
orders = Order.query.all()
|
|
||||||
assert len(orders) == 2
|
assert len(orders) == 2
|
||||||
assert orders[0].order_id == 'buy_order'
|
assert orders[0].order_id == 'buy_order'
|
||||||
assert orders[0].ft_order_side == 'buy'
|
assert orders[0].ft_order_side == 'buy'
|
||||||
@ -1277,7 +1348,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
assert orders[1].ft_order_side == 'stoploss'
|
assert orders[1].ft_order_side == 'stoploss'
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
@ -1301,6 +1372,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
|||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);"""
|
);"""
|
||||||
|
|
||||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||||
open_rate, stake_amount, amount, open_date)
|
open_rate, stake_amount, amount, open_date)
|
||||||
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||||
@ -1319,27 +1391,9 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
|||||||
connection.execute(text(insert_table_old))
|
connection.execute(text(insert_table_old))
|
||||||
|
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
|
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
||||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
|
||||||
assert trade.fee_open == fee.return_value
|
|
||||||
assert trade.fee_close == fee.return_value
|
|
||||||
assert trade.open_rate_requested is None
|
|
||||||
assert trade.close_rate_requested is None
|
|
||||||
assert trade.is_open == 1
|
|
||||||
assert trade.amount == amount
|
|
||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
|
||||||
assert trade.pair == "ETC/BTC"
|
|
||||||
assert trade.exchange == "binance"
|
|
||||||
assert trade.max_rate == 0.0
|
|
||||||
assert trade.stop_loss == 0.0
|
|
||||||
assert trade.initial_stop_loss == 0.0
|
|
||||||
assert trade.open_trade_value == trade._calc_open_trade_value()
|
|
||||||
assert log_has("trying trades_bak0", caplog)
|
|
||||||
assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0",
|
|
||||||
caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_get_last_sequence_ids():
|
def test_migrate_get_last_sequence_ids():
|
||||||
engine = MagicMock()
|
engine = MagicMock()
|
||||||
@ -1561,6 +1615,8 @@ def test_to_json(fee):
|
|||||||
|
|
||||||
assert result == {'trade_id': None,
|
assert result == {'trade_id': None,
|
||||||
'pair': 'ADA/USDT',
|
'pair': 'ADA/USDT',
|
||||||
|
'base_currency': 'ADA',
|
||||||
|
'quote_currency': 'USDT',
|
||||||
'is_open': None,
|
'is_open': None,
|
||||||
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
@ -1637,6 +1693,8 @@ def test_to_json(fee):
|
|||||||
|
|
||||||
assert result == {'trade_id': None,
|
assert result == {'trade_id': None,
|
||||||
'pair': 'XRP/BTC',
|
'pair': 'XRP/BTC',
|
||||||
|
'base_currency': 'XRP',
|
||||||
|
'quote_currency': 'BTC',
|
||||||
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
|
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
6
tests/testdata/testconfigs/recursive.json
vendored
Normal file
6
tests/testdata/testconfigs/recursive.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
// This file fails as it's loading itself over and over
|
||||||
|
"add_config_files": [
|
||||||
|
"./recursive.json"
|
||||||
|
]
|
||||||
|
}
|
12
tests/testdata/testconfigs/test_base_config.json
vendored
Normal file
12
tests/testdata/testconfigs/test_base_config.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"stake_currency": "",
|
||||||
|
"dry_run": true,
|
||||||
|
"exchange": {
|
||||||
|
"name": "",
|
||||||
|
"key": "",
|
||||||
|
"secret": "",
|
||||||
|
"pair_whitelist": [],
|
||||||
|
"ccxt_async_config": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
tests/testdata/testconfigs/test_pricing2_conf.json
vendored
Normal file
18
tests/testdata/testconfigs/test_pricing2_conf.json
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing":{
|
||||||
|
"price_side": "other",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
|
}
|
||||||
|
}
|
21
tests/testdata/testconfigs/test_pricing_conf.json
vendored
Normal file
21
tests/testdata/testconfigs/test_pricing_conf.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing":{
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"add_config_files": [
|
||||||
|
"./test_pricing2_conf.json"
|
||||||
|
]
|
||||||
|
}
|
6
tests/testdata/testconfigs/testconfig.json
vendored
Normal file
6
tests/testdata/testconfigs/testconfig.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"add_config_files": [
|
||||||
|
"test_base_config.json",
|
||||||
|
"test_pricing_conf.json"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user