Merge branch 'develop' into fut/stop_price_type

This commit is contained in:
Matthias 2023-02-09 20:02:59 +01:00
commit eab724fe54
25 changed files with 263 additions and 131 deletions

View File

@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "4.0.1" rev: "6.0.0"
hooks: hooks:
- id: flake8 - id: flake8
# stages: [push] # stages: [push]
@ -13,7 +13,7 @@ repos:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.2.1 - types-cachetools==5.3.0.0
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.28.11.8 - types-requests==2.28.11.8
- types-tabulate==0.9.0.0 - types-tabulate==0.9.0.0
@ -21,14 +21,14 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: "5.10.1" rev: "5.12.0"
hooks: hooks:
- id: isort - id: isort
name: isort (python) name: isort (python)
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0 rev: v4.4.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |

View File

@ -243,8 +243,8 @@ OKX requires a passphrase for each api key, you will therefore need to add this
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
!!! Warning "Futures" !!! Warning "Futures"
OKX Futures has the concept of "position mode" - which can be Net or long/short (hedge mode). OKX Futures has the concept of "position mode" - which can be "Buy/Sell" or long/short (hedge mode).
Freqtrade supports both modes (we recommend to use net mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades. Freqtrade supports both modes (we recommend to use Buy/Sell mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades.
OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data. OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data.
## Gate.io ## Gate.io

View File

@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.4.2 mkdocs==1.4.2
mkdocs-material==9.0.8 mkdocs-material==9.0.11
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.9.1 pymdown-extensions==9.9.2
jinja2==3.1.2 jinja2==3.1.2

View File

@ -163,7 +163,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `strategy <strategy>` | Get specific Strategy content. **Alpha** | `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha** | `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version. | `version` | Show version.
| `sysinfo` | Show informations about the system load. | `sysinfo` | Show information about the system load.
| `health` | Show bot health (last bot loop). | `health` | Show bot health (last bot loop).
!!! Warning "Alpha status" !!! Warning "Alpha status"
@ -192,6 +192,11 @@ blacklist
:param add: List of coins to add (example: "BNB/BTC") :param add: List of coins to add (example: "BNB/BTC")
cancel_open_order
Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
count count
Return the amount of open trades. Return the amount of open trades.
@ -274,7 +279,6 @@ reload_config
Reload configuration. Reload configuration.
show_config show_config
Returns part of the configuration, relevant for trading operations. Returns part of the configuration, relevant for trading operations.
start start
@ -320,6 +324,7 @@ version
whitelist whitelist
Show the current whitelist. Show the current whitelist.
``` ```
### Message WebSocket ### Message WebSocket

View File

@ -162,26 +162,33 @@ official commands. You can ask at any moment for help with `/help`.
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
| **System commands**
| `/start` | Starts the trader | `/start` | Starts the trader
| `/stop` | Stops the trader | `/stop` | Stops the trader
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | Reloads the configuration file | `/reload_config` | Reloads the configuration file
| `/show_config` | Shows part of the current configuration with relevant settings to operation | `/show_config` | Shows part of the current configuration with relevant settings to operation
| `/logs [limit]` | Show last log messages. | `/logs [limit]` | Show last log messages.
| `/help` | Show help message
| `/version` | Show version
| **Status** |
| `/status` | Lists all open trades | `/status` | Lists all open trades
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space. | `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | List all recently closed trades in a table format. | `/trades [limit]` | List all recently closed trades in a table format.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available | `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id). | `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | **Modify Trade states** |
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/fx` | alias for `/forceexit` | `/fx` | alias for `/forceexit`
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True) | `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True) | `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
| **Metrics** |
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/performance` | Show performance of each finished trade grouped by pair | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency | `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
@ -193,8 +200,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing. | `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `/edge` | Show validated pairs by Edge if it is enabled. | `/edge` | Show validated pairs by Edge if it is enabled.
| `/help` | Show help message
| `/version` | Show version
## Telegram commands in action ## Telegram commands in action

View File

@ -12,7 +12,7 @@ dependencies:
- py-find-1st - py-find-1st
- aiohttp - aiohttp
- SQLAlchemy - SQLAlchemy
- python-telegram-bot - python-telegram-bot<20.0.0
- arrow - arrow
- cachetools - cachetools
- requests - requests

View File

@ -32,6 +32,7 @@ class Binance(Exchange):
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"tickers_have_price": False, "tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType", "stop_price_type_field": "workingType",
"stop_price_type_value_mapping": { "stop_price_type_value_mapping": {
PriceType.LAST: "CONTRACT_PRICE", PriceType.LAST: "CONTRACT_PRICE",
@ -88,33 +89,6 @@ class Binance(Exchange):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try:
self._api.set_leverage(symbol=pair, leverage=round(leverage))
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType, since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False, is_new_pair: bool = False, raise_: bool = False,

View File

@ -7,6 +7,7 @@ import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import floor
from threading import Lock from threading import Lock
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
@ -2514,7 +2515,9 @@ class Exchange:
if self._config['dry_run'] or not self.exchange_has("setLeverage"): if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one margin_mode type # Some exchanges only support one margin_mode type
return return
if self._ft_has.get('floor_leverage', False) is True:
# Rounding for binance ...
leverage = floor(leverage)
try: try:
res = self._api.set_leverage(symbol=pair, leverage=leverage) res = self._api.set_leverage(symbol=pair, leverage=leverage)
self._log_exchange_response('set_leverage', res) self._log_exchange_response('set_leverage', res)

View File

@ -125,13 +125,15 @@ class Okx(Exchange):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None: if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try: try:
# TODO-lev: Test me properly (check mgnMode passed) # TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage( res = self._api.set_leverage(
leverage=leverage, leverage=leverage,
symbol=pair, symbol=pair,
params={ params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False), "posSide": self._get_posSide(side, False),
}) })
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:

View File

@ -363,7 +363,7 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.") "Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object() fo = order.to_ccxt_object()
fo['status'] = 'canceled' fo['status'] = 'canceled'
self.handle_timedout_order(fo, order.trade) self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
except ExchangeError as e: except ExchangeError as e:
@ -1170,15 +1170,13 @@ class FreqtradeBot(LoggingMixin):
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order: if not stoploss_order:
stoploss = ( stop_price = trade.stoploss_or_liquidation
self.edge.stoploss(pair=trade.pair) if self.edge:
if self.edge else stoploss = self.edge.stoploss(pair=trade.pair)
trade.stop_loss_pct / trade.leverage stop_price = (
) trade.open_rate * (1 - stoploss) if trade.is_short
if trade.is_short: else trade.open_rate * (1 + stoploss)
stop_price = trade.open_rate * (1 - stoploss) )
else:
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price):
# The above will return False if the placement failed and the trade was force-sold. # The above will return False if the placement failed and the trade was force-sold.
@ -1263,11 +1261,11 @@ class FreqtradeBot(LoggingMixin):
if not_closed: if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))): trade, order_obj, datetime.now(timezone.utc))):
self.handle_timedout_order(order, trade) self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
else: else:
self.replace_order(order, order_obj, trade) self.replace_order(order, order_obj, trade)
def handle_timedout_order(self, order: Dict, trade: Trade) -> None: def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
""" """
Check if current analyzed order timed out and cancel if necessary. Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
@ -1275,10 +1273,10 @@ class FreqtradeBot(LoggingMixin):
:return: None :return: None
""" """
if order['side'] == trade.entry_side: if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, reason)
else: else:
canceled = self.handle_cancel_exit( canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT']) trade, order, reason)
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:

View File

@ -21,9 +21,9 @@ class PairLock(_DECL_BASE):
side = Column(String(25), nullable=False, default="*") side = Column(String(25), nullable=False, default="*")
reason = Column(String(255), nullable=True) reason = Column(String(255), nullable=True)
# Time the pair was locked (start time) # Time the pair was locked (start time)
lock_time = Column(DateTime, nullable=False) lock_time = Column(DateTime(), nullable=False)
# Time until the pair is locked (end time) # Time until the pair is locked (end time)
lock_end_time = Column(DateTime, nullable=False, index=True) lock_end_time = Column(DateTime(), nullable=False, index=True)
active = Column(Boolean, nullable=False, default=True, index=True) active = Column(Boolean, nullable=False, default=True, index=True)

View File

@ -46,31 +46,31 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders") trade = relationship("Trade", back_populates="orders")
# order_side can only be 'buy', 'sell' or 'stoploss' # order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side: str = Column(String(25), nullable=False) ft_order_side = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False) ft_pair = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
ft_amount = Column(Float, nullable=False) ft_amount = Column(Float(), nullable=False)
ft_price = Column(Float, nullable=False) ft_price = Column(Float(), nullable=False)
order_id: str = Column(String(255), nullable=False, index=True) order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True) symbol = Column(String(25), nullable=True)
order_type: str = Column(String(50), nullable=True) order_type = Column(String(50), nullable=True)
side = Column(String(25), nullable=True) side = Column(String(25), nullable=True)
price = Column(Float, nullable=True) price = Column(Float(), nullable=True)
average = Column(Float, nullable=True) average = Column(Float(), nullable=True)
amount = Column(Float, nullable=True) amount = Column(Float(), nullable=True)
filled = Column(Float, nullable=True) filled = Column(Float(), nullable=True)
remaining = Column(Float, nullable=True) remaining = Column(Float(), nullable=True)
cost = Column(Float, nullable=True) cost = Column(Float(), nullable=True)
stop_price = Column(Float, nullable=True) stop_price = Column(Float(), nullable=True)
order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_date = Column(DateTime(), nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime(), nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime(), nullable=True)
funding_fee = Column(Float, nullable=True) funding_fee = Column(Float(), nullable=True)
ft_fee_base = Column(Float, nullable=True) ft_fee_base = Column(Float(), nullable=True)
@property @property
def order_date_utc(self) -> datetime: def order_date_utc(self) -> datetime:
@ -1177,44 +1177,44 @@ class Trade(_DECL_BASE, LocalTrade):
base_currency = Column(String(25), nullable=True) base_currency = Column(String(25), nullable=True)
stake_currency = Column(String(25), nullable=True) stake_currency = Column(String(25), nullable=True)
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float(), nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True) fee_open_cost = Column(Float(), nullable=True)
fee_open_currency = Column(String(25), nullable=True) fee_open_currency = Column(String(25), nullable=True)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float(), nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True) fee_close_cost = Column(Float(), nullable=True)
fee_close_currency = Column(String(25), nullable=True) fee_close_currency = Column(String(25), nullable=True)
open_rate: float = Column(Float) open_rate: float = Column(Float())
open_rate_requested = Column(Float) open_rate_requested = Column(Float())
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float) open_trade_value = Column(Float())
close_rate: Optional[float] = Column(Float) close_rate: Optional[float] = Column(Float())
close_rate_requested = Column(Float) close_rate_requested = Column(Float())
realized_profit = Column(Float, default=0.0) realized_profit = Column(Float(), default=0.0)
close_profit = Column(Float) close_profit = Column(Float())
close_profit_abs = Column(Float) close_profit_abs = Column(Float())
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float(), nullable=False)
max_stake_amount = Column(Float) max_stake_amount = Column(Float())
amount = Column(Float) amount = Column(Float())
amount_requested = Column(Float) amount_requested = Column(Float())
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime(), nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime())
open_order_id = Column(String(255)) open_order_id = Column(String(255))
# absolute value of the stop loss # absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0) stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the stop loss # percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True) stop_loss_pct = Column(Float(), nullable=True)
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0) initial_stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True) initial_stop_loss_pct = Column(Float(), nullable=True)
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id = Column(String(255), nullable=True, index=True) stoploss_order_id = Column(String(255), nullable=True, index=True)
# last update time of the stoploss order on exchange # last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True) stoploss_last_update = Column(DateTime(), nullable=True)
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float(), nullable=True, default=0.0)
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float(), nullable=True)
exit_reason = Column(String(100), nullable=True) exit_reason = Column(String(100), nullable=True)
exit_order_status = Column(String(100), nullable=True) exit_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
@ -1222,21 +1222,21 @@ class Trade(_DECL_BASE, LocalTrade):
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True) trading_mode = Column(Enum(TradingMode), nullable=True)
amount_precision = Column(Float, nullable=True) amount_precision = Column(Float(), nullable=True)
price_precision = Column(Float, nullable=True) price_precision = Column(Float(), nullable=True)
precision_mode = Column(Integer, nullable=True) precision_mode = Column(Integer, nullable=True)
contract_size = Column(Float, nullable=True) contract_size = Column(Float(), nullable=True)
# Leverage trading properties # Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0) leverage = Column(Float(), nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False) is_short = Column(Boolean, nullable=False, default=False)
liquidation_price = Column(Float, nullable=True) liquidation_price = Column(Float(), nullable=True)
# Margin Trading Properties # Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float(), nullable=False, default=0.0)
# Futures properties # Futures properties
funding_fees = Column(Float, nullable=True, default=None) funding_fees = Column(Float(), nullable=True, default=None)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@ -168,6 +168,7 @@ class ShowConfig(BaseModel):
max_open_trades: IntOrInf max_open_trades: IntOrInf
minimal_roi: Dict[str, Any] minimal_roi: Dict[str, Any]
stoploss: Optional[float] stoploss: Optional[float]
stoploss_on_exchange: bool
trailing_stop: Optional[bool] trailing_stop: Optional[bool]
trailing_stop_positive: Optional[float] trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float] trailing_stop_positive_offset: Optional[float]

View File

@ -41,7 +41,8 @@ logger = logging.getLogger(__name__)
# 2.21: Add new_candle messagetype # 2.21: Add new_candle messagetype
# 2.22: Add FreqAI to backtesting # 2.22: Add FreqAI to backtesting
# 2.23: Allow plot config request in webserver mode # 2.23: Allow plot config request in webserver mode
API_VERSION = 2.23 # 2.24: Add cancel_open_order endpoint
API_VERSION = 2.24
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@ -123,6 +124,12 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete(tradeid) return rpc._rpc_delete(tradeid)
@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading'])
def cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
rpc._rpc_cancel_open_order(tradeid)
return rpc._rpc_trade_status([tradeid])[0]
# TODO: Missing response model # TODO: Missing response model
@router.get('/edge', tags=['info']) @router.get('/edge', tags=['info'])
def edge(rpc: RPC = Depends(get_rpc)): def edge(rpc: RPC = Depends(get_rpc)):

View File

@ -122,6 +122,7 @@ class RPC:
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'), 'stoploss': config.get('stoploss'),
'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
'trailing_stop': config.get('trailing_stop'), 'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
@ -812,6 +813,29 @@ class RPC:
else: else:
raise RPCException(f'Failed to enter position for {pair}.') raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_cancel_open_order(self, trade_id: int):
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._exit_lock:
# Query for trade
trade = Trade.get_trades(
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first()
if not trade:
logger.warning('cancel_open_order: Invalid trade_id received.')
raise RPCException('Invalid trade_id.')
if not trade.open_order_id:
logger.warning('cancel_open_order: No open order for trade_id.')
raise RPCException('No open order for trade_id.')
try:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
except ExchangeError as e:
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
raise RPCException("Order not found.")
self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
Trade.commit()
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
""" """
Handler for delete <id>. Handler for delete <id>.

View File

@ -174,6 +174,7 @@ class Telegram(RPCHandler):
self._force_enter, order_side=SignalDirection.SHORT)), self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades), CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade), CommandHandler('delete', self._delete_trade),
CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance), CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler(['sells', 'exits'], self._exit_reason_performance), CommandHandler(['sells', 'exits'], self._exit_reason_performance),
@ -1144,10 +1145,25 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.") raise RPCException("Trade-id not set.")
trade_id = int(context.args[0]) trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id) msg = self._rpc._rpc_delete(trade_id)
self._send_msg(( self._send_msg(
f"`{msg['result_msg']}`\n" f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.' 'Please make sure to take care of this asset on the exchange manually.'
)) )
@authorized_only
def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
:param bot: telegram bot
:param update: message update
:return: None
"""
if not context.args or len(context.args) == 0:
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
self._rpc._rpc_cancel_open_order(trade_id)
self._send_msg('Open order canceled.')
@authorized_only @authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
@ -1456,6 +1472,10 @@ class Telegram(RPCHandler):
"*/fx <trade_id>|all:* `Alias to /forceexit`\n" "*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/cancel_open_order <trade_id>:* `Cancels open orders for trade. "
"Only valid when the trade has open orders.`\n"
"*/coo <trade_id>|all:* `Alias to /cancel_open_order`\n"
"*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in "
"order and/or only displaying the base currency of each pairing.`\n" "order and/or only displaying the base currency of each pairing.`\n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "

