diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d32f79a3f..34d57ae4d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -487,6 +487,7 @@ class Exchange: 'side': side, 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().timestamp, 'status': "closed" if ordertype == "market" else "open", 'fee': None, 'info': {} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a95f58fc..ff282aa77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Trade +from freqtrade.persistence import Order, Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -527,6 +527,7 @@ class FreqtradeBot: order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) + order_obj = Order.parse_from_ccxt_object(order, pair) order_id = order['id'] order_status = order.get('status', None) @@ -580,6 +581,7 @@ class FreqtradeBot: strategy=self.strategy.get_strategy_name(), timeframe=timeframe_to_minutes(self.config['timeframe']) ) + trade.orders.append(order_obj) # Update fees if order is closed if order_status == 'closed': @@ -781,6 +783,9 @@ class FreqtradeBot: stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, stop_price=stop_price, order_types=self.strategy.order_types) + + order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair) + trade.orders.append(order_obj) trade.stoploss_order_id = str(stoploss_order['id']) return True except InvalidOrderException as e: @@ -1123,12 +1128,15 @@ class FreqtradeBot: return False # Execute sell and update trade record - order = self.exchange.sell(pair=str(trade.pair), + order = self.exchange.sell(pair=trade.pair, ordertype=order_type, amount=amount, rate=limit, time_in_force=time_in_force ) + order_obj = Order.parse_from_ccxt_object(order, trade.pair) + trade.orders.append(order_obj) + trade.open_order_id = order['id'] trade.close_rate_requested = limit trade.sell_reason = sell_reason.value diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 0973fab3f..764856f2b 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Trade, clean_dry_run_db, cleanup, init +from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db, + cleanup, init) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f56ff47fe..3b77438ea 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,11 +7,11 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, +from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, ForeignKey, create_engine, desc, func) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Query +from sqlalchemy.orm import Query, relationship from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool @@ -85,13 +85,71 @@ def clean_dry_run_db() -> None: trade.open_order_id = None +class Order(_DECL_BASE): + """ + Order database model + Keeps a record of all orders placed on the exchange + + One to many relationship with Trades: + - One trade can have many orders + - One Order can only be associated with one Trade + + Mirrors CCXT Order structure + """ + __tablename__ = 'orders' + + id = Column(Integer, primary_key=True) + trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + + order_id = Column(String, nullable=False, index=True) + status = Column(String, nullable=False) + symbol = Column(String, nullable=False) + order_type = Column(String, nullable=False) + side = Column(String, nullable=False) + price = Column(Float, nullable=False) + amount = Column(Float, nullable=False) + filled = Column(Float, nullable=True) + remaining = Column(Float, nullable=True) + cost = Column(Float, nullable=True) + order_date = Column(DateTime, nullable=False, default=datetime.utcnow) + order_filled_date = Column(DateTime, nullable=True) + + @staticmethod + def parse_from_ccxt_object(order, pair) -> 'Order': + """ + Parse an order from a ccxt object and return a new order Object. + """ + o = Order(order_id=str(order['id'])) + + o.status = order['status'] + o.symbol = order.get('symbol', pair) + o.order_type = order['type'] + o.side = order['side'] + o.price = order['price'] + o.amount = order['amount'] + o.filled = order.get('filled') + o.remaining = order.get('remaining') + o.cost = order.get('cost') + o.order_date = datetime.fromtimestamp(order['timestamp']) + return o + + def __repr__(self): + + return (f'Order(id={self.id}, trade_id={self.trade_id}, side={self.side}, ' + f'status={self.status})') + + class Trade(_DECL_BASE): """ - Class used to define a trade structure + Trade database model. + Also handles updating and querying trades """ __tablename__ = 'trades' id = Column(Integer, primary_key=True) + + orders = relationship("Order", order_by="Order.id") + exchange = Column(String, nullable=False) pair = Column(String, nullable=False, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)