Merge branch 'develop' into pr/mkavinkumar1/6545

This commit is contained in:
Matthias
2022-07-19 07:22:02 +02:00
34 changed files with 447 additions and 135 deletions

View File

@@ -313,6 +313,10 @@ CONF_SCHEMA = {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
},
'show_candle': {
'type': 'string',
'enum': ['off', 'ohlc'],
},
}
},
'reload': {'type': 'boolean'},
@@ -538,3 +542,4 @@ TradeList = List[List]
LongShort = Literal['long', 'short']
EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell']
MakerTaker = Literal['maker', 'taker']

View File

@@ -73,7 +73,7 @@ EXCHANGE_HAS_REQUIRED = [
EXCHANGE_HAS_OPTIONAL = [
# Private
'fetchMyTrades', # Trades for order - fee detection
# 'createLimitOrder', 'createMarketOrder', # Either OR for orders
'createLimitOrder', 'createMarketOrder', # Either OR for orders
# 'setLeverage', # Margin/Futures trading
# 'setMarginMode', # Margin/Futures trading
# 'fetchFundingHistory', # Futures trading

View File

@@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_
from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
@@ -79,6 +79,7 @@ class Exchange:
"ccxt_futures_name": "swap",
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
}
_ft_has: Dict = {}
_ft_has_futures: Dict = {}
@@ -175,23 +176,11 @@ class Exchange:
logger.info(f'Using Exchange "{self.name}"')
if validate:
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))
# Initial markets load
self._load_markets()
# Check if all pairs are available
self.validate_stakecurrency(config['stake_currency'])
if not exchange_config.get('skip_pair_validation'):
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_config(config)
self.required_candle_call_count = self.validate_required_startup_candles(
config.get('startup_candle_count', 0), config.get('timeframe', ''))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
self.validate_pricing(config['exit_pricing'])
self.validate_pricing(config['entry_pricing'])
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get(
@@ -214,6 +203,20 @@ class Exchange:
logger.info("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close())
def validate_config(self, config):
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))
# Check if all pairs are available
self.validate_stakecurrency(config['stake_currency'])
if not config['exchange'].get('skip_pair_validation'):
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
self.validate_pricing(config['exit_pricing'])
self.validate_pricing(config['entry_pricing'])
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
"""
@@ -423,7 +426,7 @@ class Exchange:
if 'symbol' in order and order['symbol'] is not None:
contract_size = self._get_contract_size(order['symbol'])
if contract_size != 1:
for prop in ['amount', 'cost', 'filled', 'remaining']:
for prop in self._ft_has.get('order_props_in_contracts', []):
if prop in order and order[prop] is not None:
order[prop] = order[prop] * contract_size
return order
@@ -586,13 +589,10 @@ class Exchange:
"""
Checks if order-types configured in strategy/config are supported
"""
# TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as
# Revert the change in test_validate_ordertypes.
# if any(v == 'market' for k, v in order_types.items()):
# if not self.exchange_has('createMarketOrder'):
# raise OperationalException(
# f'Exchange {self.name} does not support market orders.')
if any(v == 'market' for k, v in order_types.items()):
if not self.exchange_has('createMarketOrder'):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)):
@@ -824,7 +824,7 @@ class Exchange:
'price': rate,
'average': rate,
'amount': _amount,
'cost': _amount * rate / leverage,
'cost': _amount * rate,
'type': ordertype,
'side': side,
'filled': 0,
@@ -850,20 +850,27 @@ class Exchange:
'filled': _amount,
'cost': (dry_order['amount'] * average) / leverage
})
dry_order = self.add_dry_order_fee(pair, dry_order)
# market orders will always incurr taker fees
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
dry_order = self.check_dry_limit_order_filled(dry_order)
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]:
def add_dry_order_fee(
self,
pair: str,
dry_order: Dict[str, Any],
taker_or_maker: MakerTaker,
) -> Dict[str, Any]:
fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
dry_order.update({
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
'cost': dry_order['cost'] * fee,
'rate': fee
}
})
return dry_order
@@ -929,7 +936,8 @@ class Exchange:
pass
return False
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
def check_dry_limit_order_filled(
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
@@ -943,7 +951,12 @@ class Exchange:
'filled': order['amount'],
'remaining': 0,
})
self.add_dry_order_fee(pair, order)
self.add_dry_order_fee(
pair,
order,
'taker' if immediate else 'maker',
)
return order
@@ -1631,7 +1644,7 @@ class Exchange:
@retrier
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: str = 'maker') -> float:
price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
try:
if self._config['dry_run'] and self._config.get('fee', None) is not None:
return self._config['fee']
@@ -1679,7 +1692,7 @@ class Exchange:
fee_curr = fee.get('currency')
if fee_curr is None:
return None
fee_cost = fee['cost']
fee_cost = float(fee['cost'])
if self._ft_has['fee_cost_in_contracts']:
# Convert cost via "contracts" conversion
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
@@ -1687,7 +1700,7 @@ class Exchange:
# Calculate fee based on order details
if fee_curr == self.get_pair_base_currency(symbol):
# Base currency - divide by amount
return round(fee['cost'] / amount, 8)
return round(fee_cost / amount, 8)
elif fee_curr == self.get_pair_quote_currency(symbol):
# Quote currency - divide by cost
return round(fee_cost / cost, 8) if cost else None
@@ -1718,7 +1731,7 @@ class Exchange:
:param amount: Amount of the order
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (fee['cost'],
return (float(fee['cost']),
fee['currency'],
self.calculate_fee_rate(
fee,

View File

@@ -1,12 +1,13 @@
""" Gate.io exchange subclass """
import logging
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ class Gateio(Exchange):
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -96,12 +98,29 @@ class Gateio(Exchange):
}
return trades
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if self.trading_mode == TradingMode.FUTURES:
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
return self.fetch_order(
order = self.fetch_order(
order_id=order_id,
pair=pair,
params={'stop': True}
)
if self.trading_mode == TradingMode.FUTURES:
if order['status'] == 'closed':
# Places a real order - which we need to fetch explicitly.
new_orderid = order.get('info', {}).get('trade_id')
if new_orderid:
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['stopPrice'] = order.get('stopPrice')
return order1
return order
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
return self.cancel_order(

View File

@@ -333,6 +333,8 @@ class FreqtradeBot(LoggingMixin):
if not trade.is_open and not trade.fee_updated(trade.exit_side):
# Get sell fee
order = trade.select_order(trade.exit_side, False)
if not order:
order = trade.select_order('stoploss', False)
if order:
logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"

View File

@@ -6,6 +6,7 @@ This module contains the hyperopt logic
import logging
import random
import sys
import warnings
from datetime import datetime, timezone
from math import ceil
@@ -17,6 +18,7 @@ import rapidjson
from colorama import Fore, Style
from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from joblib.externals import cloudpickle
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
@@ -87,6 +89,7 @@ class Hyperopt:
self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
@@ -137,6 +140,17 @@ class Hyperopt:
logger.info(f"Removing `{p}`.")
p.unlink()
def hyperopt_pickle_magic(self, bases) -> None:
"""
Hyperopt magic to allow strategy inheritance across files.
For this to properly work, we need to register the module of the imported class
to pickle as value.
"""
for modules in bases:
if modules.__name__ != 'IStrategy':
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
# Ensure the number of dimensions match

View File

@@ -255,18 +255,18 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
"""
# Trades can be empty
if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade
# Create description for exit summarizing the trade
trades['desc'] = trades.apply(
lambda row: f"{row['profit_ratio']:.2%}, " +
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
f"{row['exit_reason']}, " +
f"{row['trade_duration']} min",
axis=1)
trade_buys = go.Scatter(
trade_entries = go.Scatter(
x=trades["open_date"],
y=trades["open_rate"],
mode='markers',
name='Trade buy',
name='Trade entry',
text=trades["desc"],
marker=dict(
symbol='circle-open',
@@ -277,12 +277,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
)
trade_sells = go.Scatter(
trade_exits = go.Scatter(
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
mode='markers',
name='Sell - Profit',
name='Exit - Profit',
marker=dict(
symbol='square-open',
size=11,
@@ -290,12 +290,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
color='green'
)
)
trade_sells_loss = go.Scatter(
trade_exits_loss = go.Scatter(
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
mode='markers',
name='Sell - Loss',
name='Exit - Loss',
marker=dict(
symbol='square-open',
size=11,
@@ -303,9 +303,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
color='red'
)
)
fig.add_trace(trade_buys, 1, 1)
fig.add_trace(trade_sells, 1, 1)
fig.add_trace(trade_sells_loss, 1, 1)
fig.add_trace(trade_entries, 1, 1)
fig.add_trace(trade_exits, 1, 1)
fig.add_trace(trade_exits_loss, 1, 1)
else:
logger.warning("No trades found.")
return fig
@@ -444,7 +444,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
:param pair: Pair to Display on the graph
:param data: OHLCV DataFrame containing indicators and buy/sell signals
:param data: OHLCV DataFrame containing indicators and entry/exit signals
:param trades: All trades created
:param indicators1: List containing Main plot indicators
:param indicators2: List containing Sub plot indicators

View File

@@ -243,6 +243,22 @@ class Telegram(RPCHandler):
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
def _add_analyzed_candle(self, pair: str) -> str:
candle_val = self._config['telegram'].get(
'notification_settings', {}).get('show_candle', 'off')
if candle_val != 'off':
if candle_val == 'ohlc':
analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe(
pair, self._config['timeframe'])
candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None
if candle is not None:
return (
f"*Candle OHLC*: `{candle['open']}, {candle['high']}, "
f"{candle['low']}, {candle['close']}`\n"
)
return ''
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
@@ -259,6 +275,7 @@ class Telegram(RPCHandler):
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
message += self._add_analyzed_candle(msg['pair'])
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
@@ -322,6 +339,7 @@ class Telegram(RPCHandler):
message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"{cp_extra}"

View File

@@ -51,11 +51,13 @@
"source": [
"# Load data using values set above\n",
"from freqtrade.data.history import load_pair_history\n",
"from freqtrade.enums import CandleType\n",
"\n",
"candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair,\n",
" data_format = \"hdf5\",\n",
" candle_type=CandleType.SPOT,\n",
" )\n",
"\n",
"# Confirm success\n",