View File

@ -10,23 +10,23 @@ coveralls==3.3.1
flake8==6.0.0 flake8==6.0.0
flake8-tidy-imports==4.8.0 flake8-tidy-imports==4.8.0
mypy==0.991 mypy==0.991
pre-commit==2.21.0 pre-commit==3.0.4
pytest==7.2.1 pytest==7.2.1
pytest-asyncio==0.20.3 pytest-asyncio==0.20.3
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
isort==5.11.4 isort==5.12.0
# For datetime mocking # For datetime mocking
time-machine==2.9.0 time-machine==2.9.0
# fastapi testing # fastapi testing
httpx==0.23.3 httpx==0.23.3
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.2.8 nbconvert==7.2.9
# mypy types # mypy types
types-cachetools==5.2.1 types-cachetools==5.3.0.0
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.28.11.8 types-requests==2.28.11.8
types-tabulate==0.9.0.0 types-tabulate==0.9.0.0

View File

@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.11.0 plotly==5.13.0

View File

@ -1,11 +1,11 @@
numpy==1.24.1 numpy==1.24.2
pandas==1.5.3 pandas==1.5.3
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==2.7.12 ccxt==2.7.66
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==38.0.1; platform_machine == 'armv7l' cryptography==38.0.1; platform_machine == 'armv7l'
cryptography==39.0.0; platform_machine != 'armv7l' cryptography==39.0.1; platform_machine != 'armv7l'
aiohttp==3.8.3 aiohttp==3.8.3
SQLAlchemy==1.4.46 SQLAlchemy==1.4.46
python-telegram-bot==13.15 python-telegram-bot==13.15
@ -15,7 +15,7 @@ requests==2.28.2
urllib3==1.26.14 urllib3==1.26.14
jsonschema==4.17.3 jsonschema==4.17.3
TA-Lib==0.4.25 TA-Lib==0.4.25
technical==1.3.0 technical==1.4.0
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
jinja2==3.1.2 jinja2==3.1.2

