2018-03-02 15:22:00 +00:00
|
|
|
"""
|
|
|
|
This module contains the class to persist trades into SQLite
|
|
|
|
"""
|
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
import logging
|
2017-05-12 17:11:56 +00:00
|
|
|
from datetime import datetime
|
2017-11-01 02:27:08 +00:00
|
|
|
from decimal import Decimal, getcontext
|
2018-01-10 07:51:36 +00:00
|
|
|
from typing import Dict, Optional
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
import arrow
|
2018-01-10 07:51:36 +00:00
|
|
|
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
|
|
|
create_engine)
|
2017-11-09 22:45:22 +00:00
|
|
|
from sqlalchemy.engine import Engine
|
2017-05-12 17:11:56 +00:00
|
|
|
from sqlalchemy.ext.declarative import declarative_base
|
2017-09-03 06:50:48 +00:00
|
|
|
from sqlalchemy.orm.scoping import scoped_session
|
|
|
|
from sqlalchemy.orm.session import sessionmaker
|
2017-11-09 22:45:22 +00:00
|
|
|
from sqlalchemy.pool import StaticPool
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2017-09-08 21:10:22 +00:00
|
|
|
_CONF = {}
|
2017-11-07 19:13:36 +00:00
|
|
|
_DECL_BASE = declarative_base()
|
2017-05-12 17:11:56 +00:00
|
|
|
|
|
|
|
|
2017-11-09 22:45:22 +00:00
|
|
|
def init(config: dict, engine: Optional[Engine] = None) -> None:
|
2017-09-08 13:51:00 +00:00
|
|
|
"""
|
|
|
|
Initializes this module with the given config,
|
|
|
|
registers all known command handlers
|
|
|
|
and starts polling for message updates
|
|
|
|
:param config: config to use
|
2017-11-09 22:45:22 +00:00
|
|
|
:param engine: database engine for sqlalchemy (Optional)
|
2017-09-08 13:51:00 +00:00
|
|
|
:return: None
|
|
|
|
"""
|
2017-09-08 21:10:22 +00:00
|
|
|
_CONF.update(config)
|
2017-11-09 22:45:22 +00:00
|
|
|
if not engine:
|
2017-09-08 21:10:22 +00:00
|
|
|
if _CONF.get('dry_run', False):
|
2017-12-14 14:10:11 +00:00
|
|
|
# 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)
|
2017-09-08 19:39:31 +00:00
|
|
|
else:
|
2017-11-09 22:45:22 +00:00
|
|
|
engine = create_engine('sqlite:///tradesv3.sqlite')
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2017-09-08 19:17:58 +00:00
|
|
|
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
|
|
|
Trade.session = session()
|
|
|
|
Trade.query = session.query_property()
|
2017-11-07 19:13:36 +00:00
|
|
|
_DECL_BASE.metadata.create_all(engine)
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2018-01-23 07:23:29 +00:00
|
|
|
# Clean dry_run DB
|
|
|
|
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
|
|
|
clean_dry_run_db()
|
|
|
|
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2017-10-27 13:52:14 +00:00
|
|
|
def cleanup() -> None:
|
|
|
|
"""
|
|
|
|
Flushes all pending operations to disk.
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
Trade.session.flush()
|
|
|
|
|
|
|
|
|
2018-01-23 07:23:29 +00:00
|
|
|
def clean_dry_run_db() -> None:
|
|
|
|
"""
|
|
|
|
Remove open_order_id from a Dry_run DB
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
|
|
|
# Check we are updating only a dry_run order not a prod one
|
|
|
|
if 'dry_run' in trade.open_order_id:
|
|
|
|
trade.open_order_id = None
|
|
|
|
|
|
|
|
|
2017-11-07 19:13:36 +00:00
|
|
|
class Trade(_DECL_BASE):
|
2018-03-02 15:22:00 +00:00
|
|
|
"""
|
|
|
|
Class used to define a trade structure
|
|
|
|
"""
|
2017-05-12 17:11:56 +00:00
|
|
|
__tablename__ = 'trades'
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True)
|
2017-10-06 10:22:04 +00:00
|
|
|
exchange = Column(String, nullable=False)
|
2017-05-12 17:11:56 +00:00
|
|
|
pair = Column(String, nullable=False)
|
|
|
|
is_open = Column(Boolean, nullable=False, default=True)
|
2018-04-21 17:47:08 +00:00
|
|
|
fee_open = Column(Float, nullable=False, default=0.0)
|
|
|
|
fee_close = Column(Float, nullable=False, default=0.0)
|
2017-10-31 23:22:38 +00:00
|
|
|
open_rate = Column(Float)
|
2017-05-12 17:11:56 +00:00
|
|
|
close_rate = Column(Float)
|
|
|
|
close_profit = Column(Float)
|
2017-11-01 01:20:55 +00:00
|
|
|
stake_amount = Column(Float, nullable=False)
|
2017-10-31 23:22:38 +00:00
|
|
|
amount = Column(Float)
|
2017-05-12 17:11:56 +00:00
|
|
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
|
|
|
close_date = Column(DateTime)
|
2017-05-14 12:14:16 +00:00
|
|
|
open_order_id = Column(String)
|
2018-05-10 17:03:48 +00:00
|
|
|
stop_loss = Column(Float, nullable=False, default=0.0) # absolute value of the stop loss
|
2017-05-12 17:11:56 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
2017-11-23 19:52:07 +00:00
|
|
|
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
2017-05-12 17:11:56 +00:00
|
|
|
self.id,
|
|
|
|
self.pair,
|
|
|
|
self.amount,
|
|
|
|
self.open_rate,
|
2017-10-31 23:22:38 +00:00
|
|
|
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
2017-05-12 17:11:56 +00:00
|
|
|
)
|
2017-05-12 22:30:08 +00:00
|
|
|
|
2018-05-10 17:03:48 +00:00
|
|
|
def adjust_stop_loss(self, current_price, stoploss):
|
|
|
|
"""
|
|
|
|
|
|
|
|
this adjusts the stop loss to it's most recently observed
|
|
|
|
setting
|
|
|
|
:param current_price:
|
|
|
|
:param stoploss:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
|
|
|
|
new_loss = Decimal(current_price * (1 - abs(stoploss)))
|
|
|
|
logger.debug("calculated stop loss at: {:.6f}".format(new_loss))
|
|
|
|
if self.stop_loss is None:
|
|
|
|
logger.debug("assigning new stop loss")
|
|
|
|
self.stop_loss = new_loss # no stop loss assigned yet
|
|
|
|
else:
|
|
|
|
if _CONF.get('trailing_stop', True):
|
|
|
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
|
|
|
self.stop_loss = new_loss
|
|
|
|
logger.debug("adjusted stop loss for {:.6f} and {:.6f} to {:.6f}".format(
|
|
|
|
current_price, stoploss, self.stop_loss)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
logger.debug("keeping current stop loss of {:.6f}".format(self.stop_loss))
|
|
|
|
else:
|
|
|
|
print("utilizing fixed stop")
|
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
def update(self, order: Dict) -> None:
|
2017-06-08 18:01:01 +00:00
|
|
|
"""
|
2017-10-31 23:22:38 +00:00
|
|
|
Updates this entity with amount and actual open/close rates.
|
|
|
|
:param order: order retrieved by exchange.get_order()
|
|
|
|
:return: None
|
2017-06-08 18:01:01 +00:00
|
|
|
"""
|
2017-12-16 01:36:43 +00:00
|
|
|
# Ignore open and cancelled orders
|
2018-03-25 20:25:26 +00:00
|
|
|
if order['status'] == 'open' or order['price'] is None:
|
2017-10-31 23:22:38 +00:00
|
|
|
return
|
|
|
|
|
2017-11-17 19:17:29 +00:00
|
|
|
logger.info('Updating trade (id=%d) ...', self.id)
|
2017-12-17 21:07:56 +00:00
|
|
|
|
|
|
|
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
2018-03-25 20:25:26 +00:00
|
|
|
if order['type'] == 'limit' and order['side'] == 'buy':
|
2017-10-31 23:54:16 +00:00
|
|
|
# Update open rate and actual amount
|
2018-03-25 20:25:26 +00:00
|
|
|
self.open_rate = Decimal(order['price'])
|
2017-12-17 21:07:56 +00:00
|
|
|
self.amount = Decimal(order['amount'])
|
2017-11-17 19:17:29 +00:00
|
|
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
2017-12-16 00:09:07 +00:00
|
|
|
self.open_order_id = None
|
2018-03-25 20:25:26 +00:00
|
|
|
elif order['type'] == 'limit' and order['side'] == 'sell':
|
|
|
|
self.close(order['price'])
|
2017-10-31 23:22:38 +00:00
|
|
|
else:
|
|
|
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
2017-12-27 10:41:11 +00:00
|
|
|
cleanup()
|
2017-06-08 18:01:01 +00:00
|
|
|
|
2017-12-16 00:09:07 +00:00
|
|
|
def close(self, rate: float) -> None:
|
|
|
|
"""
|
|
|
|
Sets close_rate to the given rate, calculates total profit
|
|
|
|
and marks trade as closed
|
|
|
|
"""
|
2017-12-17 21:07:56 +00:00
|
|
|
self.close_rate = Decimal(rate)
|
|
|
|
self.close_profit = self.calc_profit_percent()
|
2017-12-16 00:09:07 +00:00
|
|
|
self.close_date = datetime.utcnow()
|
|
|
|
self.is_open = False
|
2017-10-31 23:22:38 +00:00
|
|
|
self.open_order_id = None
|
2017-12-16 00:09:07 +00:00
|
|
|
logger.info(
|
|
|
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
|
|
|
self
|
|
|
|
)
|
2017-09-08 19:17:58 +00:00
|
|
|
|
2017-12-19 05:58:02 +00:00
|
|
|
def calc_open_trade_price(
|
|
|
|
self,
|
|
|
|
fee: Optional[float] = None) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
|
|
|
Calculate the open_rate in BTC
|
|
|
|
:param fee: fee to use on the open rate (optional).
|
|
|
|
If rate is not set self.fee will be used
|
|
|
|
:return: Price in BTC of the open trade
|
|
|
|
"""
|
|
|
|
getcontext().prec = 8
|
|
|
|
|
|
|
|
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
2018-04-21 17:47:08 +00:00
|
|
|
fees = buy_trade * Decimal(fee or self.fee_open)
|
2017-12-17 21:07:56 +00:00
|
|
|
return float(buy_trade + fees)
|
|
|
|
|
2017-12-19 05:58:02 +00:00
|
|
|
def calc_close_trade_price(
|
|
|
|
self,
|
|
|
|
rate: Optional[float] = None,
|
|
|
|
fee: Optional[float] = None) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
|
|
|
Calculate the close_rate in BTC
|
|
|
|
:param fee: fee to use on the close rate (optional).
|
|
|
|
If rate is not set self.fee will be used
|
|
|
|
:param rate: rate to compare with (optional).
|
|
|
|
If rate is not set self.close_rate will be used
|
|
|
|
:return: Price in BTC of the open trade
|
|
|
|
"""
|
|
|
|
getcontext().prec = 8
|
|
|
|
|
|
|
|
if rate is None and not self.close_rate:
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
|
2018-04-21 17:47:08 +00:00
|
|
|
fees = sell_trade * Decimal(fee or self.fee_close)
|
2017-12-17 21:07:56 +00:00
|
|
|
return float(sell_trade - fees)
|
|
|
|
|
2017-12-19 05:58:02 +00:00
|
|
|
def calc_profit(
|
|
|
|
self,
|
|
|
|
rate: Optional[float] = None,
|
|
|
|
fee: Optional[float] = None) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
|
|
|
Calculate the profit in BTC between Close and Open trade
|
|
|
|
:param fee: fee to use on the close rate (optional).
|
|
|
|
If rate is not set self.fee will be used
|
|
|
|
:param rate: close rate to compare with (optional).
|
|
|
|
If rate is not set self.close_rate will be used
|
|
|
|
:return: profit in BTC as float
|
|
|
|
"""
|
|
|
|
open_trade_price = self.calc_open_trade_price()
|
|
|
|
close_trade_price = self.calc_close_trade_price(
|
2018-02-06 17:37:10 +00:00
|
|
|
rate=(rate or self.close_rate),
|
2018-04-21 17:47:08 +00:00
|
|
|
fee=(fee or self.fee_close)
|
2017-12-17 21:07:56 +00:00
|
|
|
)
|
|
|
|
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
|
|
|
|
2017-12-19 05:58:02 +00:00
|
|
|
def calc_profit_percent(
|
|
|
|
self,
|
|
|
|
rate: Optional[float] = None,
|
|
|
|
fee: Optional[float] = None) -> float:
|
2017-10-31 23:22:38 +00:00
|
|
|
"""
|
2017-11-01 01:20:55 +00:00
|
|
|
Calculates the profit in percentage (including fee).
|
2017-10-31 23:22:38 +00:00
|
|
|
:param rate: rate to compare with (optional).
|
|
|
|
If rate is not set self.close_rate will be used
|
2018-03-17 21:12:21 +00:00
|
|
|
:param fee: fee to use on the close rate (optional).
|
2017-10-31 23:22:38 +00:00
|
|
|
:return: profit in percentage as float
|
|
|
|
"""
|
2017-11-01 01:51:10 +00:00
|
|
|
getcontext().prec = 8
|
2017-12-17 21:07:56 +00:00
|
|
|
|
|
|
|
open_trade_price = self.calc_open_trade_price()
|
|
|
|
close_trade_price = self.calc_close_trade_price(
|
2018-02-06 17:37:10 +00:00
|
|
|
rate=(rate or self.close_rate),
|
2018-04-21 17:47:08 +00:00
|
|
|
fee=(fee or self.fee_close)
|
2017-12-17 21:07:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|