Merge branch 'develop' into feature_keyval_storage
This commit is contained in:
@@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||
'is_short'
|
||||
'is_short', 'open_timestamp', 'close_timestamp', 'orders'
|
||||
]
|
||||
|
||||
|
||||
@@ -283,6 +283,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
||||
if 'enter_tag' not in df.columns:
|
||||
df['enter_tag'] = df['buy_tag']
|
||||
df = df.drop(['buy_tag'], axis=1)
|
||||
if 'orders' not in df.columns:
|
||||
df.loc[:, 'orders'] = None
|
||||
|
||||
else:
|
||||
# old format - only with lists.
|
||||
@@ -337,7 +339,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
||||
:param trades: List of trade objects
|
||||
:return: Dataframe with BT_DATA_COLUMNS
|
||||
"""
|
||||
df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS)
|
||||
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
||||
if len(df) > 0:
|
||||
df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||
df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime, time, timezone
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -227,7 +227,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
Notify the user when the bot is stopped (not reloaded)
|
||||
and there are still open trades active.
|
||||
"""
|
||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
||||
open_trades = Trade.get_open_trades()
|
||||
|
||||
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||
msg = {
|
||||
@@ -302,6 +302,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(order.trade, order.order_id, fo,
|
||||
stoploss_order=(order.ft_order_side == 'stoploss'))
|
||||
|
||||
except InvalidOrderException as e:
|
||||
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
|
||||
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
|
||||
logger.warning(
|
||||
"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)
|
||||
|
||||
except ExchangeError as e:
|
||||
|
||||
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
||||
|
||||
@@ -969,6 +969,7 @@ class Backtesting:
|
||||
return False
|
||||
else:
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
self.canceled_entry_orders += 1
|
||||
|
||||
# place new order if result was not None
|
||||
@@ -1097,6 +1098,7 @@ class Backtesting:
|
||||
# 5. Process exit orders.
|
||||
order = trade.select_order(trade.exit_side, is_open=True)
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.open_order_id = None
|
||||
trade.close_date = current_time
|
||||
trade.close(order.price, show_msg=False)
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from numpy import int64
|
||||
from pandas import DataFrame, to_datetime
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -417,9 +416,6 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
if not results.empty:
|
||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||
|
||||
backtest_days = (max_date - min_date).days or 1
|
||||
strat_stats = {
|
||||
|
||||
@@ -247,6 +247,35 @@ def set_sqlite_to_wal(engine):
|
||||
connection.execute(text("PRAGMA journal_mode=wal"))
|
||||
|
||||
|
||||
def fix_old_dry_orders(engine):
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
|
||||
select id, stoploss_order_id from trades where stoploss_order_id is not null
|
||||
) and ft_order_side = 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1
|
||||
and (ft_trade_id, order_id) not in (
|
||||
select id, open_order_id from trades where open_order_id is not null
|
||||
) and ft_order_side != 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"""
|
||||
Checks if migration is necessary and migrates if necessary
|
||||
@@ -288,3 +317,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"start with a fresh database.")
|
||||
|
||||
set_sqlite_to_wal(engine)
|
||||
fix_old_dry_orders(engine)
|
||||
|
||||
@@ -139,35 +139,40 @@ class Order(_DECL_BASE):
|
||||
'info': {},
|
||||
}
|
||||
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
'status': self.status,
|
||||
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
|
||||
resp = {
|
||||
'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,
|
||||
'is_open': self.ft_is_open,
|
||||
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_date else None,
|
||||
'order_timestamp': int(self.order_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
||||
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_filled_date else None,
|
||||
'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,
|
||||
'price': self.price,
|
||||
'ft_is_entry': self.ft_order_side == entry_side,
|
||||
'remaining': self.remaining,
|
||||
}
|
||||
if not minified:
|
||||
resp.update({
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
'status': self.status,
|
||||
'average': round(self.average, 8) if self.average else 0,
|
||||
'cost': self.cost if self.cost else 0,
|
||||
'filled': self.filled,
|
||||
'is_open': self.ft_is_open,
|
||||
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_date else None,
|
||||
'order_timestamp': int(self.order_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
||||
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
})
|
||||
return resp
|
||||
|
||||
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
|
||||
self.order_filled_date = close_date
|
||||
self.filled = self.amount
|
||||
self.remaining = 0
|
||||
self.status = 'closed'
|
||||
self.ft_is_open = False
|
||||
if (self.ft_order_side == trade.entry_side
|
||||
@@ -396,9 +401,9 @@ class LocalTrade():
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
orders = [order.to_json(self.entry_side) for order in filled_orders]
|
||||
def to_json(self, minified: bool = False) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_or_open_orders()
|
||||
orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@@ -900,6 +905,21 @@ class LocalTrade():
|
||||
(o.filled or 0) > 0 and
|
||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
def select_filled_or_open_orders(self) -> List['Order']:
|
||||
"""
|
||||
Finds filled or open orders
|
||||
:param order_side: Side of the order (either 'buy', 'sell', or None)
|
||||
:return: array of Order objects
|
||||
"""
|
||||
return [o for o in self.orders if
|
||||
(
|
||||
o.ft_is_open is False
|
||||
and (o.filled or 0) > 0
|
||||
and o.status in NON_OPEN_EXCHANGE_STATES
|
||||
)
|
||||
or (o.ft_is_open is True and o.status is not None)
|
||||
]
|
||||
|
||||
def set_kval(self, key: str, value: Any) -> None:
|
||||
KeyValues.set_kval(key=key, value=value, trade_id=self.id)
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ class ShowConfig(BaseModel):
|
||||
trailing_stop_positive: Optional[float]
|
||||
trailing_stop_positive_offset: Optional[float]
|
||||
trailing_only_offset_is_reached: Optional[bool]
|
||||
unfilledtimeout: UnfilledTimeout
|
||||
unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode
|
||||
order_types: Optional[OrderTypes]
|
||||
use_custom_stoploss: Optional[bool]
|
||||
timeframe: Optional[str]
|
||||
|
||||
@@ -396,7 +396,7 @@ class Telegram(RPCHandler):
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if not order['ft_is_entry']:
|
||||
if not order['ft_is_entry'] or order['is_open'] is True:
|
||||
continue
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
|
||||
Reference in New Issue
Block a user