# Put your poloniex api key in market.py
from market import market
-# FIXME: Améliorer le bid/ask
-# FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition
-# FIXME: better compute moves to avoid rounding errors
+debug = False
class Portfolio:
URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
}
class Amount:
- MAX_DIGITS = 18
-
def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
self.currency = currency
self.value = D(value)
for key in hash_:
if key in ["info", "free", "used", "total"]:
continue
- if hash_[key]["total"] > 0:
+ if hash_[key]["total"] > 0 or key in cls.known_balances:
cls.known_balances[key] = cls.from_hash(key, hash_[key])
@classmethod
return amounts
@classmethod
- def prepare_trades(cls, market, base_currency="BTC", compute_value=None):
+ def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
cls.fetch_balances(market)
- values_in_base = cls.in_currency(base_currency, market)
+ values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
total_base_value = sum(values_in_base.values())
new_repartition = cls.dispatch_assets(total_base_value)
# Recompute it in case we have new currencies
- values_in_base = cls.in_currency(base_currency, market)
- Trade.compute_trades(values_in_base, new_repartition, market=market, compute_value=compute_value)
+ values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
+ Trade.compute_trades(values_in_base, new_repartition, market=market)
+
+ @classmethod
+ def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
+ cls.fetch_balances(market)
+ values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
+ total_base_value = sum(values_in_base.values())
+ new_repartition = cls.dispatch_assets(total_base_value)
+ Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)
def __repr__(self):
return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
return cls.get_ticker(c1, c2, market)
@classmethod
- def compute_trades(cls, values_in_base, new_repartition, market=None, compute_value=None):
+ def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
base_currency = sum(values_in_base.values()).currency
for currency in Balance.currencies():
if currency == base_currency:
continue
- cls.trades[currency] = cls(
+ trade = cls(
values_in_base.get(currency, Amount(base_currency, 0)),
new_repartition.get(currency, Amount(base_currency, 0)),
currency,
market=market
)
- if compute_value is not None:
- cls.trades[currency].prepare_order(compute_value=compute_value)
+ if only is None or trade.action == only:
+ cls.trades[currency] = trade
return cls.trades
+ @classmethod
+ def prepare_orders(cls, only=None, compute_value="default"):
+ for currency, trade in cls.trades.items():
+ if only is None or trade.action == only:
+ trade.prepare_order(compute_value=compute_value)
+
@property
def action(self):
if self.value_from == self.value_to:
rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
- self.orders.append(Order(self.order_action(inverted), delta, rate, currency))
+ self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market))
@classmethod
def compute_value(cls, ticker, action, compute_value="default"):
return compute_value(ticker, action)
@classmethod
- def all_orders(cls):
- return sum(map(lambda v: v.orders, cls.trades.values()), [])
+ def all_orders(cls, state=None):
+ all_orders = sum(map(lambda v: v.orders, cls.trades.values()), [])
+ if state is None:
+ return all_orders
+ else:
+ return list(filter(lambda o: o.status == state, all_orders))
+
+ @classmethod
+ def run_orders(cls):
+ for order in cls.all_orders(state="pending"):
+ order.run()
@classmethod
- def follow_orders(cls, market):
+ def follow_orders(cls, verbose=True, sleep=30):
orders = cls.all_orders()
finished_orders = []
while len(orders) != len(finished_orders):
- time.sleep(30)
+ time.sleep(sleep)
for order in orders:
if order in finished_orders:
continue
- if order.get_status(market) != "open":
+ if order.get_status() != "open":
finished_orders.append(order)
- print("finished {}".format(order))
- print("All orders finished")
+ if verbose:
+ print("finished {}".format(order))
+ if verbose:
+ print("All orders finished")
def __repr__(self):
return "Trade({} -> {} in {}, {})".format(
self.action)
class Order:
- DEBUG = True
+ DEBUG = debug
- def __init__(self, action, amount, rate, base_currency):
+ def __init__(self, action, amount, rate, base_currency, market):
self.action = action
self.amount = amount
self.rate = rate
self.base_currency = base_currency
+ self.market = market
self.result = None
- self.status = "not run"
+ self.status = "pending"
def __repr__(self):
return "Order({} {} at {} {} [{}])".format(
self.status
)
- def run(self, market):
+ @property
+ def pending(self):
+ return self.status == "pending"
+
+ @property
+ def finished(self):
+ return self.status == "closed" or self.status == "canceled"
+
+ def run(self):
symbol = "{}/{}".format(self.amount.currency, self.base_currency)
amount = self.amount.value
symbol, self.action, amount, self.rate))
else:
try:
- self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
+ self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
self.status = "open"
except Exception:
pass
- def get_status(self, market):
+ def get_status(self):
# other states are "closed" and "canceled"
if self.status == "open":
- result = market.fetch_order(self.result['id'])
+ result = self.market.fetch_order(self.result['id'])
self.status = result["status"]
return self.status
def print_orders(market, base_currency="BTC"):
Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
+ Trade.prepare_orders(compute_value="average")
for currency, balance in Balance.known_balances.items():
print(balance)
for currency, trade in Trade.trades.items():
print(trade)
for order in trade.orders:
print("\t", order, sep="")
- order.run(market)
+ order.run()
if __name__ == '__main__':
print_orders(market)
"info": "bar",
"used": "baz",
"total": "bazz",
+ "ETC": {
+ "free": 0.0,
+ "used": 0.0,
+ "total": 0.0
+ },
"USDT": {
"free": 6.0,
"used": 1.2,
portfolio.Balance.fetch_balances(portfolio.market)
self.assertNotIn("XMR", portfolio.Balance.currencies())
- self.assertEqual(["USDT", "XVG"], list(portfolio.Balance.currencies()))
+ self.assertListEqual(["USDT", "XVG"], list(portfolio.Balance.currencies()))
+
+ portfolio.Balance.known_balances["ETC"] = portfolio.Balance("ETC", "1", "0", "1")
+ portfolio.Balance.fetch_balances(portfolio.market)
+ self.assertEqual(0, portfolio.Balance.known_balances["ETC"].total)
+ self.assertListEqual(["USDT", "XVG", "ETC"], list(portfolio.Balance.currencies()))
@mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand")
@mock.patch.object(portfolio.market, "fetch_balance")
return { "average": D("0.000001") }
if c1 == "XEM" and c2 == "BTC":
return { "average": D("0.001") }
- raise Exception("Should be called with {}, {}".format(c1, c2))
+ self.fail("Should be called with {}, {}".format(c1, c2))
get_ticker.side_effect = _get_ticker
market = mock.Mock()
self.assertEqual(D("0.2525"), call[0][1]["BTC"].value)
self.assertEqual(D("0.7575"), call[0][1]["XEM"].value)
+ @unittest.skip("TODO")
+ def test_update_trades(self):
+ pass
+
def test__repr(self):
balance = portfolio.Balance("BTX", 3, 1, 2)
self.assertEqual("Balance(BTX [1.00000000 BTX/2.00000000 BTX/3.00000000 BTX])", repr(balance))
def tearDown(self):
self.patcher.stop()
+class AcceptanceTest(unittest.TestCase):
+ import time
+
+ def setUp(self):
+ super(AcceptanceTest, self).setUp()
+
+ self.patchers = [
+ mock.patch.multiple(portfolio.Balance, known_balances={}),
+ mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={}),
+ mock.patch.multiple(portfolio.Trade,
+ ticker_cache={},
+ ticker_cache_timestamp=self.time.time(),
+ fees_cache={},
+ trades={}),
+ mock.patch.multiple(portfolio.Computation,
+ computations=portfolio.Computation.computations)
+ ]
+ for patcher in self.patchers:
+ patcher.start()
+
+ def test_success_sell_only_necessary(self):
+ fetch_balance = {
+ "ETH": {
+ "free": D("1.0"),
+ "used": D("0.0"),
+ "total": D("1.0"),
+ },
+ "ETC": {
+ "free": D("4.0"),
+ "used": D("0.0"),
+ "total": D("4.0"),
+ },
+ "XVG": {
+ "free": D("1000.0"),
+ "used": D("0.0"),
+ "total": D("1000.0"),
+ },
+ }
+ repartition = {
+ "ETH": 2500,
+ "ETC": 2500,
+ "BTC": 4000,
+ "BTD": 500,
+ "USDT": 500,
+ }
+
+ def fetch_ticker(symbol):
+ if symbol == "ETH/BTC":
+ return {
+ "symbol": "ETH/BTC",
+ "bid": D("0.14"),
+ "ask": D("0.16")
+ }
+ if symbol == "ETC/BTC":
+ return {
+ "symbol": "ETC/BTC",
+ "bid": D("0.002"),
+ "ask": D("0.003")
+ }
+ if symbol == "XVG/BTC":
+ return {
+ "symbol": "XVG/BTC",
+ "bid": D("0.00003"),
+ "ask": D("0.00005")
+ }
+ if symbol == "BTD/BTC":
+ return {
+ "symbol": "BTD/BTC",
+ "bid": D("0.0008"),
+ "ask": D("0.0012")
+ }
+ if symbol == "USDT/BTC":
+ raise portfolio.ccxt.ExchangeError
+ if symbol == "BTC/USDT":
+ return {
+ "symbol": "BTC/USDT",
+ "bid": D("14000"),
+ "ask": D("16000")
+ }
+ self.fail("Shouldn't have been called with {}".format(symbol))
+
+ market = mock.Mock()
+ market.fetch_balance.return_value = fetch_balance
+ market.fetch_ticker.side_effect = fetch_ticker
+ with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition):
+ # Action 1
+ portfolio.Balance.prepare_trades(market)
+
+ balances = portfolio.Balance.known_balances
+ self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total)
+ self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
+ self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total)
+
+
+ trades = portfolio.Trade.trades
+ self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to)
+ self.assertEqual("sell", trades["ETH"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["ETC"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETC"].value_to)
+ self.assertEqual("buy", trades["ETC"].action)
+
+ self.assertNotIn("BTC", trades)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["BTD"].value_to)
+ self.assertEqual("buy", trades["BTD"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["USDT"].value_to)
+ self.assertEqual("buy", trades["USDT"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades["XVG"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["XVG"].value_to)
+ self.assertEqual("sell", trades["XVG"].action)
+
+ # Action 2
+ portfolio.Trade.prepare_orders(only="sell", compute_value=lambda x, y: x["bid"] * D("1.001"))
+
+ all_orders = portfolio.Trade.all_orders()
+ self.assertEqual(2, len(all_orders))
+ self.assertEqual(2, 3*all_orders[0].amount.value)
+ self.assertEqual(D("0.14014"), all_orders[0].rate)
+ self.assertEqual(1000, all_orders[1].amount.value)
+ self.assertEqual(D("0.00003003"), all_orders[1].rate)
+
+
+ def create_order(symbol, type, action, amount, price=None):
+ self.assertEqual("limit", type)
+ if symbol == "ETH/BTC":
+ self.assertEqual("bid", action)
+ self.assertEqual(2, 3*amount)
+ self.assertEqual(D("0.14014"), price)
+ elif symbol == "XVG/BTC":
+ self.assertEqual("bid", action)
+ self.assertEqual(1000, amount)
+ self.assertEqual(D("0.00003003"), price)
+ else:
+ self.fail("I shouldn't have been called")
+
+ return {
+ "id": symbol,
+ }
+ market.create_order.side_effect = create_order
+
+ # Action 3
+ portfolio.Trade.run_orders()
+
+ self.assertEqual("open", all_orders[0].status)
+ self.assertEqual("open", all_orders[1].status)
+
+ market.fetch_order.return_value = { "status": "closed" }
+ with mock.patch.object(portfolio.time, "sleep") as sleep:
+ # Action 4
+ portfolio.Trade.follow_orders(verbose=False)
+
+ sleep.assert_called_with(30)
+
+ for order in all_orders:
+ self.assertEqual("closed", order.status)
+
+ fetch_balance = {
+ "ETH": {
+ "free": D("1.0") / 3,
+ "used": D("0.0"),
+ "total": D("1.0") / 3,
+ },
+ "BTC": {
+ "free": D("0.134"),
+ "used": D("0.0"),
+ "total": D("0.134"),
+ },
+ "ETC": {
+ "free": D("4.0"),
+ "used": D("0.0"),
+ "total": D("4.0"),
+ },
+ "XVG": {
+ "free": D("0.0"),
+ "used": D("0.0"),
+ "total": D("0.0"),
+ },
+ }
+ market.fetch_balance.return_value = fetch_balance
+
+ with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition):
+ # Action 5
+ portfolio.Balance.update_trades(market, only="buy", compute_value="average")
+
+ balances = portfolio.Balance.known_balances
+ self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total)
+ self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
+ self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total)
+ self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total)
+
+
+ trades = portfolio.Trade.trades
+ self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to)
+ self.assertEqual("sell", trades["ETH"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["ETC"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.0485")), trades["ETC"].value_to)
+ self.assertEqual("buy", trades["ETC"].action)
+
+ self.assertNotIn("BTC", trades)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["BTD"].value_to)
+ self.assertEqual("buy", trades["BTD"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["USDT"].value_to)
+ self.assertEqual("buy", trades["USDT"].action)
+
+ self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades["XVG"].value_from)
+ self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["XVG"].value_to)
+ self.assertEqual("sell", trades["XVG"].action)
+
+ # Action 6
+ portfolio.Trade.prepare_orders(only="buy", compute_value=lambda x, y: x["ask"] * D("0.999"))
+
+ all_orders = portfolio.Trade.all_orders(state="pending")
+ self.assertEqual(3, len(all_orders))
+ self.assertEqual(portfolio.Amount("ETC", D("15.4")), all_orders[0].amount)
+ self.assertEqual(D("0.002997"), all_orders[0].rate)
+ self.assertEqual("ask", all_orders[0].action)
+ self.assertEqual(portfolio.Amount("BTD", D("9.7")), all_orders[1].amount)
+ self.assertEqual(D("0.0011988"), all_orders[1].rate)
+ self.assertEqual("ask", all_orders[1].action)
+ self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[2].amount)
+ self.assertEqual(D("15984"), all_orders[2].rate)
+ self.assertEqual("bid", all_orders[2].action)
+
+ with mock.patch.object(portfolio.time, "sleep") as sleep:
+ # Action 7
+ portfolio.Trade.follow_orders(verbose=False)
+
+ sleep.assert_called_with(30)
+
+ def tearDown(self):
+ for patcher in self.patchers:
+ patcher.stop()
+
if __name__ == '__main__':
unittest.main()