aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-01-18 01:43:19 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-01-18 02:00:44 +0100
commitcfab619d9223fc824649a6fe16863931f5e43891 (patch)
tree8f699388253896a7f5ebcb87acf344e5771c97a1
parentf2da658998b6e6605c6ae27ff338ef23b96dce25 (diff)
downloadTrader-cfab619d9223fc824649a6fe16863931f5e43891.tar.gz
Trader-cfab619d9223fc824649a6fe16863931f5e43891.tar.zst
Trader-cfab619d9223fc824649a6fe16863931f5e43891.zip
Move ticker to Trade class and add tests
-rw-r--r--portfolio.py96
-rw-r--r--test.py136
2 files changed, 175 insertions, 57 deletions
diff --git a/portfolio.py b/portfolio.py
index 1d8bfd5..576a228 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -102,7 +102,7 @@ class Amount:
102 def in_currency(self, other_currency, market, action="average"): 102 def in_currency(self, other_currency, market, action="average"):
103 if other_currency == self.currency: 103 if other_currency == self.currency:
104 return self 104 return self
105 asset_ticker = self.get_ticker(other_currency, market) 105 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
106 if asset_ticker is not None: 106 if asset_ticker is not None:
107 return Amount( 107 return Amount(
108 other_currency, 108 other_currency,
@@ -113,41 +113,6 @@ class Amount:
113 else: 113 else:
114 raise Exception("This asset is not available in the chosen market") 114 raise Exception("This asset is not available in the chosen market")
115 115
116 def get_ticker(self, c2, market, refresh=False):
117 c1 = self.currency
118
119 def invert(ticker):
120 return {
121 "inverted": True,
122 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
123 "notInverted": ticker,
124 }
125 def augment_ticker(ticker):
126 ticker.update({
127 "inverted": False,
128 "average": (ticker["bid"] + ticker["ask"] ) / 2,
129 })
130
131 if time.time() - self.ticker_cache_timestamp > 5:
132 self.ticker_cache = {}
133 self.ticker_cache_timestamp = time.time()
134 elif not refresh:
135 if (c1, c2, market.__class__) in self.ticker_cache:
136 return self.ticker_cache[(c1, c2, market.__class__)]
137 if (c2, c1, market.__class__) in self.ticker_cache:
138 return invert(self.ticker_cache[(c2, c1, market.__class__)])
139
140 try:
141 self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
142 augment_ticker(self.ticker_cache[(c1, c2, market.__class__)])
143 except ccxt.ExchangeError:
144 try:
145 self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
146 augment_ticker(self.ticker_cache[(c2, c1, market.__class__)])
147 except ccxt.ExchangeError:
148 self.ticker_cache[(c1, c2, market.__class__)] = None
149 return self.get_ticker(c2, market)
150
151 def __abs__(self): 116 def __abs__(self):
152 return Amount(self.currency, 0, int_val=abs(self._value)) 117 return Amount(self.currency, 0, int_val=abs(self._value))
153 118
@@ -212,7 +177,6 @@ class Amount:
212 177
213class Balance: 178class Balance:
214 known_balances = {} 179 known_balances = {}
215 trades = {}
216 180
217 def __init__(self, currency, total_value, free_value, used_value): 181 def __init__(self, currency, total_value, free_value, used_value):
218 self.currency = currency 182 self.currency = currency
@@ -288,8 +252,52 @@ class Trade:
288 self.base_currency = self.value_from.currency 252 self.base_currency = self.value_from.currency
289 253
290 if market is not None: 254 if market is not None:
255 self.market = market
291 self.prepare_order(market) 256 self.prepare_order(market)
292 257
258 fees_cache = {}
259 @classmethod
260 def fetch_fees(cls, market):
261 if market.__class__ not in cls.fees_cache:
262 cls.fees_cache[market.__class__] = market.fetch_fees()
263 return cls.fees_cache[market.__class__]
264
265 ticker_cache = {}
266 ticker_cache_timestamp = time.time()
267 @classmethod
268 def get_ticker(cls, c1, c2, market, refresh=False):
269 def invert(ticker):
270 return {
271 "inverted": True,
272 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
273 "original": ticker,
274 }
275 def augment_ticker(ticker):
276 ticker.update({
277 "inverted": False,
278 "average": (ticker["bid"] + ticker["ask"] ) / 2,
279 })
280
281 if time.time() - cls.ticker_cache_timestamp > 5:
282 cls.ticker_cache = {}
283 cls.ticker_cache_timestamp = time.time()
284 elif not refresh:
285 if (c1, c2, market.__class__) in cls.ticker_cache:
286 return cls.ticker_cache[(c1, c2, market.__class__)]
287 if (c2, c1, market.__class__) in cls.ticker_cache:
288 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
289
290 try:
291 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
292 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
293 except ccxt.ExchangeError:
294 try:
295 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
296 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
297 except ccxt.ExchangeError:
298 cls.ticker_cache[(c1, c2, market.__class__)] = None
299 return cls.get_ticker(c1, c2, market)
300
293 @classmethod 301 @classmethod
294 def compute_trades(cls, values_in_base, new_repartition, market=None): 302 def compute_trades(cls, values_in_base, new_repartition, market=None):
295 base_currency = sum(values_in_base.values()).currency 303 base_currency = sum(values_in_base.values()).currency
@@ -316,7 +324,7 @@ class Trade:
316 else: 324 else:
317 return "sell" 325 return "sell"
318 326
319 def ticker_action(self, inverted): 327 def order_action(self, inverted):
320 if self.value_from < self.value_to: 328 if self.value_from < self.value_to:
321 return "ask" if not inverted else "bid" 329 return "ask" if not inverted else "bid"
322 else: 330 else:
@@ -334,13 +342,13 @@ class Trade:
334 delta = abs(value_to - value_from) 342 delta = abs(value_to - value_from)
335 currency = self.base_currency 343 currency = self.base_currency
336 else: 344 else:
337 ticker = ticker["notInverted"] 345 ticker = ticker["original"]
338 delta = abs(self.value_to - self.value_from) 346 delta = abs(self.value_to - self.value_from)
339 currency = self.currency 347 currency = self.currency
340 348
341 rate = ticker[self.ticker_action(inverted)] 349 rate = ticker[self.order_action(inverted)]
342 350
343 self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency)) 351 self.orders.append(Order(self.order_action(inverted), delta, rate, currency))
344 352
345 @classmethod 353 @classmethod
346 def all_orders(cls): 354 def all_orders(cls):
@@ -408,12 +416,6 @@ class Order:
408 self.status = result["status"] 416 self.status = result["status"]
409 return self.status 417 return self.status
410 418
411@static_var("cache", {})
412def fetch_fees(market):
413 if market.__class__ not in fetch_fees.cache:
414 fetch_fees.cache[market.__class__] = market.fetch_fees()
415 return fetch_fees.cache[market.__class__]
416
417def print_orders(market, base_currency="BTC"): 419def print_orders(market, base_currency="BTC"):
418 Balance.prepare_trades(market, base_currency=base_currency) 420 Balance.prepare_trades(market, base_currency=base_currency)
419 for currency, trade in Trade.trades.items(): 421 for currency, trade in Trade.trades.items():
diff --git a/test.py b/test.py
index d10bfe4..7608061 100644
--- a/test.py
+++ b/test.py
@@ -17,13 +17,12 @@ class AmountTest(unittest.TestCase):
17 self.assertEqual(amount, amount.in_currency("ETC", None)) 17 self.assertEqual(amount, amount.in_currency("ETC", None))
18 18
19 ticker_mock = unittest.mock.Mock() 19 ticker_mock = unittest.mock.Mock()
20 with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): 20 with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock):
21 ticker_mock.return_value = None 21 ticker_mock.return_value = None
22 portfolio.Amount.get_ticker = ticker_mock
23 22
24 self.assertRaises(Exception, amount.in_currency, "ETH", None) 23 self.assertRaises(Exception, amount.in_currency, "ETH", None)
25 24
26 with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): 25 with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock):
27 ticker_mock.return_value = { 26 ticker_mock.return_value = {
28 "average": 0.3, 27 "average": 0.3,
29 "foo": "bar", 28 "foo": "bar",
@@ -35,10 +34,6 @@ class AmountTest(unittest.TestCase):
35 self.assertEqual(amount, converted_amount.linked_to) 34 self.assertEqual(amount, converted_amount.linked_to)
36 self.assertEqual("bar", converted_amount.ticker["foo"]) 35 self.assertEqual("bar", converted_amount.ticker["foo"])
37 36
38 @unittest.skip("TODO")
39 def test_get_ticker(self):
40 pass
41
42 def test__abs(self): 37 def test__abs(self):
43 amount = portfolio.Amount("SC", -120) 38 amount = portfolio.Amount("SC", -120)
44 self.assertEqual(120, abs(amount).value) 39 self.assertEqual(120, abs(amount).value)
@@ -276,7 +271,7 @@ class BalanceTest(unittest.TestCase):
276 "total": 0.0 271 "total": 0.0
277 }, 272 },
278 } 273 }
279 self.patcher = mock.patch.multiple(portfolio.Balance, known_balances={}, trades={}) 274 self.patcher = mock.patch.multiple(portfolio.Balance, known_balances={})
280 self.patcher.start() 275 self.patcher.start()
281 276
282 def test_values(self): 277 def test_values(self):
@@ -292,7 +287,7 @@ class BalanceTest(unittest.TestCase):
292 self.assertEqual(0.30, balance.used.value) 287 self.assertEqual(0.30, balance.used.value)
293 self.assertEqual("BTC", balance.currency) 288 self.assertEqual("BTC", balance.currency)
294 289
295 @mock.patch.object(portfolio.Amount, "get_ticker") 290 @mock.patch.object(portfolio.Trade, "get_ticker")
296 def test_in_currency(self, get_ticker): 291 def test_in_currency(self, get_ticker):
297 portfolio.Balance.known_balances = { 292 portfolio.Balance.known_balances = {
298 "BTC": portfolio.Balance("BTC", 0.65, 0.35, 0.30), 293 "BTC": portfolio.Balance("BTC", 0.65, 0.35, 0.30),
@@ -352,7 +347,7 @@ class BalanceTest(unittest.TestCase):
352 self.assertEqual(7.5, amounts["XEM"].value) 347 self.assertEqual(7.5, amounts["XEM"].value)
353 348
354 @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") 349 @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand")
355 @mock.patch.object(portfolio.Amount, "get_ticker") 350 @mock.patch.object(portfolio.Trade, "get_ticker")
356 @mock.patch.object(portfolio.Trade, "compute_trades") 351 @mock.patch.object(portfolio.Trade, "compute_trades")
357 def test_prepare_trades(self, compute_trades, get_ticker, repartition): 352 def test_prepare_trades(self, compute_trades, get_ticker, repartition):
358 repartition.return_value = { 353 repartition.return_value = {
@@ -393,5 +388,126 @@ class BalanceTest(unittest.TestCase):
393 def tearDown(self): 388 def tearDown(self):
394 self.patcher.stop() 389 self.patcher.stop()
395 390
391class TradeTest(unittest.TestCase):
392 import time
393
394 def setUp(self):
395 super(TradeTest, self).setUp()
396
397 self.patcher = mock.patch.multiple(portfolio.Trade,
398 ticker_cache={},
399 ticker_cache_timestamp=self.time.time(),
400 fees_cache={},
401 trades={})
402 self.patcher.start()
403
404 def test_get_ticker(self):
405 market = mock.Mock()
406 market.fetch_ticker.side_effect = [
407 { "bid": 1, "ask": 3 },
408 portfolio.ccxt.ExchangeError("foo"),
409 { "bid": 10, "ask": 40 },
410 portfolio.ccxt.ExchangeError("foo"),
411 portfolio.ccxt.ExchangeError("foo"),
412 ]
413
414 ticker = portfolio.Trade.get_ticker("ETH", "ETC", market)
415 market.fetch_ticker.assert_called_with("ETH/ETC")
416 self.assertEqual(1, ticker["bid"])
417 self.assertEqual(3, ticker["ask"])
418 self.assertEqual(2, ticker["average"])
419 self.assertFalse(ticker["inverted"])
420
421 ticker = portfolio.Trade.get_ticker("ETH", "XVG", market)
422 self.assertEqual(0.0625, ticker["average"])
423 self.assertTrue(ticker["inverted"])
424 self.assertIn("original", ticker)
425 self.assertEqual(10, ticker["original"]["bid"])
426
427 ticker = portfolio.Trade.get_ticker("XVG", "XMR", market)
428 self.assertIsNone(ticker)
429
430 market.fetch_ticker.assert_has_calls([
431 mock.call("ETH/ETC"),
432 mock.call("ETH/XVG"),
433 mock.call("XVG/ETH"),
434 mock.call("XVG/XMR"),
435 mock.call("XMR/XVG"),
436 ])
437
438 market2 = mock.Mock()
439 market2.fetch_ticker.side_effect = [
440 { "bid": 1, "ask": 3 },
441 { "bid": 1.2, "ask": 3.5 },
442 ]
443 ticker1 = portfolio.Trade.get_ticker("ETH", "ETC", market2)
444 ticker2 = portfolio.Trade.get_ticker("ETH", "ETC", market2)
445 ticker3 = portfolio.Trade.get_ticker("ETC", "ETH", market2)
446 market2.fetch_ticker.assert_called_once_with("ETH/ETC")
447 self.assertEqual(1, ticker1["bid"])
448 self.assertDictEqual(ticker1, ticker2)
449 self.assertDictEqual(ticker1, ticker3["original"])
450
451 ticker4 = portfolio.Trade.get_ticker("ETH", "ETC", market2, refresh=True)
452 ticker5 = portfolio.Trade.get_ticker("ETH", "ETC", market2)
453 self.assertEqual(1.2, ticker4["bid"])
454 self.assertDictEqual(ticker4, ticker5)
455
456 market3 = mock.Mock()
457 market3.fetch_ticker.side_effect = [
458 { "bid": 1, "ask": 3 },
459 { "bid": 1.2, "ask": 3.5 },
460 ]
461 ticker6 = portfolio.Trade.get_ticker("ETH", "ETC", market3)
462 portfolio.Trade.ticker_cache_timestamp -= 4
463 ticker7 = portfolio.Trade.get_ticker("ETH", "ETC", market3)
464 portfolio.Trade.ticker_cache_timestamp -= 2
465 ticker8 = portfolio.Trade.get_ticker("ETH", "ETC", market3)
466 self.assertDictEqual(ticker6, ticker7)
467 self.assertEqual(1.2, ticker8["bid"])
468
469 @unittest.skip("TODO")
470 def test_values_assertion(self):
471 pass
472
473 @unittest.skip("TODO")
474 def test_fetch_fees(self):
475 pass
476
477 @unittest.skip("TODO")
478 def test_compute_trades(self):
479 pass
480
481 @unittest.skip("TODO")
482 def test_action(self):
483 pass
484
485 @unittest.skip("TODO")
486 def test_action(self):
487 pass
488
489 @unittest.skip("TODO")
490 def test_order_action(self):
491 pass
492
493 @unittest.skip("TODO")
494 def test_prepare_order(self):
495 pass
496
497 @unittest.skip("TODO")
498 def test_all_orders(self):
499 pass
500
501 @unittest.skip("TODO")
502 def test_follow_orders(self):
503 pass
504
505 @unittest.skip("TODO")
506 def test__repr(self):
507 pass
508
509 def tearDown(self):
510 self.patcher.stop()
511
396if __name__ == '__main__': 512if __name__ == '__main__':
397 unittest.main() 513 unittest.main()