mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into fut/stop_price_type
This commit is contained in:
commit
eab724fe54
|
@ -2,7 +2,7 @@
|
|||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "4.0.1"
|
||||
rev: "6.0.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
# stages: [push]
|
||||
|
@ -13,7 +13,7 @@ repos:
|
|||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.2.1
|
||||
- types-cachetools==5.3.0.0
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.28.11.8
|
||||
- types-tabulate==0.9.0.0
|
||||
|
@ -21,14 +21,14 @@ repos:
|
|||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: "5.10.1"
|
||||
rev: "5.12.0"
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: |
|
||||
|
|
|
@ -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.
|
||||
|
||||
!!! Warning "Futures"
|
||||
OKX Futures has the concept of "position mode" - which can be Net 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.
|
||||
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 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.
|
||||
|
||||
## Gate.io
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
markdown==3.3.7
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.0.8
|
||||
mkdocs-material==9.0.11
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==9.9.1
|
||||
pymdown-extensions==9.9.2
|
||||
jinja2==3.1.2
|
||||
|
|
|
@ -163,7 +163,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
|
||||
| `available_pairs` | List available backtest data. **Alpha**
|
||||
| `version` | Show version.
|
||||
| `sysinfo` | Show informations about the system load.
|
||||
| `sysinfo` | Show information about the system load.
|
||||
| `health` | Show bot health (last bot loop).
|
||||
|
||||
!!! Warning "Alpha status"
|
||||
|
@ -192,6 +192,11 @@ blacklist
|
|||
|
||||
: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
|
||||
Return the amount of open trades.
|
||||
|
||||
|
@ -274,7 +279,6 @@ reload_config
|
|||
Reload configuration.
|
||||
|
||||
show_config
|
||||
|
||||
Returns part of the configuration, relevant for trading operations.
|
||||
|
||||
start
|
||||
|
@ -320,6 +324,7 @@ version
|
|||
whitelist
|
||||
Show the current whitelist.
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Message WebSocket
|
||||
|
|
|
@ -162,26 +162,33 @@ official commands. You can ask at any moment for help with `/help`.
|
|||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
| **System commands**
|
||||
| `/start` | Starts the trader
|
||||
| `/stop` | Stops the trader
|
||||
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | Reloads the configuration file
|
||||
| `/show_config` | Shows part of the current configuration with relevant settings to operation
|
||||
| `/logs [limit]` | Show last log messages.
|
||||
| `/help` | Show help message
|
||||
| `/version` | Show version
|
||||
| **Status** |
|
||||
| `/status` | Lists all open trades
|
||||
| `/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 (**)
|
||||
| `/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
|
||||
| `/locks` | Show currently locked pairs.
|
||||
| `/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 all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||
| `/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)
|
||||
| `/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
|
||||
| `/balance` | Show account balance per currency
|
||||
| `/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.
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||
| `/help` | Show help message
|
||||
| `/version` | Show version
|
||||
|
||||
|
||||
## Telegram commands in action
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ dependencies:
|
|||
- py-find-1st
|
||||
- aiohttp
|
||||
- SQLAlchemy
|
||||
- python-telegram-bot
|
||||
- python-telegram-bot<20.0.0
|
||||
- arrow
|
||||
- cachetools
|
||||
- requests
|
||||
|
|
|
@ -32,6 +32,7 @@ class Binance(Exchange):
|
|||
_ft_has_futures: Dict = {
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"stop_price_type_field": "workingType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "CONTRACT_PRICE",
|
||||
|
@ -88,33 +89,6 @@ class Binance(Exchange):
|
|||
except ccxt.BaseError as 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,
|
||||
since_ms: int, candle_type: CandleType,
|
||||
is_new_pair: bool = False, raise_: bool = False,
|
||||
|
|
|
@ -7,6 +7,7 @@ import inspect
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import floor
|
||||
from threading import Lock
|
||||
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"):
|
||||
# Some exchanges only support one margin_mode type
|
||||
return
|
||||
|
||||
if self._ft_has.get('floor_leverage', False) is True:
|
||||
# Rounding for binance ...
|
||||
leverage = floor(leverage)
|
||||
try:
|
||||
res = self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||
self._log_exchange_response('set_leverage', res)
|
||||
|
|
|
@ -125,13 +125,15 @@ class Okx(Exchange):
|
|||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||
try:
|
||||
# TODO-lev: Test me properly (check mgnMode passed)
|
||||
self._api.set_leverage(
|
||||
res = self._api.set_leverage(
|
||||
leverage=leverage,
|
||||
symbol=pair,
|
||||
params={
|
||||
"mgnMode": self.margin_mode.value,
|
||||
"posSide": self._get_posSide(side, False),
|
||||
})
|
||||
self._log_exchange_response('set_leverage', res)
|
||||
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
|
|
|
@ -363,7 +363,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
"Order is older than 5 days. Assuming order was fully cancelled.")
|
||||
fo = order.to_ccxt_object()
|
||||
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:
|
||||
|
||||
|
@ -1170,15 +1170,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if not stoploss_order:
|
||||
stoploss = (
|
||||
self.edge.stoploss(pair=trade.pair)
|
||||
if self.edge else
|
||||
trade.stop_loss_pct / trade.leverage
|
||||
)
|
||||
if trade.is_short:
|
||||
stop_price = trade.open_rate * (1 - stoploss)
|
||||
else:
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
stop_price = trade.stoploss_or_liquidation
|
||||
if self.edge:
|
||||
stoploss = self.edge.stoploss(pair=trade.pair)
|
||||
stop_price = (
|
||||
trade.open_rate * (1 - stoploss) if trade.is_short
|
||||
else trade.open_rate * (1 + stoploss)
|
||||
)
|
||||
|
||||
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.
|
||||
|
@ -1263,11 +1261,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
if not_closed:
|
||||
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
|
||||
trade, order_obj, datetime.now(timezone.utc))):
|
||||
self.handle_timedout_order(order, trade)
|
||||
self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
|
||||
else:
|
||||
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.
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
|
@ -1275,10 +1273,10 @@ class FreqtradeBot(LoggingMixin):
|
|||
:return: None
|
||||
"""
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
self.handle_cancel_enter(trade, order, reason)
|
||||
else:
|
||||
canceled = self.handle_cancel_exit(
|
||||
trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
trade, order, reason)
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
|
|
|
@ -21,9 +21,9 @@ class PairLock(_DECL_BASE):
|
|||
side = Column(String(25), nullable=False, default="*")
|
||||
reason = Column(String(255), nullable=True)
|
||||
# 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)
|
||||
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)
|
||||
|
||||
|
|
|
@ -46,31 +46,31 @@ class Order(_DECL_BASE):
|
|||
trade = relationship("Trade", back_populates="orders")
|
||||
|
||||
# order_side can only be 'buy', 'sell' or 'stoploss'
|
||||
ft_order_side: str = Column(String(25), nullable=False)
|
||||
ft_pair: str = Column(String(25), nullable=False)
|
||||
ft_order_side = Column(String(25), nullable=False)
|
||||
ft_pair = Column(String(25), nullable=False)
|
||||
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||
ft_amount = Column(Float, nullable=False)
|
||||
ft_price = Column(Float, nullable=False)
|
||||
ft_amount = 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)
|
||||
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)
|
||||
price = Column(Float, nullable=True)
|
||||
average = Column(Float, nullable=True)
|
||||
amount = Column(Float, nullable=True)
|
||||
filled = Column(Float, nullable=True)
|
||||
remaining = Column(Float, nullable=True)
|
||||
cost = Column(Float, nullable=True)
|
||||
stop_price = Column(Float, nullable=True)
|
||||
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
|
||||
order_filled_date = Column(DateTime, nullable=True)
|
||||
order_update_date = Column(DateTime, nullable=True)
|
||||
price = Column(Float(), nullable=True)
|
||||
average = Column(Float(), nullable=True)
|
||||
amount = Column(Float(), nullable=True)
|
||||
filled = Column(Float(), nullable=True)
|
||||
remaining = Column(Float(), nullable=True)
|
||||
cost = Column(Float(), nullable=True)
|
||||
stop_price = Column(Float(), nullable=True)
|
||||
order_date = Column(DateTime(), nullable=True, default=datetime.utcnow)
|
||||
order_filled_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
|
||||
def order_date_utc(self) -> datetime:
|
||||
|
@ -1177,44 +1177,44 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||
base_currency = Column(String(25), nullable=True)
|
||||
stake_currency = Column(String(25), nullable=True)
|
||||
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||
fee_open = Column(Float, nullable=False, default=0.0)
|
||||
fee_open_cost = Column(Float, nullable=True)
|
||||
fee_open = Column(Float(), nullable=False, default=0.0)
|
||||
fee_open_cost = Column(Float(), nullable=True)
|
||||
fee_open_currency = Column(String(25), nullable=True)
|
||||
fee_close = Column(Float, nullable=False, default=0.0)
|
||||
fee_close_cost = Column(Float, nullable=True)
|
||||
fee_close = Column(Float(), nullable=False, default=0.0)
|
||||
fee_close_cost = Column(Float(), nullable=True)
|
||||
fee_close_currency = Column(String(25), nullable=True)
|
||||
open_rate: float = Column(Float)
|
||||
open_rate_requested = Column(Float)
|
||||
open_rate: float = Column(Float())
|
||||
open_rate_requested = Column(Float())
|
||||
# open_trade_value - calculated via _calc_open_trade_value
|
||||
open_trade_value = Column(Float)
|
||||
close_rate: Optional[float] = Column(Float)
|
||||
close_rate_requested = Column(Float)
|
||||
realized_profit = Column(Float, default=0.0)
|
||||
close_profit = Column(Float)
|
||||
close_profit_abs = Column(Float)
|
||||
stake_amount = Column(Float, nullable=False)
|
||||
max_stake_amount = Column(Float)
|
||||
amount = Column(Float)
|
||||
amount_requested = Column(Float)
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
open_trade_value = Column(Float())
|
||||
close_rate: Optional[float] = Column(Float())
|
||||
close_rate_requested = Column(Float())
|
||||
realized_profit = Column(Float(), default=0.0)
|
||||
close_profit = Column(Float())
|
||||
close_profit_abs = Column(Float())
|
||||
stake_amount = Column(Float(), nullable=False)
|
||||
max_stake_amount = Column(Float())
|
||||
amount = Column(Float())
|
||||
amount_requested = Column(Float())
|
||||
open_date = Column(DateTime(), nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime())
|
||||
open_order_id = Column(String(255))
|
||||
# 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
|
||||
stop_loss_pct = Column(Float, nullable=True)
|
||||
stop_loss_pct = Column(Float(), nullable=True)
|
||||
# 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
|
||||
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 = Column(String(255), nullable=True, index=True)
|
||||
# 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
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
max_rate = Column(Float(), nullable=True, default=0.0)
|
||||
# Lowest price reached
|
||||
min_rate = Column(Float, nullable=True)
|
||||
min_rate = Column(Float(), nullable=True)
|
||||
exit_reason = Column(String(100), nullable=True)
|
||||
exit_order_status = 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)
|
||||
|
||||
trading_mode = Column(Enum(TradingMode), nullable=True)
|
||||
amount_precision = Column(Float, nullable=True)
|
||||
price_precision = Column(Float, nullable=True)
|
||||
amount_precision = Column(Float(), nullable=True)
|
||||
price_precision = Column(Float(), nullable=True)
|
||||
precision_mode = Column(Integer, nullable=True)
|
||||
contract_size = Column(Float, nullable=True)
|
||||
contract_size = Column(Float(), nullable=True)
|
||||
|
||||
# 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)
|
||||
liquidation_price = Column(Float, nullable=True)
|
||||
liquidation_price = Column(Float(), nullable=True)
|
||||
|
||||
# Margin Trading Properties
|
||||
interest_rate = Column(Float, nullable=False, default=0.0)
|
||||
interest_rate = Column(Float(), nullable=False, default=0.0)
|
||||
|
||||
# Futures properties
|
||||
funding_fees = Column(Float, nullable=True, default=None)
|
||||
funding_fees = Column(Float(), nullable=True, default=None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -168,6 +168,7 @@ class ShowConfig(BaseModel):
|
|||
max_open_trades: IntOrInf
|
||||
minimal_roi: Dict[str, Any]
|
||||
stoploss: Optional[float]
|
||||
stoploss_on_exchange: bool
|
||||
trailing_stop: Optional[bool]
|
||||
trailing_stop_positive: Optional[float]
|
||||
trailing_stop_positive_offset: Optional[float]
|
||||
|
|
|
@ -41,7 +41,8 @@ logger = logging.getLogger(__name__)
|
|||
# 2.21: Add new_candle messagetype
|
||||
# 2.22: Add FreqAI to backtesting
|
||||
# 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.
|
||||
router_public = APIRouter()
|
||||
|
@ -123,6 +124,12 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
|||
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
|
||||
@router.get('/edge', tags=['info'])
|
||||
def edge(rpc: RPC = Depends(get_rpc)):
|
||||
|
|
|
@ -122,6 +122,7 @@ class RPC:
|
|||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
||||
'stoploss': config.get('stoploss'),
|
||||
'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
|
||||
'trailing_stop': config.get('trailing_stop'),
|
||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||
|
@ -812,6 +813,29 @@ class RPC:
|
|||
else:
|
||||
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]]:
|
||||
"""
|
||||
Handler for delete <id>.
|
||||
|
|
|
@ -174,6 +174,7 @@ class Telegram(RPCHandler):
|
|||
self._force_enter, order_side=SignalDirection.SHORT)),
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('delete', self._delete_trade),
|
||||
CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
|
||||
CommandHandler(['sells', 'exits'], self._exit_reason_performance),
|
||||
|
@ -1144,10 +1145,25 @@ class Telegram(RPCHandler):
|
|||
raise RPCException("Trade-id not set.")
|
||||
trade_id = int(context.args[0])
|
||||
msg = self._rpc._rpc_delete(trade_id)
|
||||
self._send_msg((
|
||||
self._send_msg(
|
||||
f"`{msg['result_msg']}`\n"
|
||||
'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
|
||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||
|
@ -1456,6 +1472,10 @@ class Telegram(RPCHandler):
|
|||
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
|
||||
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"
|
||||
"*/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 "
|
||||
"order and/or only displaying the base currency of each pairing.`\n"
|
||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||
|
|
|
@ -10,23 +10,23 @@ coveralls==3.3.1
|
|||
flake8==6.0.0
|
||||
flake8-tidy-imports==4.8.0
|
||||
mypy==0.991
|
||||
pre-commit==2.21.0
|
||||
pre-commit==3.0.4
|
||||
pytest==7.2.1
|
||||
pytest-asyncio==0.20.3
|
||||
pytest-cov==4.0.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-random-order==1.1.0
|
||||
isort==5.11.4
|
||||
isort==5.12.0
|
||||
# For datetime mocking
|
||||
time-machine==2.9.0
|
||||
# fastapi testing
|
||||
httpx==0.23.3
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.2.8
|
||||
nbconvert==7.2.9
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.2.1
|
||||
types-cachetools==5.3.0.0
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.28.11.8
|
||||
types-tabulate==0.9.0.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.11.0
|
||||
plotly==5.13.0
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
numpy==1.24.1
|
||||
numpy==1.24.2
|
||||
pandas==1.5.3
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==2.7.12
|
||||
ccxt==2.7.66
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
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
|
||||
SQLAlchemy==1.4.46
|
||||
python-telegram-bot==13.15
|
||||
|
@ -15,7 +15,7 @@ requests==2.28.2
|
|||
urllib3==1.26.14
|
||||
jsonschema==4.17.3
|
||||
TA-Lib==0.4.25
|
||||
technical==1.3.0
|
||||
technical==1.4.0
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.1.0
|
||||
jinja2==3.1.2
|
||||
|
|
|
@ -177,8 +177,7 @@ class FtRestClient():
|
|||
return self._get("version")
|
||||
|
||||
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 self._get("show_config")
|
||||
|
@ -232,6 +231,14 @@ class FtRestClient():
|
|||
"""
|
||||
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):
|
||||
"""Show the current whitelist.
|
||||
|
||||
|
|
|
@ -522,8 +522,15 @@ def test__set_leverage_binance(mocker, default_conf):
|
|||
api_mock.set_leverage = MagicMock()
|
||||
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
|
||||
default_conf['dry_run'] = False
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN)
|
||||
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||
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(
|
||||
mocker,
|
||||
|
|
|
@ -468,9 +468,13 @@ class TestCCXTExchange():
|
|||
|
||||
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exc, exchangename = exchange
|
||||
# For some weired reason, this test returns random lengths for bittrex.
|
||||
if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
|
||||
return
|
||||
if exchangename in ('binanceus', 'bittrex'):
|
||||
# TODO: reenable binanceus test once downtime "ages out" (2023-02-06)
|
||||
# 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']
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
self.ccxt__async_get_candle_history(
|
||||
|
|
|
@ -706,6 +706,46 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
|||
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):
|
||||
ftbot, client = botclient
|
||||
rc = client_get(client, f"{BASE_URI}/logs")
|
||||
|
|
|
@ -99,7 +99,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
|||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||
"['balance'], ['start'], ['stop'], "
|
||||
"['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
|
||||
"['trades'], ['delete'], ['performance'], "
|
||||
"['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], "
|
||||
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
|
||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||
"['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]
|
||||
|
||||
|
||||
@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:
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
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.
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert log_has_re(r"Order is older than \d days.*", caplog)
|
||||
|
|
Loading…
Reference in New Issue
Block a user