diff options
-rw-r--r-- | portfolio.py | 51 | ||||
-rw-r--r-- | test.py | 30 |
2 files changed, 63 insertions, 18 deletions
diff --git a/portfolio.py b/portfolio.py index 4991dce..5211dd8 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -89,14 +89,14 @@ class Amount: | |||
89 | self.ticker_cache = {} | 89 | self.ticker_cache = {} |
90 | self.ticker_cache_timestamp = time.time() | 90 | self.ticker_cache_timestamp = time.time() |
91 | 91 | ||
92 | def in_currency(self, other_currency, market, action="average"): | 92 | def in_currency(self, other_currency, market, action=None, compute_value="average"): |
93 | if other_currency == self.currency: | 93 | if other_currency == self.currency: |
94 | return self | 94 | return self |
95 | asset_ticker = Trade.get_ticker(self.currency, other_currency, market) | 95 | asset_ticker = Trade.get_ticker(self.currency, other_currency, market) |
96 | if asset_ticker is not None: | 96 | if asset_ticker is not None: |
97 | return Amount( | 97 | return Amount( |
98 | other_currency, | 98 | other_currency, |
99 | self.value * asset_ticker[action], | 99 | self.value * Trade.compute_value(asset_ticker, action, compute_value=compute_value), |
100 | linked_to=self, | 100 | linked_to=self, |
101 | ticker=asset_ticker) | 101 | ticker=asset_ticker) |
102 | else: | 102 | else: |
@@ -175,12 +175,12 @@ class Balance: | |||
175 | return cls(currency, hash_["total"], hash_["free"], hash_["used"]) | 175 | return cls(currency, hash_["total"], hash_["free"], hash_["used"]) |
176 | 176 | ||
177 | @classmethod | 177 | @classmethod |
178 | def in_currency(cls, other_currency, market, action="average", type="total"): | 178 | def in_currency(cls, other_currency, market, compute_value="average", type="total"): |
179 | amounts = {} | 179 | amounts = {} |
180 | for currency in cls.known_balances: | 180 | for currency in cls.known_balances: |
181 | balance = cls.known_balances[currency] | 181 | balance = cls.known_balances[currency] |
182 | other_currency_amount = getattr(balance, type)\ | 182 | other_currency_amount = getattr(balance, type)\ |
183 | .in_currency(other_currency, market, action=action) | 183 | .in_currency(other_currency, market, compute_value=compute_value) |
184 | amounts[currency] = other_currency_amount | 184 | amounts[currency] = other_currency_amount |
185 | return amounts | 185 | return amounts |
186 | 186 | ||
@@ -213,16 +213,34 @@ class Balance: | |||
213 | return amounts | 213 | return amounts |
214 | 214 | ||
215 | @classmethod | 215 | @classmethod |
216 | def prepare_trades(cls, market, base_currency="BTC"): | 216 | def prepare_trades(cls, market, base_currency="BTC", compute_value=None): |
217 | cls.fetch_balances(market) | 217 | cls.fetch_balances(market) |
218 | values_in_base = cls.in_currency(base_currency, market) | 218 | values_in_base = cls.in_currency(base_currency, market) |
219 | total_base_value = sum(values_in_base.values()) | 219 | total_base_value = sum(values_in_base.values()) |
220 | new_repartition = cls.dispatch_assets(total_base_value) | 220 | new_repartition = cls.dispatch_assets(total_base_value) |
221 | Trade.compute_trades(values_in_base, new_repartition, market=market) | 221 | # Recompute it in case we have new currencies |
222 | values_in_base = cls.in_currency(base_currency, market) | ||
223 | Trade.compute_trades(values_in_base, new_repartition, market=market, compute_value=compute_value) | ||
222 | 224 | ||
223 | def __repr__(self): | 225 | def __repr__(self): |
224 | return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) | 226 | return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) |
225 | 227 | ||
228 | class Computation: | ||
229 | def average_inverse(ticker, action): | ||
230 | if ticker["inverted"]: | ||
231 | return 1/ticker["original"]["average"] | ||
232 | else: | ||
233 | return ticker["average"] | ||
234 | |||
235 | computations = { | ||
236 | "default": lambda x, y: x[y], | ||
237 | "average_inverse": average_inverse, | ||
238 | "average": lambda x, y: x["average"], | ||
239 | "bid": lambda x, y: x["bid"], | ||
240 | "ask": lambda x, y: x["ask"], | ||
241 | } | ||
242 | |||
243 | |||
226 | class Trade: | 244 | class Trade: |
227 | trades = {} | 245 | trades = {} |
228 | 246 | ||
@@ -282,7 +300,7 @@ class Trade: | |||
282 | return cls.get_ticker(c1, c2, market) | 300 | return cls.get_ticker(c1, c2, market) |
283 | 301 | ||
284 | @classmethod | 302 | @classmethod |
285 | def compute_trades(cls, values_in_base, new_repartition, market=None): | 303 | def compute_trades(cls, values_in_base, new_repartition, market=None, compute_value=None): |
286 | base_currency = sum(values_in_base.values()).currency | 304 | base_currency = sum(values_in_base.values()).currency |
287 | for currency in Balance.currencies(): | 305 | for currency in Balance.currencies(): |
288 | if currency == base_currency: | 306 | if currency == base_currency: |
@@ -293,7 +311,8 @@ class Trade: | |||
293 | currency, | 311 | currency, |
294 | market=market | 312 | market=market |
295 | ) | 313 | ) |
296 | cls.trades[currency].prepare_order() | 314 | if compute_value is not None: |
315 | cls.trades[currency].prepare_order(compute_value=compute_value) | ||
297 | return cls.trades | 316 | return cls.trades |
298 | 317 | ||
299 | @property | 318 | @property |
@@ -314,7 +333,7 @@ class Trade: | |||
314 | else: | 333 | else: |
315 | return "bid" if not inverted else "ask" | 334 | return "bid" if not inverted else "ask" |
316 | 335 | ||
317 | def prepare_order(self): | 336 | def prepare_order(self, compute_value="default"): |
318 | if self.action is None: | 337 | if self.action is None: |
319 | return | 338 | return |
320 | ticker = self.value_from.ticker | 339 | ticker = self.value_from.ticker |
@@ -322,7 +341,9 @@ class Trade: | |||
322 | 341 | ||
323 | if not inverted: | 342 | if not inverted: |
324 | value_from = self.value_from.linked_to | 343 | value_from = self.value_from.linked_to |
325 | value_to = self.value_to.in_currency(self.currency, self.market) | 344 | # The ticker will most probably be inverted, but we want the average |
345 | # of the initial value | ||
346 | value_to = self.value_to.in_currency(self.currency, self.market, compute_value="average_inverse") | ||
326 | delta = abs(value_to - value_from) | 347 | delta = abs(value_to - value_from) |
327 | currency = self.base_currency | 348 | currency = self.base_currency |
328 | else: | 349 | else: |
@@ -330,11 +351,17 @@ class Trade: | |||
330 | delta = abs(self.value_to - self.value_from) | 351 | delta = abs(self.value_to - self.value_from) |
331 | currency = self.currency | 352 | currency = self.currency |
332 | 353 | ||
333 | rate = ticker[self.order_action(inverted)] | 354 | rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) |
334 | 355 | ||
335 | self.orders.append(Order(self.order_action(inverted), delta, rate, currency)) | 356 | self.orders.append(Order(self.order_action(inverted), delta, rate, currency)) |
336 | 357 | ||
337 | @classmethod | 358 | @classmethod |
359 | def compute_value(cls, ticker, action, compute_value="default"): | ||
360 | if type(compute_value) == str: | ||
361 | compute_value = Computation.computations[compute_value] | ||
362 | return compute_value(ticker, action) | ||
363 | |||
364 | @classmethod | ||
338 | def all_orders(cls): | 365 | def all_orders(cls): |
339 | return sum(map(lambda v: v.orders, cls.trades.values()), []) | 366 | return sum(map(lambda v: v.orders, cls.trades.values()), []) |
340 | 367 | ||
@@ -401,7 +428,7 @@ class Order: | |||
401 | return self.status | 428 | return self.status |
402 | 429 | ||
403 | def print_orders(market, base_currency="BTC"): | 430 | def print_orders(market, base_currency="BTC"): |
404 | Balance.prepare_trades(market, base_currency=base_currency) | 431 | Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") |
405 | for currency, balance in Balance.known_balances.items(): | 432 | for currency, balance in Balance.known_balances.items(): |
406 | print(balance) | 433 | print(balance) |
407 | for currency, trade in Trade.trades.items(): | 434 | for currency, trade in Trade.trades.items(): |
@@ -22,6 +22,8 @@ class AmountTest(unittest.TestCase): | |||
22 | 22 | ||
23 | with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): | 23 | with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): |
24 | ticker_mock.return_value = { | 24 | ticker_mock.return_value = { |
25 | "bid": D("0.2"), | ||
26 | "ask": D("0.4"), | ||
25 | "average": D("0.3"), | 27 | "average": D("0.3"), |
26 | "foo": "bar", | 28 | "foo": "bar", |
27 | } | 29 | } |
@@ -32,6 +34,12 @@ class AmountTest(unittest.TestCase): | |||
32 | self.assertEqual(amount, converted_amount.linked_to) | 34 | self.assertEqual(amount, converted_amount.linked_to) |
33 | self.assertEqual("bar", converted_amount.ticker["foo"]) | 35 | self.assertEqual("bar", converted_amount.ticker["foo"]) |
34 | 36 | ||
37 | converted_amount = amount.in_currency("ETH", None, action="bid", compute_value="default") | ||
38 | self.assertEqual(D("2"), converted_amount.value) | ||
39 | |||
40 | converted_amount = amount.in_currency("ETH", None, compute_value="ask") | ||
41 | self.assertEqual(D("4"), converted_amount.value) | ||
42 | |||
35 | def test__abs(self): | 43 | def test__abs(self): |
36 | amount = portfolio.Amount("SC", -120) | 44 | amount = portfolio.Amount("SC", -120) |
37 | self.assertEqual(120, abs(amount).value) | 45 | self.assertEqual(120, abs(amount).value) |
@@ -295,11 +303,11 @@ class BalanceTest(unittest.TestCase): | |||
295 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 303 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
296 | self.assertEqual(D("0.30"), amounts["ETH"].value) | 304 | self.assertEqual(D("0.30"), amounts["ETH"].value) |
297 | 305 | ||
298 | amounts = portfolio.Balance.in_currency("BTC", market, action="bid") | 306 | amounts = portfolio.Balance.in_currency("BTC", market, compute_value="bid") |
299 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 307 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
300 | self.assertEqual(D("0.27"), amounts["ETH"].value) | 308 | self.assertEqual(D("0.27"), amounts["ETH"].value) |
301 | 309 | ||
302 | amounts = portfolio.Balance.in_currency("BTC", market, action="bid", type="used") | 310 | amounts = portfolio.Balance.in_currency("BTC", market, compute_value="bid", type="used") |
303 | self.assertEqual(D("0.30"), amounts["BTC"].value) | 311 | self.assertEqual(D("0.30"), amounts["BTC"].value) |
304 | self.assertEqual(0, amounts["ETH"].value) | 312 | self.assertEqual(0, amounts["ETH"].value) |
305 | 313 | ||
@@ -344,10 +352,16 @@ class BalanceTest(unittest.TestCase): | |||
344 | "XEM": 7500, | 352 | "XEM": 7500, |
345 | "BTC": 2500, | 353 | "BTC": 2500, |
346 | } | 354 | } |
347 | get_ticker.side_effect = [ | 355 | def _get_ticker(c1, c2, market): |
348 | { "average": D("0.0001") }, | 356 | if c1 == "USDT" and c2 == "BTC": |
349 | { "average": D("0.000001") } | 357 | return { "average": D("0.0001") } |
350 | ] | 358 | if c1 == "XVG" and c2 == "BTC": |
359 | return { "average": D("0.000001") } | ||
360 | if c1 == "XEM" and c2 == "BTC": | ||
361 | return { "average": D("0.001") } | ||
362 | raise Exception("Should be called with {}, {}".format(c1, c2)) | ||
363 | get_ticker.side_effect = _get_ticker | ||
364 | |||
351 | market = mock.Mock() | 365 | market = mock.Mock() |
352 | market.fetch_balance.return_value = { | 366 | market.fetch_balance.return_value = { |
353 | "USDT": { | 367 | "USDT": { |
@@ -493,6 +507,10 @@ class TradeTest(unittest.TestCase): | |||
493 | pass | 507 | pass |
494 | 508 | ||
495 | @unittest.skip("TODO") | 509 | @unittest.skip("TODO") |
510 | def test_compute_value(self): | ||
511 | pass | ||
512 | |||
513 | @unittest.skip("TODO") | ||
496 | def test__repr(self): | 514 | def test__repr(self): |
497 | pass | 515 | pass |
498 | 516 | ||