Merge branch 'feat/short' into pr/samgermain/5780

This commit is contained in:
Matthias
2021-12-02 07:09:37 +01:00
49 changed files with 600 additions and 350 deletions

View File

@@ -1,4 +1,5 @@
from datetime import date, datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
@@ -125,19 +126,24 @@ class Daily(BaseModel):
class UnfilledTimeout(BaseModel):
buy: int
sell: int
unit: str
buy: Optional[int]
sell: Optional[int]
unit: Optional[str]
exit_timeout_count: Optional[int]
class OrderTypeValues(str, Enum):
limit = 'limit'
market = 'market'
class OrderTypes(BaseModel):
buy: str
sell: str
emergencysell: Optional[str]
forcesell: Optional[str]
forcebuy: Optional[str]
stoploss: str
buy: OrderTypeValues
sell: OrderTypeValues
emergencysell: Optional[OrderTypeValues]
forcesell: Optional[OrderTypeValues]
forcebuy: Optional[OrderTypeValues]
stoploss: OrderTypeValues
stoploss_on_exchange: bool
stoploss_on_exchange_interval: Optional[int]
@@ -185,7 +191,8 @@ class TradeSchema(BaseModel):
amount_requested: float
stake_amount: float
strategy: str
buy_tag: Optional[str]
buy_tag: Optional[str] # Deprecated
enter_tag: Optional[str]
timeframe: int
fee_open: Optional[float]
fee_open_cost: Optional[float]
@@ -277,10 +284,12 @@ class Logs(BaseModel):
class ForceBuyPayload(BaseModel):
pair: str
price: Optional[float]
ordertype: Optional[OrderTypeValues]
class ForceSellPayload(BaseModel):
tradeid: str
ordertype: Optional[OrderTypeValues]
class BlacklistPayload(BaseModel):

View File

@@ -29,7 +29,8 @@ logger = logging.getLogger(__name__)
# API version
# Pre-1.1, no version was provided
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
API_VERSION = 1.1
# 1.11: forcebuy and forcesell accept ordertype
API_VERSION = 1.11
# Public API, requires no auth.
router_public = APIRouter()
@@ -129,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
ordertype = payload.ordertype.value if payload.ordertype else None
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
if trade:
return ForceBuyResponse.parse_obj(trade.to_json())
@@ -139,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_forcesell(payload.tradeid)
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_forcesell(payload.tradeid, ordertype)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])

View File

@@ -646,7 +646,7 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
"""
Handler for forcesell <id>.
Sells the given trade at current price
@@ -671,7 +671,11 @@ class RPC:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forcesell", self._freqtrade.strategy.order_types["sell"])
self._freqtrade.execute_trade_exit(
trade, current_rate, sell_reason, ordertype=order_type)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
@@ -699,7 +703,8 @@ class RPC:
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
def _rpc_forcebuy(self, pair: str, price: Optional[float],
order_type: Optional[str] = None) -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price
@@ -727,7 +732,10 @@ class RPC:
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
if not order_type:
order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy'])
if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
@@ -782,27 +790,23 @@ class RPC:
return pair_rates
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for buy tag performance.
Shows a performance statistic from finished trades
"""
buy_tags = Trade.get_buy_tag_performance(pair)
return buy_tags
return Trade.get_enter_tag_performance(pair)
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for sell reason performance.
Shows a performance statistic from finished trades
"""
sell_reasons = Trade.get_sell_reason_performance(pair)
return sell_reasons
return Trade.get_sell_reason_performance(pair)
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for mix tag (buy_tag + sell_reason) performance.
Handler for mix tag (enter_tag + sell_reason) performance.
Shows a performance statistic from finished trades
"""
mix_tags = Trade.get_mix_tag_performance(pair)

View File

@@ -112,6 +112,7 @@ class Telegram(RPCHandler):
r'/stats$', r'/count$', r'/locks$', r'/balance$',
r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/help$', r'/version$']
# Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys]
@@ -154,7 +155,7 @@ class Telegram(RPCHandler):
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('buys', self._buy_tag_performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler('sells', self._sell_reason_performance),
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats),
@@ -182,7 +183,8 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'),
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
CallbackQueryHandler(self._enter_tag_performance,
pattern='update_enter_tag_performance'),
CallbackQueryHandler(self._sell_reason_performance,
pattern='update_sell_reason_performance'),
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
@@ -226,7 +228,7 @@ class Telegram(RPCHandler):
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg['type'] == RPCMessageType.BUY_FILL:
@@ -251,7 +253,7 @@ class Telegram(RPCHandler):
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
msg['emoji'] = self._get_sell_emoji(msg)
# Check if all sell properties are available.
@@ -271,7 +273,7 @@ class Telegram(RPCHandler):
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"*Buy Tag:* `{msg['buy_tag']}`\n"
f"*Enter Tag:* `{msg['enter_tag']}`\n"
f"*Sell Reason:* `{msg['sell_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
@@ -397,7 +399,7 @@ class Telegram(RPCHandler):
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
"*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
"*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
"*Current Rate:* `{current_rate:.8f}`",
@@ -972,7 +974,7 @@ class Telegram(RPCHandler):
self._send_msg(str(e))
@authorized_only
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /buys PAIR .
Shows a performance statistic from finished trades
@@ -985,11 +987,11 @@ class Telegram(RPCHandler):
if context.args and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_buy_tag_performance(pair)
trades = self._rpc._rpc_enter_tag_performance(pair)
output = "<b>Buy Tag Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['buy_tag']}\t"
f"{i+1}.\t <code>{trade['enter_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
@@ -1001,7 +1003,7 @@ class Telegram(RPCHandler):
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_buy_tag_performance",
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -1277,7 +1279,8 @@ class Telegram(RPCHandler):
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
# TODO-lev: Update commands and help (?)
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"

View File

@@ -2,6 +2,7 @@
This module manages webhook communication
"""
import logging
import time
from typing import Any, Dict
from requests import RequestException, post
@@ -28,12 +29,9 @@ class Webhook(RPCHandler):
super().__init__(rpc, config)
self._url = self._config['webhook']['url']
self._format = self._config['webhook'].get('format', 'form')
if self._format != 'form' and self._format != 'json':
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
'`form` (default) and `json`'.format(self._format))
self._retries = self._config['webhook'].get('retries', 0)
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
def cleanup(self) -> None:
"""
@@ -77,13 +75,30 @@ class Webhook(RPCHandler):
def _send_msg(self, payload: dict) -> None:
"""do the actual call to the webhook"""
try:
if self._format == 'form':
post(self._url, data=payload)
elif self._format == 'json':
post(self._url, json=payload)
else:
raise NotImplementedError('Unknown format: {}'.format(self._format))
success = False
attempts = 0
while not success and attempts <= self._retries:
if attempts:
if self._retry_delay:
time.sleep(self._retry_delay)
logger.info("Retrying webhook...")
except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc)
attempts += 1
try:
if self._format == 'form':
response = post(self._url, data=payload)
elif self._format == 'json':
response = post(self._url, json=payload)
elif self._format == 'raw':
response = post(self._url, data=payload['data'],
headers={'Content-Type': 'text/plain'})
else:
raise NotImplementedError('Unknown format: {}'.format(self._format))
# Throw a RequestException if the post was not successful
response.raise_for_status()
success = True
except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc)