Merge branch 'develop' into feat/short

This commit is contained in:
Sam Germain
2022-03-03 13:51:52 -06:00
39 changed files with 604 additions and 210 deletions

View File

@@ -110,6 +110,7 @@ def ask_user_config() -> Dict[str, Any]:
"bittrex",
"ftx",
"gateio",
"huobi",
"kraken",
"kucoin",
"okx",

View File

@@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
RunMode.HYPEROPT: 'hyperoptimization',
}
if method in no_unlimited_runmodes.keys():
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
# tradable_balance_ratio
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
and config['stake_amount'] > config['dry_run_wallet']):
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
and config['stake_amount'] > wallet_size):
wallet = round_coin_value(wallet_size, config['stake_currency'])
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
raise OperationalException(f"Starting balance ({wallet}) "
f"is smaller than stake_amount {stake}.")
raise OperationalException(
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
)
return config

View File

@@ -447,7 +447,6 @@ SCHEMA_TRADE_REQUIRED = [
'dry_run_wallet',
'ask_strategy',
'bid_strategy',
'unfilledtimeout',
'stoploss',
'minimal_roi',
'internals',
@@ -463,7 +462,6 @@ SCHEMA_BACKTEST_REQUIRED = [
'dry_run_wallet',
'dataformat_ohlcv',
'dataformat_trades',
'unfilledtimeout',
]
SCHEMA_MINIMAL_REQUIRED = [

View File

@@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.okx import Okx

View File

@@ -6,13 +6,10 @@ from pathlib import Path
from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exceptions import (DDosProtection, OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__)
@@ -22,6 +19,7 @@ class Binance(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop_loss_limit"},
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
"ohlcv_candle_limit": 1000,
@@ -52,82 +50,6 @@ class Binance(Exchange):
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
)
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
:param side: "buy" or "sell"
"""
# Limit price threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
# TODO: Name limit_rate in other exchange subclasses
rate = stop_price * limit_price_pct
else:
rate = stop_price * (2 - limit_price_pct)
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
stop_price = self.price_to_precision(pair, stop_price)
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
# Ensure rate is less than stop price
if bad_stop_price:
raise OperationalException(
'In stoploss limit order, stop price should be better than limit price')
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
amount = self.amount_to_precision(pair, amount)
rate = self.price_to_precision(pair, rate)
self._lev_prep(pair, leverage, side)
order = self._api.create_order(
symbol=pair,
type=ordertype,
side=side,
amount=amount,
price=rate,
params=params
)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `binance Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def _set_leverage(
self,

View File

@@ -769,7 +769,8 @@ class Exchange:
# Dry-run methods
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
rate: float, leverage: float, params: Dict = {},
stop_loss: bool = False) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount)
dry_order: Dict[str, Any] = {
@@ -785,15 +786,18 @@ class Exchange:
'remaining': _amount,
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open",
'status': "closed" if ordertype == "market" and not stop_loss else "open",
'fee': None,
'info': {},
'leverage': leverage
}
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order["stopPrice"] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
if dry_order["type"] == "market":
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate)
dry_order.update({
@@ -884,7 +888,9 @@ class Exchange:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
if order['status'] != "closed" and order['type'] in ["limit"]:
if (order['status'] != "closed"
and order['type'] in ["limit"]
and not order.get('ft_order_type')):
pair = order['symbol']
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
order.update({
@@ -1002,19 +1008,99 @@ class Exchange:
"""
raise OperationalException(f"stoploss is not implemented for {self.name}.")
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopPrice': stop_price})
return params
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str, leverage: float) -> Dict:
"""
creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
to the corresponding exchange type.
The precise ordertype is determined by the order_types dict or exchange default.
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
exchange's subclass.
The exception below should never raise, since we disallow
starting the bot in validate_ordertypes()
Note: Changes to this interface need to be applied to all sub-classes too.
"""
raise OperationalException(f"stoploss is not implemented for {self.name}.")
This may work with a limited number of other exchanges, but correct working
needs to be tested individually.
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
`stoploss_adjust` must still be implemented for this to work.
"""
if not self._ft_has['stoploss_on_exchange']:
raise OperationalException(f"stoploss is not implemented for {self.name}.")
user_order_type = order_types.get('stoploss', 'market')
if user_order_type in self._ft_has["stoploss_order_types"].keys():
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
else:
# Otherwise pick only one available
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
stop_price_norm = self.price_to_precision(pair, stop_price)
rate = None
if user_order_type == 'limit':
# Limit price threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
rate = stop_price * limit_price_pct
# Ensure rate is less than stop price
if stop_price_norm <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
rate = self.price_to_precision(pair, rate)
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair,
ordertype,
side,
amount,
stop_price_norm,
stop_loss=True,
leverage=leverage,
)
return dry_order
try:
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True
amount = self.amount_to_precision(pair, amount)
self._lev_prep(pair, leverage, side)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=rate, params=params)
logger.info(f"stoploss {user_order_type} order added for {pair}. "
f"stop price: {stop_price}. limit: {rate}")
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not place stoploss order due to {e.__class__.__name__}. "
f"Message: {e}") from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str) -> Dict:
@@ -2384,7 +2470,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:

View File

@@ -62,7 +62,7 @@ class Ftx(Exchange):
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage)
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order
try:

View File

@@ -0,0 +1,39 @@
""" Huobi exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Huobi(Exchange):
"""
Huobi exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop-limit"},
"ohlcv_candle_limit": 1000,
"l2_limit_range": [5, 10, 20],
"l2_limit_range_required": False,
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({
"stopPrice": stop_price,
"operator": "lte",
})
return params

View File

@@ -99,6 +99,8 @@ class Kraken(Exchange):
"""
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.
TODO: investigate if this can be combined with generic implementation
(careful, prices are reversed)
"""
params = self._params.copy()
if self.trading_mode == TradingMode.FUTURES:
@@ -119,7 +121,7 @@ class Kraken(Exchange):
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage)
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order
try:

View File

@@ -19,8 +19,26 @@ class Kucoin(Exchange):
"""
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"l2_limit_range": [20, 100],
"l2_limit_range_required": False,
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({
'stopPrice': stop_price,
'stop': 'loss'
})
return params

View File

@@ -1070,7 +1070,7 @@ class FreqtradeBot(LoggingMixin):
return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
@@ -1142,15 +1142,19 @@ class FreqtradeBot(LoggingMixin):
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
order_obj = trade.select_order_by_order_id(trade.open_order_id)
if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
time_method, trade, order_obj, datetime.now(timezone.utc)))
):
if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count()
if max_timeouts > 0 and canceled_count >= max_timeouts:
max_timeouts = self.config.get(
'unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.')
try:
@@ -1252,11 +1256,12 @@ class FreqtradeBot(LoggingMixin):
reason=reason)
return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
"""
exit order cancel - cancel order and update trade
:return: Reason for cancel
:return: True if exit order was cancelled, false otherwise
"""
cancelled = False
# 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):
@@ -1268,7 +1273,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException:
logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order'
return False
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@@ -1282,9 +1287,12 @@ class FreqtradeBot(LoggingMixin):
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
trade.sell_reason = None
cancelled = True
else:
# TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False
self.wallets.update()
self._notify_exit_cancel(
@@ -1292,7 +1300,7 @@ class FreqtradeBot(LoggingMixin):
order_type=self.strategy.order_types[trade.exit_side],
reason=reason
)
return reason
return cancelled
def _safe_exit_amount(self, pair: str, amount: float) -> float:
"""
@@ -1351,8 +1359,8 @@ class FreqtradeBot(LoggingMixin):
# if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price
if self.config['dry_run'] and exit_type == 'stoploss' \
and self.strategy.order_types['stoploss_on_exchange']:
if (self.config['dry_run'] and exit_type == 'stoploss'
and self.strategy.order_types['stoploss_on_exchange']):
limit = trade.stop_loss
# set custom_exit_price if available

View File

@@ -121,7 +121,7 @@ class Order(_DECL_BASE):
ft_pair: str = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String(255), nullable=False, index=True)
order_id: str = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True)
order_type: str = Column(String(50), nullable=True)
@@ -199,8 +199,12 @@ class Order(_DECL_BASE):
def to_json(self) -> Dict[str, Any]:
return {
'pair': self.ft_pair,
'order_id': self.order_id,
'status': self.status,
'amount': self.amount,
'average': round(self.average, 8) if self.average else 0,
'safe_price': self.safe_price,
'cost': self.cost if self.cost else 0,
'filled': self.filled,
'ft_order_side': self.ft_order_side,
@@ -214,10 +218,8 @@ class Order(_DECL_BASE):
'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type,
'pair': self.ft_pair,
'price': self.price,
'remaining': self.remaining,
'status': self.status,
}
def close_bt_order(self, close_date: datetime):
@@ -456,14 +458,7 @@ class LocalTrade():
def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders()
filled_entries = []
filled_exits = []
if len(filled_orders) > 0:
for order in filled_orders:
if order.ft_order_side == 'buy':
filled_entries.append(order.to_json())
if order.ft_order_side == 'sell':
filled_exits.append(order.to_json())
orders = [order.to_json() for order in filled_orders]
return {
'trade_id': self.id,
@@ -535,8 +530,7 @@ class LocalTrade():
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
'filled_entry_orders': filled_entries,
'filled_exit_orders': filled_exits,
'orders': orders,
}
@staticmethod

View File

@@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.deps import get_config
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException
@@ -22,7 +22,7 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)):
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
"""Start backtesting if not done so already"""
if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running')
@@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest():
def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"""
Get backtesting result.
Returns Result after backtesting has been ran.
@@ -157,7 +157,7 @@ def api_get_backtest():
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest():
def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"""Reset backtesting"""
if ApiServer._bgtask_running:
return {
@@ -183,7 +183,7 @@ def api_delete_backtest():
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort():
def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
if not ApiServer._bgtask_running:
return {
"status": "not_running",

View File

@@ -184,6 +184,22 @@ class ShowConfig(BaseModel):
max_entry_position_adjustment: int
class OrderSchema(BaseModel):
pair: str
order_id: str
status: str
remaining: float
amount: float
safe_price: float
cost: float
filled: float
ft_order_side: str
order_type: str
is_open: bool
order_timestamp: Optional[int]
order_filled_timestamp: Optional[int]
class TradeSchema(BaseModel):
trade_id: int
pair: str
@@ -233,6 +249,7 @@ class TradeSchema(BaseModel):
min_rate: Optional[float]
max_rate: Optional[float]
open_order_id: Optional[str]
orders: List[OrderSchema]
leverage: Optional[float]
interest_rate: Optional[float]

View File

@@ -34,8 +34,8 @@ logger = logging.getLogger(__name__)
# 1.12: add blacklist delete endpoint
# 1.13: forcebuy supports stake_amount
# versions 2.xx -> futures/short branch
# 2.13: addition of Forceenter
API_VERSION = 2.13
# 2.14: Add entry/exit orders to trade response
API_VERSION = 2.14
# Public API, requires no auth.
router_public = APIRouter()

View File

@@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException
@@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config)
return ApiServer._exchange
def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER:
raise RPCException('Bot is not in the correct state')
return None

View File

@@ -389,46 +389,52 @@ class Telegram(RPCHandler):
else:
return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders, base_currency, is_open):
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
"""
Prepare details of trade with entry adjustment enabled
"""
lines = []
lines: List[str] = []
if len(filled_orders) > 0:
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders):
if order['ft_order_side'] != 'buy':
continue
cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"]
cur_entry_average = order["average"]
cur_entry_average = order["safe_price"]
lines.append(" ")
if x == 0:
lines.append("*Entry #{}:*".format(x+1))
lines.append("*Entry Amount:* {} ({:.8f} {})"
.format(cur_entry_amount, order["cost"], base_currency))
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
lines.append(f"*Entry #{x+1}:*")
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average}")
else:
sumA = 0
sumB = 0
for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
sumB += filled_orders[y]["amount"]
prev_avg_price = sumA/sumB
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
/ filled_orders[0]["average"])
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
prev_avg_price = sumA / sumB
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
minus_on_entry = 0
if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry))
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open:
lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append("*Entry Amount:* {} ({:.8f} {})"
.format(cur_entry_amount, order["cost"], base_currency))
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
.format(cur_entry_average, price_to_1st_entry))
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
lines.append("({}d {}h {}m {}s from previous entry)"
.format(days, hours, minutes, seconds))
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled at:* {order['order_filled_date']}")
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
return lines
@authorized_only
@@ -459,7 +465,7 @@ class Telegram(RPCHandler):
messages = []
for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len(r['filled_entry_orders'])
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
r['sell_reason'] = r.get('sell_reason', "")
lines = [
"*Trade ID:* `{trade_id}`" +
@@ -505,8 +511,8 @@ class Telegram(RPCHandler):
lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details(
r['filled_entry_orders'], r['base_currency'], r['is_open'])
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
r['orders'], r['base_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "")
# Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]).format(**r))

View File

@@ -0,0 +1,12 @@
"exchange": {
"name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
],
"pair_blacklist": [
"HT/.*"
]
}