View File

@ -177,8 +177,7 @@ class FtRestClient():
return self._get("version") return self._get("version")
def show_config(self): def show_config(self):
""" """ Returns part of the configuration, relevant for trading operations.
Returns part of the configuration, relevant for trading operations.
:return: json object containing the version :return: json object containing the version
""" """
return self._get("show_config") return self._get("show_config")
@ -232,6 +231,14 @@ class FtRestClient():
""" """
return self._delete(f"trades/{trade_id}") return self._delete(f"trades/{trade_id}")
def cancel_open_order(self, trade_id):
"""Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
:return: json object
"""
return self._delete(f"trades/{trade_id}/open-order")
def whitelist(self): def whitelist(self):
"""Show the current whitelist. """Show the current whitelist.

View File

@ -522,8 +522,15 @@ def test__set_leverage_binance(mocker, default_conf):
api_mock.set_leverage = MagicMock() api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
default_conf['dry_run'] = False default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, id="binance") default_conf['trading_mode'] = TradingMode.FUTURES
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) default_conf['margin_mode'] = MarginMode.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange._set_leverage(3.2, 'BTC/USDT:USDT')
assert api_mock.set_leverage.call_count == 1
# Leverage is rounded to 3.
assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3
assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT'
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, mocker,

View File

@ -468,9 +468,13 @@ class TestCCXTExchange():
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
exc, exchangename = exchange exc, exchangename = exchange
# For some weired reason, this test returns random lengths for bittrex. if exchangename in ('binanceus', 'bittrex'):
if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'): # TODO: reenable binanceus test once downtime "ages out" (2023-02-06)
return # For some weired reason, this test returns random lengths for bittrex.
pytest.skip("Exchange doesn't provide stable ohlcv history")
if not exc._ft_has['ohlcv_has_history']:
pytest.skip("Exchange does not support candle history")
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe'] timeframe = EXCHANGES[exchangename]['timeframe']
self.ccxt__async_get_candle_history( self.ccxt__async_get_candle_history(

View File

@ -706,6 +706,46 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
assert_response(rc, 502) assert_response(rc, 502)
@pytest.mark.parametrize('is_short', [True, False])
def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short):
ftbot, client = botclient
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
fetch_ticker=ticker,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
rc = client_delete(client, f"{BASE_URI}/trades/10/open-order")
assert_response(rc, 502)
assert 'Invalid trade_id.' in rc.json()['error']
create_mock_trades(fee, is_short=is_short)
Trade.commit()
rc = client_delete(client, f"{BASE_URI}/trades/5/open-order")
assert_response(rc, 502)
assert 'No open order for trade_id' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
side_effect=ExchangeError)
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc, 502)
assert 'Order not found.' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc)
assert cancel_mock.call_count == 1
def test_api_logs(botclient): def test_api_logs(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/logs") rc = client_get(client, f"{BASE_URI}/logs")

View File

@ -99,7 +99,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], " "['balance'], ['start'], ['stop'], "
"['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
"['trades'], ['delete'], ['performance'], " "['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], "
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
"['stats'], ['daily'], ['weekly'], ['monthly'], " "['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['unlock', 'delete_locks'], " "['count'], ['locks'], ['unlock', 'delete_locks'], "
@ -1678,6 +1678,40 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0] assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
@pytest.mark.parametrize('is_short', [True, False])
def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
context = MagicMock()
context.args = []
telegram._cancel_open_order(update=update, context=context)
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee, is_short=is_short)
context = MagicMock()
context.args = [5]
telegram._cancel_open_order(update=update, context=context)
assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
context = MagicMock()
context.args = [6]
telegram._cancel_open_order(update=update, context=context)
assert msg_mock.call_count == 1
assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

@ -5028,7 +5028,7 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
assert log_has_re(r"Error updating Order .*", caplog) assert log_has_re(r"Error updating Order .*", caplog)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException) mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order') hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_order')
# Orders which are no longer found after X days should be assumed as canceled. # Orders which are no longer found after X days should be assumed as canceled.
freqtrade.startup_update_open_orders() freqtrade.startup_update_open_orders()
assert log_has_re(r"Order is older than \d days.*", caplog) assert log_has_re(r"Order is older than \d days.*", caplog)