stable/freqtrade/persistence.py

125 lines
4.3 KiB
Python

import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Optional, Dict
import arrow
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE = declarative_base()
def init(config: dict, engine: Optional[Engine] = None) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param engine: database engine for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not engine:
if _CONF.get('dry_run', False):
# the user wants dry run to use a DB
if _CONF.get('dry_run_db', False):
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
# Otherwise dry run will store in memory
else:
engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else:
engine = create_engine('sqlite:///tradesv3.sqlite')
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine)
def cleanup() -> None:
"""
Flushes all pending operations to disk.
:return: None
"""
Trade.session.flush()
class Trade(_DECL_BASE):
__tablename__ = 'trades'
id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
fee = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
close_rate = Column(Float)
close_profit = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
self.id,
self.pair,
self.amount,
self.open_rate,
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
)
def update(self, order: Dict) -> None:
"""
Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.get_order()
:return: None
"""
if not order['closed']:
return
logger.info('Updating trade (id=%d) ...', self.id)
if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount
self.open_rate = order['rate']
self.amount = order['amount']
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
self.close_date = datetime.utcnow()
self.is_open = False
logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
self
)
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
self.open_order_id = None
Trade.session.flush()
def calc_profit(self, rate: Optional[float] = None) -> float:
"""
Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in percentage as float
"""
getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee))