Merge branch 'develop' into pr/jpribyl/3210-1

This commit is contained in:
Matthias
2020-05-16 13:09:38 +02:00
61 changed files with 1459 additions and 413 deletions

View File

@@ -19,7 +19,8 @@ from freqtrade.commands.list_commands import (start_list_exchanges,
start_list_hyperopts,
start_list_markets,
start_list_strategies,
start_list_timeframes)
start_list_timeframes,
start_show_trades)
from freqtrade.commands.optimize_commands import (start_backtesting,
start_edge, start_hyperopt)
from freqtrade.commands.pairlist_commands import start_test_pairlist

View File

@@ -64,6 +64,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "ticker_interval"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
"hyperopt_list_min_trades", "hyperopt_list_max_trades",
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
@@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies",
"list-hyperopts", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "plot-profit"]
"plot-dataframe", "plot-profit", "show-trades"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
@@ -163,7 +165,7 @@ class Arguments:
start_list_markets, start_list_strategies,
start_list_timeframes, start_new_config,
start_new_hyperopt, start_new_strategy,
start_plot_dataframe, start_plot_profit,
start_plot_dataframe, start_plot_profit, start_show_trades,
start_backtesting, start_hyperopt, start_edge,
start_test_pairlist, start_trading)
@@ -330,6 +332,15 @@ class Arguments:
plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
# Add show-trades subcommand
show_trades = subparsers.add_parser(
'show-trades',
help='Show trades.',
parents=[_common_parser],
)
show_trades.set_defaults(func=start_show_trades)
self._build_args(optionlist=ARGS_SHOW_TRADES, parser=show_trades)
# Add hyperopt-list subcommand
hyperopt_list_cmd = subparsers.add_parser(
'hyperopt-list',

View File

@@ -222,7 +222,7 @@ AVAILABLE_CLI_OPTIONS = {
),
"print_json": Arg(
'--print-json',
help='Print best result detailization in JSON format.',
help='Print output in JSON format.',
action='store_true',
default=False,
),
@@ -430,6 +430,11 @@ AVAILABLE_CLI_OPTIONS = {
choices=["DB", "file"],
default="file",
),
"trade_ids": Arg(
'--trade-ids',
help='Specify the list of trade ids.',
nargs='+',
),
# hyperopt-list, hyperopt-show
"hyperopt_list_profitable": Arg(
'--profitable',

View File

@@ -197,3 +197,30 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
args.get('list_pairs_print_json', False) or
args.get('print_csv', False)):
print(f"{summary_str}.")
def start_show_trades(args: Dict[str, Any]) -> None:
"""
Show trades
"""
from freqtrade.persistence import init, Trade
import json
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config:
raise OperationalException("--db-url is required for this command.")
logger.info(f'Using DB: "{config["db_url"]}"')
init(config['db_url'], clean_open_orders=False)
tfilter = []
if config.get('trade_ids'):
tfilter.append(Trade.id.in_(config['trade_ids']))
trades = Trade.get_trades(tfilter).all()
logger.info(f"Printing {len(trades)} Trades: ")
if config.get('print_json', False):
print(json.dumps([trade.to_json() for trade in trades], indent=4))
else:
for trade in trades:
print(trade)

View File

@@ -18,6 +18,9 @@ def start_trading(args: Dict[str, Any]) -> int:
try:
worker = Worker(args)
worker.run()
except Exception as e:
logger.error(str(e))
logger.exception("Fatal exception!")
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
finally:

View File

@@ -356,8 +356,12 @@ class Configuration:
self._args_to_config(config, argname='indicators2',
logstring='Using indicators2: {}')
self._args_to_config(config, argname='trade_ids',
logstring='Filtering on trade_ids: {}')
self._args_to_config(config, argname='plot_limit',
logstring='Limiting plot to: {}')
self._args_to_config(config, argname='trade_source',
logstring='Using trades from: {}')

View File

@@ -24,6 +24,9 @@ AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
DRY_RUN_WALLET = 1000
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
# it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies'

View File

@@ -1,14 +1,17 @@
"""
Functions to convert data from one format to another
"""
import itertools
import logging
from datetime import datetime, timezone
from typing import Any, Dict
from operator import itemgetter
from typing import Any, Dict, List
import pandas as pd
from pandas import DataFrame, to_datetime
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
DEFAULT_TRADES_COLUMNS)
logger = logging.getLogger(__name__)
@@ -154,7 +157,27 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
return frame
def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame:
def trades_remove_duplicates(trades: List[List]) -> List[List]:
"""
Removes duplicates from the trades list.
Uses itertools.groupby to avoid converting to pandas.
Tests show it as being pretty efficient on lists of 4M Lists.
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
:return: same format as above, but with duplicates removed
"""
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
def trades_dict_to_list(trades: List[Dict]) -> List[List]:
"""
Convert fetch_trades result into a List (to be more memory efficient).
:param trades: List of trades, as returned by ccxt.fetch_trades.
:return: List of Lists, with constants.DEFAULT_TRADES_COLUMNS as columns
"""
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
"""
Converts trades list to OHLCV list
TODO: This should get a dedicated test
@@ -164,9 +187,10 @@ def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame:
"""
from freqtrade.exchange import timeframe_to_minutes
timeframe_minutes = timeframe_to_minutes(timeframe)
df = pd.DataFrame(trades)
df['datetime'] = pd.to_datetime(df['datetime'])
df = df.set_index('datetime')
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
utc=True,)
df = df.set_index('timestamp')
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()

