# -*- coding: utf-8 -*-
import select
import sys
import datetime
import time
import logging
import sqlalchemy as sa
import click
from cointrader import Base, engine, db
from cointrader.indicators import (
WAIT, BUY, SELL, Signal, signal_map
)
from cointrader.exchanges.poloniex import ApiError
from cointrader.helpers import (
render_bot_statistic, render_bot_tradelog,
render_bot_title, render_signal_detail,
render_user_options
)
TIMEOUT = 0
# Number of seconds the bot will wait for user input in automatic mode
# to reattach the bot
log = logging.getLogger(__name__)
[docs]def replay_tradelog(trades):
btc = 0
amount = 0
for t in trades:
if t.order_type == "INIT":
btc = t.btc
amount = t.amount
elif t.order_type == "BUY":
btc -= t.btc
amount += t.amount
else:
btc += t.btc
amount -= t.amount
return btc, amount
[docs]def init_db():
Base.metadata.create_all(engine)
[docs]def load_bot(market, strategy, resolution, start, end):
"""Will load an existing bot from the database. While loading the
bot will replay its trades from the trade log to set the available btc
and coins for further trading.
Beside the btc and amount of coins all other aspects of the coin
like the time frame and strategy are defined by the user. They are
not loaded from the database."""
try:
bot = db.query(Cointrader).filter(Cointrader.market == market._name).one()
log.info("Loading bot {} {}".format(bot.market, bot.id))
bot._market = market
bot._strategy = strategy
bot._resolution = resolution
bot._start = start
bot._end = end
bot.strategy = str(strategy)
btc, amount = replay_tradelog(bot.trades)
log.info("Loaded state from trade log: {} BTC {} COINS".format(btc, amount))
bot.btc = btc
bot.amount = amount
db.commit()
return bot
except sa.orm.exc.NoResultFound:
return None
[docs]def create_bot(market, strategy, resolution, start, end, btc, amount):
"""Will create a new bot instance."""
bot = Cointrader(market, strategy, resolution, start, end)
log.info("Creating new bot {}".format(bot.market))
# Setup the bot with coins and BTC.
if btc is None:
balances = market._exchange.get_balance()
btc = balances["BTC"]["quantity"]
if amount is None:
balances = market._exchange.get_balance()
amount = balances[market.currency]["quantity"]
bot.btc = btc
bot.amount = amount
chart = market.get_chart(resolution, start, end)
rate = chart.get_first_point()["close"]
date = datetime.datetime.utcfromtimestamp(chart.get_first_point()["date"])
trade = Trade(date, "INIT", 0, 0, market._name, rate, amount, 0, btc, 0)
bot.trades.append(trade)
db.add(bot)
db.commit()
return bot
[docs]def get_bot(market, strategy, resolution, start, end, btc, amount):
"""Will load or create a bot instance.
The bot will operate with the given `resolution` on the `market` using
the specified `strategy`.
The `start` and `end`
The bot is equipped with a specified `amount` of coins and `btc` for
trading. If no btc or amount is specified (None), the bot will be
initialised with *all* available coins on the given market.
:market: :class:`Market` instance
:strategy: :class:`Strategy` instance
:resolution: Resolution in seconds the bot will operate on the market.
:start: Datetime where the bot will start to operate
:end: Datetime where the bot will end to operate
:btc: Amount of BTC the Bot will be initialised with
:amount: Amount of Coins (eg. Dash, Ripple) the Bot will be initialised with
:returns:
"""
bot = load_bot(market, strategy, resolution, start, end)
if bot is None:
bot = create_bot(market, strategy, resolution, start, end, btc, amount)
return bot
[docs]class Trade(Base):
"""All trades of cointrader are saved in the database. A trade can either be a BUY or SELL."""
__tablename__ = "trades"
id = sa.Column(sa.Integer, primary_key=True)
bot_id = sa.Column(sa.Integer, sa.ForeignKey('bots.id'))
date = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow)
order_type = sa.Column(sa.String, nullable=False)
order_id = sa.Column(sa.Integer, nullable=False)
trade_id = sa.Column(sa.Integer, nullable=False)
market = sa.Column(sa.String, nullable=False)
rate = sa.Column(sa.Float, nullable=False)
amount = sa.Column(sa.Float, nullable=False)
amount_taxed = sa.Column(sa.Float, nullable=False)
btc = sa.Column(sa.Float, nullable=False)
btc_taxed = sa.Column(sa.Float, nullable=False)
def __init__(self, date, order_type, order_id, trade_id, market, rate, amount, amount_taxed, btc, btc_taxed):
"""Initialize a new trade log entry.
:bot_id: ID of the bot which initiated the trade
:date: Date of the order
:order_id: ID of the order
:order_type: Type of order. Can be either "BUY, SELL"
:trade_id: ID of a single trade within the order
:market: Currency_pair linke BTC_DASH
:rate: Rate for the order
:amount: How many coins sold
:amount_taxed: How many coins bought (including fee) order
:btc: How many payed on buy
:btc_taxed: How many BTC get (including fee) from sell
"""
if not isinstance(date, datetime.datetime):
self.date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
else:
self.date = date
self.order_type = order_type
self.order_id = order_id
self.trade_id = trade_id
self.market = market
self.rate = rate
self.amount = amount
self.amount_taxed = amount_taxed
self.btc = btc
self.btc_taxed = btc_taxed
if self.order_type == "BUY":
log.info("{}: BUY {} @ {} paid -> {} BTC".format(self.date, self.amount_taxed, self.rate, self.btc))
elif self.order_type == "SELL":
log.info("{}: SELL {} @ {} earned -> {} BTC".format(self.date, self.amount, self.rate, self.btc_taxed))
else:
log.info("{}: INIT {} BTC {} COINS".format(self.date, self.btc, self.amount))
[docs]class Cointrader(Base):
"""Cointrader"""
__tablename__ = "bots"
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow)
active = sa.Column(sa.Boolean, nullable=False, default=True)
market = sa.Column(sa.String, nullable=False)
strategy = sa.Column(sa.String, nullable=False)
automatic = sa.Column(sa.Boolean, nullable=False)
trades = sa.orm.relationship("Trade")
def __init__(self, market, strategy, resolution="30m", start=None, end=None, automatic=False):
self.market = market._name
self.strategy = str(strategy)
self.automatic = automatic
self._market = market
self._strategy = strategy
self._resolution = resolution
self._start = start
self._end = end
self.amount = 0
self.btc = 0
# The bot has either btc to buy or amount of coins to sell.
[docs] def get_last_sell(self):
for t in self.trades[::-1]:
if t.order_type == "SELL":
return t
[docs] def get_last_buy(self):
for t in self.trades[::-1]:
if t.order_type == "BUY":
return t
def _buy(self):
result = self._market.buy(self.btc)
# {u'orderNumber': u'101983568396',
# u'resultingTrades': [{u'tradeID': u'10337029',
# u'rate': u'0.01459299',
# u'amount': u'0.01263972',
# u'date': u'2017-08-28 19:51:50',
# u'total': u'0.00018445', u'type': u'buy'}]}
order_id = result["orderNumber"]
order_type = "BUY"
total_amount = 0
for t in result["resultingTrades"]:
trade_id = t["tradeID"]
date = t["date"]
amount = t["amount"]
total_amount += float(amount)
rate = t["rate"]
btc = t["total"]
trade = Trade(date, order_type, order_id, trade_id, self._market._name, rate, 0, amount, self.btc, btc)
self.trades.append(trade)
# Finally set the internal state of the bot. BTC will be 0 after
# buying but we now have some amount of coins.
self.amount = total_amount
self.btc = 0
self.state = 1
db.commit()
def _sell(self):
result = self._market.sell(self.amount)
# {u'orderNumber': u'101984509454',
# u'resultingTrades': [{u'tradeID': u'10337105',
# u'rate': u'0.01458758',
# u'amount': u'0.01263972',
# u'date': u'2017-08-28 19:57:51',
# u'total': u'0.00018438',
# u'type': u'sell'}]}
order_id = result["orderNumber"]
order_type = "SELL"
total_btc = 0
for t in result["resultingTrades"]:
trade_id = t["tradeID"]
date = t["date"]
amount = t["amount"]
rate = t["rate"]
btc = t["total"]
total_btc += float(btc)
trade = Trade(date, order_type, order_id, trade_id, self._market._name, rate, self.amount, amount, 0, btc)
self.trades.append(trade)
# Finally set the internal state of the bot. Amount will be 0 after
# selling but we now have some BTC.
self.state = 0
self.amount = 0
self.btc = total_btc
db.commit()
[docs] def stat(self, delete_trades=False):
"""Returns a dictionary with some statistic of the performance
of the bot. Performance means how good cointrader performs in
comparison to the market movement. Market movement is measured
by looking at the start- and end rate of the chart.
The performance of cointrader is measured by looking at the
start and end value of the trade. These values are also
multiplied with the start and end rate. So if cointrader does
some good decisions and increases eater btc or amount of coins
of the bot the performance should be better."""
chart = self._market.get_chart(self._resolution, self._start, self._end)
first = chart.get_first_point()
market_start_rate = first["close"]
start_date = datetime.datetime.utcfromtimestamp(first["date"])
last = chart.get_last_point()
market_end_rate = last["close"]
end_date = datetime.datetime.utcfromtimestamp(last["date"])
# Set start value
for trade in self.trades:
if trade.order_type == "INIT":
trader_start_btc = trade.btc
trader_start_amount = trade.amount
market_start_btc = trade.btc
market_start_amount = trade.amount
trader_end_btc = trader_start_btc
trader_end_amount = trader_start_amount
for trade in self.trades:
if trade.order_type == "BUY":
trader_end_amount += trade.amount_taxed
trader_end_btc -= trade.btc
elif trade.order_type == "SELL":
trader_end_btc += trade.btc_taxed
trader_end_amount -= trade.amount
trader_start_value = trader_start_btc + trader_start_amount * market_start_rate
market_start_value = trader_start_value
trader_end_value = trader_end_btc + trader_end_amount * market_end_rate
market_end_value = market_start_btc + market_start_amount * market_end_rate
trader_profit = trader_end_value - trader_start_value
market_profit = market_end_value - market_start_value
stat = {
"start": start_date,
"end": end_date,
"market_start_value": market_start_value,
"market_end_value": market_end_value,
"profit_chart": market_profit / market_end_value * 100,
"trader_start_value": trader_start_value,
"trader_end_value": trader_end_value,
"profit_cointrader": trader_profit / trader_end_value * 100,
}
if delete_trades:
for trade in self.trades:
db.delete(trade)
db.commit()
return stat
def _in_time(self, date):
return (self._start is None or self._start <= date) and (self._end is None or date <= self._end)
def _get_interval(self, automatic, backtest):
# Set number of seconds to wait until the bot again call for a
# trading signal. This defaults to the resolution of the bot
# which is provided on initialisation.
if automatic and not backtest:
interval = self._market._exchange.resolution2seconds(self._resolution)
else:
interval = 0
return interval
def _handle_signal(self, signal):
if signal.value == BUY and self.btc and self._in_time(signal.date):
self._buy()
elif signal.value == SELL and self.amount and self._in_time(signal.date):
self._sell()
[docs] def start(self, backtest=False, automatic=False):
"""Start the bot and begin trading with given amount of BTC.
The bot will trigger a analysis of the chart every N seconds.
The default number of seconds is set on initialisation using the
`resolution` option. You can overwrite this setting
by using the `interval` option.
By setting the `backtest` option the trade will be simulated on
real chart data. This is useful for testing to see how good
your strategy performs.
:btc: Amount of BTC to start trading with
:backtest: Simulate trading on historic chart data on the given market.
:returns: None
"""
interval = self._get_interval(automatic, backtest)
while 1:
chart = self._market.get_chart(self._resolution, self._start, self._end)
signal = self._strategy.signal(chart)
log.debug("{} {}".format(signal.date, signal_map[signal.value]))
if not automatic:
click.echo(render_bot_title(self, self._market, chart))
click.echo(render_signal_detail(signal))
options = []
if self.btc:
options.append(('b', 'Buy'))
if self.amount:
options.append(('s', 'Sell'))
options.append(('l', 'Tradelog'))
options.append(('p', 'Performance of bot'))
if not automatic:
options.append(('d', 'Detach'))
options.append(('q', 'Quit'))
click.echo(render_user_options(options))
c = click.getchar()
if c == 'b' and self.btc:
# btc = click.prompt('BTC', default=self.self.btc)
if click.confirm('Buy for {} btc?'.format(self.btc)):
signal = Signal(BUY, datetime.datetime.utcnow())
elif c == 's' and self.amount:
# amount = click.prompt('Amount', default=self.self.amount)
if click.confirm('Sell {}?'.format(self.amount)):
signal = Signal(SELL, datetime.datetime.utcnow())
elif c == 'l':
click.echo(render_bot_tradelog(self.trades))
elif c == 'p':
click.echo(render_bot_statistic(self.stat()))
elif c == 'd':
automatic = True
log.info("Bot detached")
elif c == 'q':
log.info("Bot stopped")
sys.exit(0)
else:
signal = Signal(WAIT, datetime.datetime.utcnow())
if automatic:
click.echo("Press 'A' with in the next {} secods to reattach the bot.".format(TIMEOUT))
i, o, e = select.select([sys.stdin], [], [], TIMEOUT)
if (i):
value = sys.stdin.readline().strip()
print(value)
if value == "A":
automatic = False
if signal:
try:
self._handle_signal(signal)
except ApiError as ex:
log.error("Can not place order: {}".format(ex.message))
if backtest:
if not self._market.continue_backtest():
log.info("Backtest finished")
break
time.sleep(interval)