diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-18 01:43:19 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-18 02:00:44 +0100 |
commit | cfab619d9223fc824649a6fe16863931f5e43891 (patch) | |
tree | 8f699388253896a7f5ebcb87acf344e5771c97a1 | |
parent | f2da658998b6e6605c6ae27ff338ef23b96dce25 (diff) | |
download | Trader-cfab619d9223fc824649a6fe16863931f5e43891.tar.gz Trader-cfab619d9223fc824649a6fe16863931f5e43891.tar.zst Trader-cfab619d9223fc824649a6fe16863931f5e43891.zip |
Move ticker to Trade class and add tests
-rw-r--r-- | portfolio.py | 96 | ||||
-rw-r--r-- | test.py | 136 |
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 | ||
213 | class Balance: | 178 | class 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", {}) | ||
412 | def 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 | |||
417 | def print_orders(market, base_currency="BTC"): | 419 | def 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(): |
@@ -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 | ||
391 | class 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 | |||
396 | if __name__ == '__main__': | 512 | if __name__ == '__main__': |
397 | unittest.main() | 513 | unittest.main() |