View File

@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame
from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.state import RunMode
@@ -18,9 +19,10 @@ logger = logging.getLogger(__name__)
class DataProvider:
def __init__(self, config: dict, exchange: Exchange) -> None:
def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None:
self._config = config
self._exchange = exchange
self._pairlists = pairlists
def refresh(self,
pairlist: List[Tuple[str, str]],
@@ -95,10 +97,14 @@ class DataProvider:
def ticker(self, pair: str):
"""
Return last ticker data
Return last ticker data from exchange
:param pair: Pair to get the data for
:return: Ticker dict from exchange or empty dict if ticker is not available for the pair
"""
# TODO: Implement me
pass
try:
return self._exchange.fetch_ticker(pair)
except DependencyException:
return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
"""
@@ -116,3 +122,17 @@ class DataProvider:
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
"""
return RunMode(self._config.get('runmode', RunMode.OTHER))
def current_whitelist(self) -> List[str]:
"""
fetch latest available whitelist.
Useful when you have a large whitelist and need to call each pair as an informative pair.
As available pairs does not show whitelist until after informative pairs have been cached.
:return: list of pairs in whitelist
"""
if self._pairlists:
return self._pairlists.whitelist
else:
raise OperationalException("Dataprovider was not initialized with a pairlist provider.")

View File

@@ -9,10 +9,13 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import ohlcv_to_dataframe, trades_to_ohlcv
from freqtrade.data.converter import (ohlcv_to_dataframe,
trades_remove_duplicates,
trades_to_ohlcv)
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import format_ms_time
logger = logging.getLogger(__name__)
@@ -257,27 +260,40 @@ def _download_trades_history(exchange: Exchange,
"""
try:
since = timerange.startts * 1000 if timerange and timerange.starttype == 'date' else None
since = timerange.startts * 1000 if \
(timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000
trades = data_handler.trades_load(pair)
from_id = trades[-1]['id'] if trades else None
# TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None')
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None')
from_id = trades[-1][1] if trades else None
if trades and since < trades[-1][0]:
# Reset since to the last available point
# - 5 seconds (to ensure we're getting all trades)
since = trades[-1][0] - (5 * 1000)
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
f"since: {format_ms_time(since)}.")
logger.debug(f"Current Start: {format_ms_time(trades[0][0]) if trades else 'None'}")
logger.debug(f"Current End: {format_ms_time(trades[-1][0]) if trades else 'None'}")
logger.info(f"Current Amount of trades: {len(trades)}")
# Default since_ms to 30 days if nothing is given
new_trades = exchange.get_historic_trades(pair=pair,
since=since if since else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000,
since=since,
from_id=from_id,
)
trades.extend(new_trades[1])
# Remove duplicates to make sure we're not storing data we don't need
trades = trades_remove_duplicates(trades)
data_handler.trades_store(pair, data=trades)
logger.debug("New Start: %s", trades[0]['datetime'])
logger.debug("New End: %s", trades[-1]['datetime'])
logger.debug(f"New Start: {format_ms_time(trades[0][0])}")
logger.debug(f"New End: {format_ms_time(trades[-1][0])}")
logger.info(f"New Amount of trades: {len(trades)}")
return True

View File

@@ -8,16 +8,20 @@ from abc import ABC, abstractclassmethod, abstractmethod
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Type
from typing import List, Optional, Type
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.data.converter import clean_ohlcv_dataframe, trim_dataframe
from freqtrade.data.converter import (clean_ohlcv_dataframe,
trades_remove_duplicates, trim_dataframe)
from freqtrade.exchange import timeframe_to_seconds
logger = logging.getLogger(__name__)
# Type for trades list
TradeList = List[List]
class IDataHandler(ABC):
@@ -89,23 +93,25 @@ class IDataHandler(ABC):
"""
@abstractmethod
def trades_store(self, pair: str, data: List[Dict]) -> None:
def trades_store(self, pair: str, data: TradeList) -> None:
"""
Store trades data (list of Dicts) to file
:param pair: Pair - used for filename
:param data: List of Dicts containing trade data
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
@abstractmethod
def trades_append(self, pair: str, data: List[Dict]):
def trades_append(self, pair: str, data: TradeList):
"""
Append data to existing files
:param pair: Pair - used for filename
:param data: List of Dicts containing trade data
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
@abstractmethod
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> List[Dict]:
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
"""
Load a pair from file, either .json.gz or .json
:param pair: Load trades for this pair
@@ -121,6 +127,16 @@ class IDataHandler(ABC):
:return: True when deleted, false if file did not exist.
"""
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
"""
Load a pair from file, either .json.gz or .json
Removes duplicates in the process.
:param pair: Load trades for this pair
:param timerange: Timerange to load trades for - currently not implemented
:return: List of trades
"""
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
def ohlcv_load(self, pair, timeframe: str,
timerange: Optional[TimeRange] = None,
fill_missing: bool = True,

View File

@@ -1,6 +1,7 @@
import logging
import re
from pathlib import Path
from typing import Dict, List, Optional
from typing import List, Optional
import numpy as np
from pandas import DataFrame, read_json, to_datetime
@@ -8,8 +9,11 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import trades_dict_to_list
from .idatahandler import IDataHandler
from .idatahandler import IDataHandler, TradeList
logger = logging.getLogger(__name__)
class JsonDataHandler(IDataHandler):
@@ -113,24 +117,26 @@ class JsonDataHandler(IDataHandler):
# Check if regex found something and only return these results to avoid exceptions.
return [match[0].replace('_', '/') for match in _tmp if match]
def trades_store(self, pair: str, data: List[Dict]) -> None:
def trades_store(self, pair: str, data: TradeList) -> None:
"""
Store trades data (list of Dicts) to file
:param pair: Pair - used for filename
:param data: List of Dicts containing trade data
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
filename = self._pair_trades_filename(self._datadir, pair)
misc.file_dump_json(filename, data, is_zip=self._use_zip)
def trades_append(self, pair: str, data: List[Dict]):
def trades_append(self, pair: str, data: TradeList):
"""
Append data to existing files
:param pair: Pair - used for filename
:param data: List of Dicts containing trade data
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
raise NotImplementedError()
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> List[Dict]:
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
"""
Load a pair from file, either .json.gz or .json
# TODO: respect timerange ...
@@ -140,9 +146,15 @@ class JsonDataHandler(IDataHandler):
"""
filename = self._pair_trades_filename(self._datadir, pair)
tradesdata = misc.file_load_json(filename)
if not tradesdata:
return []
if isinstance(tradesdata[0], dict):
# Convert trades dict to list
logger.info("Old trades format detected - converting")
tradesdata = trades_dict_to_list(tradesdata)
pass
return tradesdata
def trades_purge(self, pair: str) -> bool:

View File

@@ -238,20 +238,9 @@ class Edge:
:param result Dataframe
:return: result Dataframe
"""
# stake and fees
# stake = 0.015
# 0.05% is 0.0005
# fee = 0.001
# we set stake amount to an arbitrary amount.
# as it doesn't change the calculation.
# all returned values are relative.
# they are defined as ratios.
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
# All returned values are relative, they are defined as ratios.
stake = 0.015
fee = self.fee
open_fee = fee / 2
close_fee = fee / 2
result['trade_duration'] = result['close_time'] - result['open_time']
@@ -262,12 +251,12 @@ class Edge:
# Buy Price
result['buy_vol'] = stake / result['open_rate'] # How many target are we buying
result['buy_fee'] = stake * open_fee
result['buy_fee'] = stake * self.fee
result['buy_spend'] = stake + result['buy_fee'] # How much we're spending
# Sell price
result['sell_sum'] = result['buy_vol'] * result['close_rate']
result['sell_fee'] = result['sell_sum'] * close_fee
result['sell_fee'] = result['sell_sum'] * self.fee
result['sell_take'] = result['sell_sum'] - result['sell_fee']
# profit_ratio

View File

@@ -72,7 +72,7 @@ class Binance(Exchange):
rate = self.price_to_precision(pair, rate)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, price=stop_price, params=params)
amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order

View File

@@ -18,12 +18,11 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
TRUNCATE, decimal_to_precision)
from pandas import DataFrame
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts
from freqtrade.misc import deep_merge_dicts, safe_value_fallback
CcxtModuleType = Any
@@ -472,26 +471,31 @@ class Exchange:
'pair': pair,
'price': rate,
'amount': _amount,
"cost": _amount * rate,
'cost': _amount * rate,
'type': ordertype,
'side': side,
'remaining': _amount,
'datetime': arrow.utcnow().isoformat(),
'status': "closed" if ordertype == "market" else "open",
'fee': None,
"info": {}
'info': {}
}
self._store_dry_order(dry_order)
self._store_dry_order(dry_order, pair)
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order
def _store_dry_order(self, dry_order: Dict) -> None:
def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
closed_order = dry_order.copy()
if closed_order["type"] in ["market", "limit"]:
if closed_order['type'] in ["market", "limit"]:
closed_order.update({
"status": "closed",
"filled": closed_order["amount"],
"remaining": 0
'status': 'closed',
'filled': closed_order['amount'],
'remaining': 0,
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
}
})
if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]})
@@ -769,7 +773,7 @@ class Exchange:
@retrier_async
async def _async_fetch_trades(self, pair: str,
since: Optional[int] = None,
params: Optional[dict] = None) -> List[Dict]:
params: Optional[dict] = None) -> List[List]:
"""
Asyncronously gets trade history using fetch_trades.
Handles exchange errors, does one call to the exchange.
@@ -789,7 +793,7 @@ class Exchange:
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
)
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
return trades
return trades_dict_to_list(trades)
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical trade data.'
@@ -803,7 +807,7 @@ class Exchange:
async def _async_get_trade_history_id(self, pair: str,
until: int,
since: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
from_id: Optional[str] = None) -> Tuple[str, List[List]]:
"""
Asyncronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`)
@@ -814,7 +818,7 @@ class Exchange:
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
trades: List[List] = []
if not from_id:
# Fetch first elements using timebased method to get an ID to paginate on
@@ -823,7 +827,9 @@ class Exchange:
# e.g. Binance returns the "last 1000" candles within a 1h time interval
# - so we will miss the first trades.
t = await self._async_fetch_trades(pair, since=since)
from_id = t[-1]['id']
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
from_id = t[-1][1]
trades.extend(t[:-1])
while True:
t = await self._async_fetch_trades(pair,
@@ -831,21 +837,21 @@ class Exchange:
if len(t):
# Skip last id since its the key for the next call
trades.extend(t[:-1])
if from_id == t[-1]['id'] or t[-1]['timestamp'] > until:
if from_id == t[-1][1] or t[-1][0] > until:
logger.debug(f"Stopping because from_id did not change. "
f"Reached {t[-1]['timestamp']} > {until}")
f"Reached {t[-1][0]} > {until}")
# Reached the end of the defined-download period - add last trade as well.
trades.extend(t[-1:])
break
from_id = t[-1]['id']
from_id = t[-1][1]
else:
break
return (pair, trades)
async def _async_get_trade_history_time(self, pair: str, until: int,
since: Optional[int] = None) -> Tuple[str, List]:
since: Optional[int] = None) -> Tuple[str, List[List]]:
"""
Asyncronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`)
@@ -855,16 +861,18 @@ class Exchange:
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
trades: List[List] = []
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
while True:
t = await self._async_fetch_trades(pair, since=since)
if len(t):
since = t[-1]['timestamp']
since = t[-1][1]
trades.extend(t)
# Reached the end of the defined-download period
if until and t[-1]['timestamp'] > until:
if until and t[-1][0] > until:
logger.debug(
f"Stopping because until was reached. {t[-1]['timestamp']} > {until}")
f"Stopping because until was reached. {t[-1][0]} > {until}")
break
else:
break
@@ -874,7 +882,7 @@ class Exchange:
async def _async_get_trade_history(self, pair: str,
since: Optional[int] = None,
until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
from_id: Optional[str] = None) -> Tuple[str, List[List]]:
"""
Async wrapper handling downloading trades using either time or id based methods.
"""
@@ -1041,9 +1049,9 @@ class Exchange:
return matched_trades
except ccxt.NetworkError as e:
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}') from e
f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@@ -1063,6 +1071,61 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
@staticmethod
def order_has_fee(order: Dict) -> bool:
"""
Verifies if the passed in order dict has the needed keys to extract fees,
and that these keys (currency, cost) are not empty.
:param order: Order or trade (one trade) dict
:return: True if the fee substructure contains currency and cost, false otherwise
"""
if not isinstance(order, dict):
return False
return ('fee' in order and order['fee'] is not None
and (order['fee'].keys() >= {'currency', 'cost'})
and order['fee']['currency'] is not None
and order['fee']['cost'] is not None
)
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
"""
Calculate fee rate if it's not given by the exchange.
:param order: Order or trade (one trade) dict
"""
if order['fee'].get('rate') is not None:
return order['fee'].get('rate')
fee_curr = order['fee']['currency']
# Calculate fee based on order details
if fee_curr in self.get_pair_base_currency(order['symbol']):
# Base currency - divide by amount
return round(
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost
return round(order['fee']['cost'] / order['cost'], 8)
else:
# If Fee currency is a different currency
try:
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask')
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
except DependencyException:
return None
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
"""
Extract tuple of cost, currency, rate.
Requires order_has_fee to run first!
:param order: Order or trade (one trade) dict
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (order['fee']['cost'],
order['fee']['currency'],
self.calculate_fee_rate(order))
# calculate rate ? (order['fee']['cost'] / (order['amount'] * order['price']))
def is_exchange_bad(exchange_name: str) -> bool:
return exchange_name in BAD_EXCHANGES

View File

@@ -54,8 +54,11 @@ class FreqtradeBot:
# Init objects
self.config = config
self._sell_rate_cache = TTLCache(maxsize=100, ttl=5)
self._buy_rate_cache = TTLCache(maxsize=100, ttl=5)
# Cache values for 1800 to avoid frequent polling of the exchange for prices
# Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration.
self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
@@ -68,15 +71,15 @@ class FreqtradeBot:
self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange)
self.pairlists = PairListManager(self.exchange, self.config)
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
# Attach Dataprovider to Strategy baseclass
IStrategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets
self.pairlists = PairListManager(self.exchange, self.config)
# Initializing Edge only if enabled
self.edge = Edge(self.config, self.exchange, self.strategy) if \
self.config.get('edge', {}).get('enabled', False) else None
@@ -620,7 +623,7 @@ class FreqtradeBot:
trades_closed += 1
continue
# Check if we can sell our current pair
if trade.open_order_id is None and self.handle_trade(trade):
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
trades_closed += 1
except DependencyException as exception:
@@ -762,7 +765,7 @@ class FreqtradeBot:
# We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(stoploss_order)
self.update_trade_state(trade, stoploss_order, sl_order=True)
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval']))
@@ -922,7 +925,8 @@ class FreqtradeBot:
"""
was_trade_fully_canceled = False
if order['status'] != 'canceled':
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('canceled', 'closed'):
reason = constants.CANCEL_REASON['TIMEOUT']
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
@@ -933,7 +937,10 @@ class FreqtradeBot:
logger.info('Buy order %s for %s.', reason, trade)
if safe_value_fallback(corder, order, 'remaining', 'remaining') == order['amount']:
# Using filled to determine the filled amount
filled_amount = safe_value_fallback(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
@@ -945,8 +952,7 @@ class FreqtradeBot:
# cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys.
trade.amount = order['amount'] - safe_value_fallback(corder, order,
'remaining', 'remaining')
trade.amount = filled_amount
trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, corder, trade.amount)
@@ -966,11 +972,15 @@ class FreqtradeBot:
Sell cancel - cancel order and update trade
:return: Reason for cancel
"""
# if trade is not partially completed, just cancel the trade
# if trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order):
# if trade is not partially completed, just delete the trade
self.exchange.cancel_order(trade.open_order_id, trade.pair)
try:
# if trade is not partially completed, just delete the order
self.exchange.cancel_order(trade.open_order_id, trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade)
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@@ -1014,7 +1024,7 @@ class FreqtradeBot:
if wallet_amount >= amount:
return amount
elif wallet_amount > amount * 0.98:
logger.info(f"{pair} - Falling back to wallet-amount.")
logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.")
return wallet_amount
else:
raise DependencyException(
@@ -1064,7 +1074,7 @@ class FreqtradeBot:
trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed':
trade.update(order)
self.update_trade_state(trade, order)
Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys
@@ -1155,7 +1165,7 @@ class FreqtradeBot:
#
def update_trade_state(self, trade: Trade, action_order: dict = None,
order_amount: float = None) -> bool:
order_amount: float = None, sl_order: bool = False) -> bool:
"""
Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders.
@@ -1163,84 +1173,125 @@ class FreqtradeBot:
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
try:
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception)
return False
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order, order_amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
# Fee was applied, so set to 0
trade.fee_open = 0
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
order_id = trade.open_order_id
elif trade.stoploss_order_id and sl_order:
order_id = trade.stoploss_order_id
else:
return False
# Update trade with order values
logger.info('Found open order for %s', trade)
try:
order = action_order or self.exchange.get_order(order_id, trade.pair)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', order_id, exception)
return False
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order, order_amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout.
return True
trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout.
return True
trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
return False
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
amount: float, fee_abs: float) -> float:
"""
Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available.
"""
self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency
logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0:
real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs)
logger.info(f"Applying fee on amount for {trade} "
f"(from {amount} to {real_amount}).")
return real_amount
return amount
def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
"""
Get real amount for the trade
Detect and update trade fee.
Calls trade.update_fee() uppon correct detection.
Returns modified amount if the fee was taken from the destination currency.
Necessary for exchanges which charge fees in base currency (e.g. binance)
:return: identical (or new) amount for the trade
"""
# Init variables
if order_amount is None:
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
return order_amount
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible
if ('fee' in order and order['fee'] is not None and
(order['fee'].keys() >= {'currency', 'cost'})):
if (order['fee']['currency'] is not None and
order['fee']['cost'] is not None and
trade_base_currency == order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
if self.exchange.order_has_fee(order):
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
# Fallback to Trades
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if trade_base_currency == fee_currency:
# Apply fee to amount
return self.apply_fee_conditional(trade, trade_base_currency,
amount=order_amount, fee_abs=fee_cost)
return order_amount
return self.fee_detection_from_trades(trade, order, order_amount)
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
"""
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount
fee_currency = None
amount = 0
fee_abs = 0
fee_abs = 0.0
fee_cost = 0.0
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
fee_rate_array: List[float] = []
for exectrade in trades:
amount += exectrade['amount']
if ("fee" in exectrade and exectrade['fee'] is not None and
(exectrade['fee'].keys() >= {'currency', 'cost'})):
if self.exchange.order_has_fee(exectrade):
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
fee_cost += fee_cost_
if fee_rate_ is not None:
fee_rate_array.append(fee_rate_)
# only applies if fee is in quote currency!
if (exectrade['fee']['currency'] is not None and
exectrade['fee']['cost'] is not None and
trade_base_currency == exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost']
if trade_base_currency == fee_currency:
fee_abs += fee_cost_
# Ensure at least one trade was found:
if fee_currency:
# fee_rate should use mean
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} "
f"(from {order_amount} to {real_amount}) from Trades")
return real_amount
return self.apply_fee_conditional(trade, trade_base_currency,
amount=amount, fee_abs=fee_abs)
else:
return amount

View File

@@ -387,12 +387,19 @@ class Hyperopt:
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'

View File

@@ -86,11 +86,15 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'close_profit_abs'):
if not has_column(cols, 'fee_close_cost'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
fee_close = get_column_def(cols, 'fee_close', 'fee')
fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
@@ -120,7 +124,9 @@ def check_migrate(engine) -> None:
# Copy data back - following the correct schema
engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
(id, exchange, pair, is_open,
fee_open, fee_open_cost, fee_open_currency,
fee_close, fee_close_cost, fee_open_currency, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
@@ -136,7 +142,9 @@ def check_migrate(engine) -> None:
else pair
end
pair,
is_open, {fee_open} fee_open, {fee_close} fee_close,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
@@ -185,7 +193,11 @@ class Trade(_DECL_BASE):
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_price - calculated via _calc_open_trade_price
@@ -235,7 +247,11 @@ class Trade(_DECL_BASE):
'pair': self.pair,
'is_open': self.is_open,
'fee_open': self.fee_open,
'fee_open_cost': self.fee_open_cost,
'fee_open_currency': self.fee_open_currency,
'fee_close': self.fee_close,
'fee_close_cost': self.fee_close_cost,
'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'close_date_hum': (arrow.get(self.close_date).humanize()
@@ -360,6 +376,35 @@ class Trade(_DECL_BASE):
self
)
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
side: str) -> None:
"""
Update Fee parameters. Only acts once per side
"""
if side == 'buy' and self.fee_open_currency is None:
self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency
if fee_rate is not None:
self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None:
self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency
if fee_rate is not None:
self.fee_close = fee_rate
def fee_updated(self, side: str) -> bool:
"""
Verify if this side (buy / sell) has already been updated
"""
if side == 'buy':
return self.fee_open_currency is not None
elif side == 'sell':
return self.fee_close_currency is not None
else:
return False
def _calc_open_trade_price(self) -> float:
"""
Calculate the open_rate including open_fee.

View File

@@ -2,11 +2,17 @@ import logging
import threading
from datetime import date, datetime
from ipaddress import IPv4Address
from typing import Dict, Callable, Any
from typing import Any, Callable, Dict
from arrow import Arrow
from flask import Flask, jsonify, request
from flask.json import JSONEncoder
from flask_cors import CORS
from flask_jwt_extended import (JWTManager, create_access_token,
create_refresh_token, get_jwt_identity,
jwt_refresh_token_required,
verify_jwt_in_request_optional)
from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server
from freqtrade.__init__ import __version__
@@ -38,9 +44,9 @@ class ArrowJSONEncoder(JSONEncoder):
def require_login(func: Callable[[Any, Any], Any]):
def func_wrapper(obj, *args, **kwargs):
verify_jwt_in_request_optional()
auth = request.authorization
if auth and obj.check_auth(auth.username, auth.password):
if get_jwt_identity() or auth and obj.check_auth(auth.username, auth.password):
return func(obj, *args, **kwargs)
else:
return jsonify({"error": "Unauthorized"}), 401
@@ -70,8 +76,8 @@ class ApiServer(RPC):
"""
def check_auth(self, username, password):
return (username == self._config['api_server'].get('username') and
password == self._config['api_server'].get('password'))
return (safe_str_cmp(username, self._config['api_server'].get('username')) and
safe_str_cmp(password, self._config['api_server'].get('password')))
def __init__(self, freqtrade) -> None:
"""
@@ -83,6 +89,13 @@ class ApiServer(RPC):
self._config = freqtrade.config
self.app = Flask(__name__)
self._cors = CORS(self.app, resources={r"/api/*": {"origins": "*"}})
# Setup the Flask-JWT-Extended extension
self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get(
'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app)
self.app.json_encoder = ArrowJSONEncoder
# Register application handling
@@ -148,6 +161,10 @@ class ApiServer(RPC):
self.app.register_error_handler(404, self.page_not_found)
# Actions to control the bot
self.app.add_url_rule(f'{BASE_URI}/token/login', 'login',
view_func=self._token_login, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/token/refresh', 'token_refresh',
view_func=self._token_refresh, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/start', 'start',
view_func=self._start, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
@@ -199,6 +216,37 @@ class ApiServer(RPC):
'code': 404
}), 404
@require_login
@rpc_catch_errors
def _token_login(self):
"""
Handler for /token/login
Returns a JWT token
"""
auth = request.authorization
if auth and self.check_auth(auth.username, auth.password):
keystuff = {'u': auth.username}
ret = {
'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff),
}
return self.rest_dump(ret)
return jsonify({"error": "Unauthorized"}), 401
@jwt_refresh_token_required
@rpc_catch_errors
def _token_refresh(self):
"""
Handler for /token/refresh
Returns a JWT token based on a JWT refresh token
"""
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token}
return self.rest_dump(ret)
@require_login
@rpc_catch_errors
def _start(self):

View File

@@ -94,6 +94,7 @@ class RPC:
'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'],
'max_open_trades': config['max_open_trades'],
'minimal_roi': config['minimal_roi'].copy(),
'stoploss': config['stoploss'],
'trailing_stop': config['trailing_stop'],
@@ -103,6 +104,8 @@ class RPC:
'ticker_interval': config['ticker_interval'],
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),
'state': str(self._freqtrade.state)
}
return val

View File

@@ -579,7 +579,7 @@ class Telegram(RPC):
"*/whitelist:* `Show current whitelist` \n" \
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
"to the blacklist.` \n" \
"*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \
"*/help:* `This help message`\n" \
"*/version:* `Show version`"
@@ -621,10 +621,12 @@ class Telegram(RPC):
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Max open Trades:* `{val['max_open_trades']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"{sl_info}"
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
f"*Strategy:* `{val['strategy']}`"
f"*Strategy:* `{val['strategy']}`\n"
f"*Current state:* `{val['state']}`"
)
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:

View File

@@ -14,6 +14,9 @@ class State(Enum):
STOPPED = 2
RELOAD_CONF = 3
def __str__(self):
return f"{self.name.lower()}"
class RunMode(Enum):
"""