aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--portfolio.py430
-rw-r--r--script.py218
-rw-r--r--test.py174
3 files changed, 604 insertions, 218 deletions
diff --git a/portfolio.py b/portfolio.py
new file mode 100644
index 0000000..507f796
--- /dev/null
+++ b/portfolio.py
@@ -0,0 +1,430 @@
1import ccxt
2import time
3# Put your poloniex api key in market.py
4from market import market
5
6# FIXME: Améliorer le bid/ask
7# FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition
8# FIXME: better compute moves to avoid rounding errors
9
10def static_var(varname, value):
11 def decorate(func):
12 setattr(func, varname, value)
13 return func
14 return decorate
15
16class Portfolio:
17 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
18 liquidities = {}
19 data = None
20
21 @classmethod
22 def repartition_pertenthousand(cls, liquidity="medium"):
23 cls.parse_cryptoportfolio()
24 liquidities = cls.liquidities[liquidity]
25 last_date = sorted(liquidities.keys())[-1]
26 return liquidities[last_date]
27
28 @classmethod
29 def get_cryptoportfolio(cls):
30 import json
31 import urllib3
32 urllib3.disable_warnings()
33 http = urllib3.PoolManager()
34
35 r = http.request("GET", cls.URL)
36 cls.data = json.loads(r.data)
37
38 @classmethod
39 def parse_cryptoportfolio(cls):
40 if cls.data is None:
41 cls.get_cryptoportfolio()
42
43 def filter_weights(weight_hash):
44 if weight_hash[1] == 0:
45 return False
46 if weight_hash[0] == "_row":
47 return False
48 return True
49
50 def clean_weights(i):
51 def clean_weights_(h):
52 if type(h[1][i]) == str:
53 return [h[0], h[1][i]]
54 else:
55 return [h[0], int(h[1][i] * 10000)]
56 return clean_weights_
57
58 def parse_weights(portfolio_hash):
59 # FIXME: we'll need shorts at some point
60 assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"]))
61 weights_hash = portfolio_hash["weights"]
62 weights = {}
63 for i in range(len(weights_hash["_row"])):
64 weights[weights_hash["_row"][i]] = dict(filter(
65 filter_weights,
66 map(clean_weights(i), weights_hash.items())))
67 return weights
68
69 high_liquidity = parse_weights(cls.data["portfolio_1"])
70 medium_liquidity = parse_weights(cls.data["portfolio_2"])
71
72 cls.liquidities = {
73 "medium": medium_liquidity,
74 "high": high_liquidity,
75 }
76
77class Amount:
78 MAX_DIGITS = 18
79
80 def __init__(self, currency, value, int_val=None, linked_to=None, ticker=None):
81 self.currency = currency
82 if int_val is None:
83 self._value = int(value * 10**self.MAX_DIGITS)
84 else:
85 self._value = int_val
86 self.linked_to = linked_to
87 self.ticker = ticker
88
89 self.ticker_cache = {}
90 self.ticker_cache_timestamp = time.time()
91
92 @property
93 def value(self):
94 return self._value / 10 ** self.MAX_DIGITS
95
96 def in_currency(self, other_currency, market, action="average"):
97 if other_currency == self.currency:
98 return self
99 asset_ticker = self.get_ticker(other_currency, market)
100 if asset_ticker is not None:
101 return Amount(
102 other_currency,
103 0,
104 int_val=int(self._value * asset_ticker[action]),
105 linked_to=self,
106 ticker=asset_ticker)
107 else:
108 raise Exception("This asset is not available in the chosen market")
109
110 def get_ticker(self, c2, market, refresh=False):
111 c1 = self.currency
112
113 def invert(ticker):
114 return {
115 "inverted": True,
116 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
117 "notInverted": ticker,
118 }
119 def augment_ticker(ticker):
120 ticker.update({
121 "inverted": False,
122 "average": (ticker["bid"] + ticker["ask"] ) / 2,
123 })
124
125 if time.time() - self.ticker_cache_timestamp > 5:
126 self.ticker_cache = {}
127 self.ticker_cache_timestamp = time.time()
128 elif not refresh:
129 if (c1, c2, market.__class__) in self.ticker_cache:
130 return self.ticker_cache[(c1, c2, market.__class__)]
131 if (c2, c1, market.__class__) in self.ticker_cache:
132 return invert(self.ticker_cache[(c2, c1, market.__class__)])
133
134 try:
135 self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
136 augment_ticker(self.ticker_cache[(c1, c2, market.__class__)])
137 except ccxt.ExchangeError:
138 try:
139 self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
140 augment_ticker(self.ticker_cache[(c2, c1, market.__class__)])
141 except ccxt.ExchangeError:
142 self.ticker_cache[(c1, c2, market.__class__)] = None
143 return self.get_ticker(c2, market)
144
145 def __abs__(self):
146 return Amount(self.currency, 0, int_val=abs(self._value))
147
148 def __add__(self, other):
149 if other.currency != self.currency and other._value * self._value != 0:
150 raise Exception("Summing amounts must be done with same currencies")
151 return Amount(self.currency, 0, int_val=self._value + other._value)
152
153 def __radd__(self, other):
154 if other == 0:
155 return self
156 else:
157 return self.__add__(other)
158
159 def __sub__(self, other):
160 if other.currency != self.currency and other._value * self._value != 0:
161 raise Exception("Summing amounts must be done with same currencies")
162 return Amount(self.currency, 0, int_val=self._value - other._value)
163
164 def __int__(self):
165 return self._value
166
167 def __mul__(self, value):
168 if type(value) != int and type(value) != float:
169 raise TypeError("Amount may only be multiplied by numbers")
170 return Amount(self.currency, 0, int_val=(self._value * value))
171
172 def __rmul__(self, value):
173 return self.__mul__(value)
174
175 def __floordiv__(self, value):
176 if type(value) != int:
177 raise TypeError("Amount may only be multiplied by integers")
178 return Amount(self.currency, 0, int_val=(self._value // value))
179
180 def __truediv__(self, value):
181 return self.__floordiv__(value)
182
183 def __lt__(self, other):
184 if self.currency != other.currency:
185 raise Exception("Comparing amounts must be done with same currencies")
186 return self._value < other._value
187
188 def __eq__(self, other):
189 if other == 0:
190 return self._value == 0
191 if self.currency != other.currency:
192 raise Exception("Comparing amounts must be done with same currencies")
193 return self._value == other._value
194
195 def __str__(self):
196 if self.linked_to is None:
197 return "{:.8f} {}".format(self.value, self.currency)
198 else:
199 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
200
201 def __repr__(self):
202 if self.linked_to is None:
203 return "Amount({:.8f} {})".format(self.value, self.currency)
204 else:
205 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
206
207class Balance:
208 known_balances = {}
209 trades = {}
210
211 def __init__(self, currency, total_value, free_value, used_value):
212 self.currency = currency
213 self.total = Amount(currency, total_value)
214 self.free = Amount(currency, free_value)
215 self.used = Amount(currency, used_value)
216
217 @classmethod
218 def in_currency(cls, other_currency, market, action="average", type="total"):
219 amounts = {}
220 for currency in cls.known_balances:
221 balance = cls.known_balances[currency]
222 other_currency_amount = getattr(balance, type)\
223 .in_currency(other_currency, market, action=action)
224 amounts[currency] = other_currency_amount
225 return amounts
226
227 @classmethod
228 def currencies(cls):
229 return cls.known_balances.keys()
230
231 @classmethod
232 def from_hash(cls, currency, hash_):
233 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
234
235 @classmethod
236 def _fill_balances(cls, hash_):
237 for key in hash_:
238 if key in ["info", "free", "used", "total"]:
239 continue
240 if hash_[key]["total"] > 0:
241 cls.known_balances[key] = cls.from_hash(key, hash_[key])
242
243 @classmethod
244 def fetch_balances(cls, market):
245 cls._fill_balances(market.fetch_balance())
246 return cls.known_balances
247
248 @classmethod
249 def dispatch_assets(cls, amount):
250 repartition_pertenthousand = Portfolio.repartition_pertenthousand()
251 sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
252 amounts = {}
253 for currency, ptt in repartition_pertenthousand.items():
254 amounts[currency] = ptt * amount / sum_pertenthousand
255 if currency not in cls.known_balances:
256 cls.known_balances[currency] = cls(currency, 0, 0, 0)
257 return amounts
258
259 @classmethod
260 def prepare_trades(cls, market, base_currency="BTC"):
261 cls.fetch_balances(market)
262 values_in_base = cls.in_currency(base_currency, market)
263 total_base_value = sum(values_in_base.values())
264 new_repartition = cls.dispatch_assets(total_base_value)
265 Trade.compute_trades(values_in_base, new_repartition, market=market)
266
267 def __int__(self):
268 return int(self.total)
269
270 def __repr__(self):
271 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
272
273class Trade:
274 trades = {}
275
276 def __init__(self, value_from, value_to, currency, market=None):
277 # We have value_from of currency, and want to finish with value_to of
278 # that currency. value_* may not be in currency's terms
279 self.currency = currency
280 self.value_from = value_from
281 self.value_to = value_to
282 self.orders = []
283 assert self.value_from.currency == self.value_to.currency
284 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
285 self.base_currency = self.value_from.currency
286
287 if market is not None:
288 self.prepare_order(market)
289
290 @classmethod
291 def compute_trades(cls, values_in_base, new_repartition, market=None):
292 base_currency = sum(values_in_base.values()).currency
293 for currency in Balance.currencies():
294 if currency == base_currency:
295 continue
296 cls.trades[currency] = cls(
297 values_in_base.get(currency, Amount(base_currency, 0)),
298 new_repartition.get(currency, Amount(base_currency, 0)),
299 currency,
300 market=market
301 )
302 return cls.trades
303
304 @property
305 def action(self):
306 if self.value_from == self.value_to:
307 return None
308 if self.base_currency == self.currency:
309 return None
310
311 if self.value_from < self.value_to:
312 return "buy"
313 else:
314 return "sell"
315
316 def ticker_action(self, inverted):
317 if self.value_from < self.value_to:
318 return "ask" if not inverted else "bid"
319 else:
320 return "bid" if not inverted else "ask"
321
322 def prepare_order(self, market):
323 if self.action is None:
324 return
325 ticker = self.value_from.ticker
326 inverted = ticker["inverted"]
327
328 if not inverted:
329 value_from = self.value_from.linked_to
330 value_to = self.value_to.in_currency(self.currency, market)
331 delta = abs(value_to - value_from)
332 currency = self.base_currency
333 else:
334 ticker = ticker["notInverted"]
335 delta = abs(self.value_to - self.value_from)
336 currency = self.currency
337
338 rate = ticker[self.ticker_action(inverted)]
339
340 self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency))
341
342 @classmethod
343 def all_orders(cls):
344 return sum(map(lambda v: v.orders, cls.trades.values()), [])
345
346 @classmethod
347 def follow_orders(cls, market):
348 orders = cls.all_orders()
349 finished_orders = []
350 while len(orders) != len(finished_orders):
351 time.sleep(30)
352 for order in orders:
353 if order in finished_orders:
354 continue
355 if order.get_status(market) != "open":
356 finished_orders.append(order)
357 print("finished {}".format(order))
358 print("All orders finished")
359
360 def __repr__(self):
361 return "Trade({} -> {} in {}, {})".format(
362 self.value_from,
363 self.value_to,
364 self.currency,
365 self.action)
366
367class Order:
368 DEBUG = True
369
370 def __init__(self, action, amount, rate, base_currency):
371 self.action = action
372 self.amount = amount
373 self.rate = rate
374 self.base_currency = base_currency
375 self.result = None
376 self.status = "not run"
377
378 def __repr__(self):
379 return "Order({} {} at {} {} [{}])".format(
380 self.action,
381 self.amount,
382 self.rate,
383 self.base_currency,
384 self.status
385 )
386
387 def run(self, market):
388 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
389 amount = self.amount.value
390
391 if self.DEBUG:
392 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
393 symbol, self.action, amount, self.rate))
394 else:
395 try:
396 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
397 self.status = "open"
398 except Exception:
399 pass
400
401 def get_status(self, market):
402 # other states are "closed" and "canceled"
403 if self.status == "open":
404 result = market.fetch_order(self.result['id'])
405 self.status = result["status"]
406 return self.status
407
408@static_var("cache", {})
409def fetch_fees(market):
410 if market.__class__ not in fetch_fees.cache:
411 fetch_fees.cache[market.__class__] = market.fetch_fees()
412 return fetch_fees.cache[market.__class__]
413
414def print_orders(market, base_currency="BTC"):
415 Balance.prepare_trades(market, base_currency=base_currency)
416 for currency, trade in Trade.trades.items():
417 print(trade)
418 for order in trade.orders:
419 print("\t", order, sep="")
420
421def make_orders(market, base_currency="BTC"):
422 Balance.prepare_trades(market, base_currency=base_currency)
423 for currency, trade in Trade.trades.items():
424 print(trade)
425 for order in trade.orders:
426 print("\t", order, sep="")
427 order.run(market)
428
429if __name__ == '__main__':
430 print_orders(market)
diff --git a/script.py b/script.py
deleted file mode 100644
index 187ff73..0000000
--- a/script.py
+++ /dev/null
@@ -1,218 +0,0 @@
1import ccxt
2# Put your poloniex api key in market.py
3from market import market
4
5def static_var(varname, value):
6 def decorate(func):
7 setattr(func, varname, value)
8 return func
9 return decorate
10
11max_digits = 18
12
13repartition_pertenthousand = {
14 "BTC": 2857,
15 "ZEC": 3701,
16 "DOGE": 1805,
17 "DGB": 1015,
18 "SC": 623,
19 }
20
21
22def formatted_price(value):
23 return round(value / 10**max_digits, 8)
24
25@static_var("cache", {})
26def get_ticker(c1, c2, market):
27 def invert(ticker):
28 return {
29 "inverted": True,
30 "bid": float(1/ticker["ask"]),
31 "ask": float(1/ticker["bid"]),
32 "bidA": float(1/ticker["askA"]),
33 "askA": float(1/ticker["bidA"]),
34 "bidE": float(1/ticker["askE"]),
35 "askE": float(1/ticker["bidE"]),
36 }
37 def augment_ticker(ticker):
38 bid_factor = 1.01
39 ask_factor = 0.99
40 fees = fetch_fees(market)
41 # FIXME: need to do better than just a multiplier
42 ticker.update({
43 "inverted": False,
44 # Adjusted
45 "bidA": ticker["bid"] * bid_factor,
46 "askA": ticker["ask"] * ask_factor,
47 # Expected in the end
48 "bidE": ticker["bid"] * bid_factor * (1 - fees["maker"]),
49 "askE": ticker["ask"] * ask_factor * (1 - fees["maker"]),
50 # fees
51 "bidF": ticker["bid"] * bid_factor * fees["maker"],
52 "askF": ticker["ask"] * ask_factor * fees["maker"],
53 })
54
55 if (c1, c2, market.__class__) in get_ticker.cache:
56 return get_ticker.cache[(c1, c2, market.__class__)]
57 if (c2, c1, market.__class__) in get_ticker.cache:
58 return invert(get_ticker.cache[(c2, c1, market.__class__)])
59
60 try:
61 get_ticker.cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
62 augment_ticker(get_ticker.cache[(c1, c2, market.__class__)])
63 except ccxt.ExchangeError:
64 try:
65 get_ticker.cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
66 augment_ticker(get_ticker.cache[(c2, c1, market.__class__)])
67 except ccxt.ExchangeError:
68 get_ticker.cache[(c1, c2, market.__class__)] = None
69 return get_ticker(c1, c2, market)
70
71def fetch_balances(market):
72 balances = {}
73 fetched_balance = market.fetch_balance()
74 for key, value in fetched_balance["total"].items():
75 if value > 0:
76 balances[key] = int(value * 10**max_digits)
77 return balances
78
79@static_var("cache", {})
80def fetch_fees(market):
81 if market.__class__ not in fetch_fees.cache:
82 fetch_fees.cache[market.__class__] = market.fetch_fees()
83 return fetch_fees.cache[market.__class__]
84
85def assets_value(assets, market, base_currency="BTC"):
86 repartition_in_base_currency = {}
87 for currency, asset_value in assets.items():
88 if currency == base_currency:
89 repartition_in_base_currency[currency] = [asset_value, 0]
90 else:
91 asset_ticker = get_ticker(currency, base_currency, market)
92 if asset_ticker is None:
93 raise Exception("This asset is not available in the chosen market")
94 repartition_in_base_currency[currency] = [
95 int(asset_ticker["bidE"] * asset_value),
96 int(asset_ticker["bidF"] * asset_value)
97 ]
98
99 return repartition_in_base_currency
100
101def dispatch_assets(base_currency_value, repartition_pertenthousand, market, base_currency="BTC"):
102 sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
103 repartition_in_base_currency = {}
104 for currency, ptt in repartition_pertenthousand.items():
105 repartition_in_base_currency[currency] = int(ptt * base_currency_value / sum_pertenthousand)
106 return repartition_in_base_currency
107
108def compute_moves(current_assets, repartition_pertenthousand, market, no_fees=True, base_currency="BTC"):
109 value_in_base = assets_value(current_assets, market, base_currency=base_currency)
110 total_base_value = sum([ v[0] for k, v in value_in_base.items()])
111
112 new_repartition = dispatch_assets(total_base_value, repartition_pertenthousand, market, base_currency=base_currency)
113 mouvements = {}
114
115 if no_fees:
116 for key in set(value_in_base.keys()).union(set(new_repartition.keys())):
117 mouvements[key] = value_in_base.get(key, [0, 0])[0] - new_repartition.get(key, 0)
118 else:
119 for key in set(value_in_base.keys()).union(set(new_repartition.keys())):
120 value, fee = value_in_base.get(key, [0, 0])
121 mouvements[key] = [value - new_repartition.get(key, 0), fee]
122
123 return mouvements
124
125def compute_order(currency, value, market, base_currency="BTC"):
126 if currency == base_currency or value == 0:
127 return [None, 0, False]
128
129 asset_ticker = get_ticker(currency, base_currency, market)
130 if asset_ticker["inverted"]:
131 asset_ticker = get_ticker(base_currency, currency, market)
132 if value > 0:
133 rate = asset_ticker["askA"]
134 return ["buy", rate, True]
135 else:
136 rate = asset_ticker["bidA"]
137 return ["sell", rate, True]
138 else:
139 if value > 0:
140 rate = asset_ticker["bidA"]
141 return ["sell", rate, False]
142 else:
143 rate = asset_ticker["askA"]
144 return ["buy", rate, False]
145
146def make_order(currency, value, market, base_currency="BTC"):
147 action, rate, inverted = compute_order(currency, value, market, base_currency=base_currency)
148 amount = formatted_price(abs(value))
149 if not inverted:
150 symbol = "{}/{}".format(currency, base_currency)
151 else:
152 symbol = "{}/{}".format(base_currency, currency)
153 return market.create_order(symbol, 'limit', action, amount, price=rate)
154
155def make_orders(current_assets, repartition_pertenthousand, market, base_currency="BTC"):
156 mouvements = compute_moves(
157 current_assets,
158 repartition_pertenthousand,
159 market,
160 base_currency=base_currency)
161
162 results = []
163 for currency, value in sorted(mouvements.items(), key=lambda x: x[1]):
164 # FIXME: wait for sales to finish
165 results.append(make_order(currency, value, market, base_currency=base_currency))
166 return results
167
168def print_assets(assets, indent="", market=None, base_currency="BTC"):
169 if market is not None:
170 format_string = "{}{} {} ({} {})"
171 else:
172 format_string = "{}{} {}"
173 base_currency_price = 0
174
175 for currency, value in assets.items():
176 if market is not None:
177 asset_ticker = get_ticker(currency, base_currency, market)
178 base_currency_price = asset_ticker["bidE"] * value
179 print(format_string.format(
180 indent,
181 formatted_price(value),
182 currency,
183 formatted_price(base_currency_price),
184 base_currency))
185
186def print_orders(current_assets, repartition_pertenthousand, market, base_currency="BTC"):
187 mouvements = compute_moves(
188 current_assets,
189 repartition_pertenthousand,
190 market,
191 no_fees=False,
192 base_currency=base_currency)
193
194 for currency, [value, fee] in mouvements.items():
195 action, rate, inverted = compute_order(
196 currency,
197 value,
198 market,
199 base_currency=base_currency)
200 if action is not None:
201 currency_price = int(value / rate)
202
203 if not inverted:
204 c1, c2 = [base_currency, currency]
205 v1, v2 = [value, currency_price]
206 else:
207 c1, c2 = [currency, base_currency]
208 v1, v2 = [currency_price, value]
209
210 print("need to {} {} {}'s worth of {}, i.e. {} {} ( + {} {} fee)".format(
211 action,
212 formatted_price(abs(v1)), c1,
213 c2,
214 formatted_price(abs(v2)), c2,
215 formatted_price(fee), c2))
216
217current_assets = fetch_balances(market)
218print_orders(current_assets, repartition_pertenthousand, market)
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..9e228d7
--- /dev/null
+++ b/test.py
@@ -0,0 +1,174 @@
1import portfolio
2import unittest
3from unittest import mock
4
5class AmountTest(unittest.TestCase):
6 def setUp(self):
7 super(AmountTest, self).setUp()
8
9 def test_values(self):
10 amount = portfolio.Amount("BTC", 0.65)
11 self.assertEqual(0.65, amount.value)
12 self.assertEqual("BTC", amount.currency)
13
14 amount = portfolio.Amount("BTC", 10, int_val=2000000000000000)
15 self.assertEqual(0.002, amount.value)
16
17 def test_in_currency(self):
18 amount = portfolio.Amount("ETC", 10)
19
20 self.assertEqual(amount, amount.in_currency("ETC", None))
21
22 ticker_mock = unittest.mock.Mock()
23 with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock):
24 ticker_mock.return_value = None
25 portfolio.Amount.get_ticker = ticker_mock
26
27 self.assertRaises(Exception, amount.in_currency, "ETH", None)
28
29 with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock):
30 ticker_mock.return_value = {
31 "average": 0.3,
32 "foo": "bar",
33 }
34 converted_amount = amount.in_currency("ETH", None)
35
36 self.assertEqual(3.0, converted_amount.value)
37 self.assertEqual("ETH", converted_amount.currency)
38 self.assertEqual(amount, converted_amount.linked_to)
39 self.assertEqual("bar", converted_amount.ticker["foo"])
40
41 @unittest.skip("TODO")
42 def test_get_ticker(self):
43 pass
44
45 def test__abs(self):
46 amount = portfolio.Amount("SC", -120)
47 self.assertEqual(120, abs(amount).value)
48 self.assertEqual("SC", abs(amount).currency)
49
50 amount = portfolio.Amount("SC", 10)
51 self.assertEqual(10, abs(amount).value)
52 self.assertEqual("SC", abs(amount).currency)
53
54 def test__add(self):
55 amount1 = portfolio.Amount("XVG", 12.9)
56 amount2 = portfolio.Amount("XVG", 13.1)
57
58 self.assertEqual(26, (amount1 + amount2).value)
59 self.assertEqual("XVG", (amount1 + amount2).currency)
60
61 amount3 = portfolio.Amount("ETH", 1.6)
62 with self.assertRaises(Exception):
63 amount1 + amount3
64
65 amount4 = portfolio.Amount("ETH", 0.0)
66 self.assertEqual(amount1, amount1 + amount4)
67
68 def test__radd(self):
69 amount = portfolio.Amount("XVG", 12.9)
70
71 self.assertEqual(amount, 0 + amount)
72 with self.assertRaises(Exception):
73 4 + amount
74
75 def test__sub(self):
76 amount1 = portfolio.Amount("XVG", 13.3)
77 amount2 = portfolio.Amount("XVG", 13.1)
78
79 self.assertEqual(0.2, (amount1 - amount2).value)
80 self.assertEqual("XVG", (amount1 - amount2).currency)
81
82 amount3 = portfolio.Amount("ETH", 1.6)
83 with self.assertRaises(Exception):
84 amount1 - amount3
85
86 amount4 = portfolio.Amount("ETH", 0.0)
87 self.assertEqual(amount1, amount1 - amount4)
88
89 def test__int(self):
90 amount = portfolio.Amount("XMR", 0.1)
91 self.assertEqual(100000000000000000, int(amount))
92
93 def test__mul(self):
94 amount = portfolio.Amount("XEM", 11)
95
96 self.assertEqual(38.5, (amount * 3.5).value)
97 self.assertEqual(33, (amount * 3).value)
98
99 with self.assertRaises(Exception):
100 amount * amount
101
102 def test__rmul(self):
103 amount = portfolio.Amount("XEM", 11)
104
105 self.assertEqual(38.5, (3.5 * amount).value)
106 self.assertEqual(33, (3 * amount).value)
107
108 def test__floordiv(self):
109 amount = portfolio.Amount("XEM", 11)
110
111 self.assertEqual(5.5, (amount // 2).value)
112 with self.assertRaises(TypeError):
113 amount // 2.5
114 self.assertEqual(1571428571428571428, (amount // 7)._value)
115
116 def test__div(self):
117 amount = portfolio.Amount("XEM", 11)
118
119 with self.assertRaises(TypeError):
120 amount / 2.5
121 self.assertEqual(5.5, (amount / 2).value)
122 self.assertEqual(1571428571428571428, (amount / 7)._value)
123
124 def test__lt(self):
125 amount1 = portfolio.Amount("BTD", 11.3)
126 amount2 = portfolio.Amount("BTD", 13.1)
127
128 self.assertTrue(amount1 < amount2)
129 self.assertFalse(amount2 < amount1)
130 self.assertFalse(amount1 < amount1)
131
132 amount3 = portfolio.Amount("BTC", 1.6)
133 with self.assertRaises(Exception):
134 amount1 < amount3
135
136 def test__eq(self):
137 amount1 = portfolio.Amount("BTD", 11.3)
138 amount2 = portfolio.Amount("BTD", 13.1)
139 amount3 = portfolio.Amount("BTD", 11.3)
140
141 self.assertFalse(amount1 == amount2)
142 self.assertFalse(amount2 == amount1)
143 self.assertTrue(amount1 == amount3)
144 self.assertFalse(amount2 == 0)
145
146 amount4 = portfolio.Amount("BTC", 1.6)
147 with self.assertRaises(Exception):
148 amount1 == amount4
149
150 amount5 = portfolio.Amount("BTD", 0)
151 self.assertTrue(amount5 == 0)
152
153 def test__str(self):
154 amount1 = portfolio.Amount("BTX", 32)
155 self.assertEqual("32.00000000 BTX", str(amount1))
156
157 amount2 = portfolio.Amount("USDT", 12000)
158 amount1.linked_to = amount2
159 self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1))
160
161 def test__repr(self):
162 amount1 = portfolio.Amount("BTX", 32)
163 self.assertEqual("Amount(32.00000000 BTX)", repr(amount1))
164
165 amount2 = portfolio.Amount("USDT", 12000)
166 amount1.linked_to = amount2
167 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1))
168
169 amount3 = portfolio.Amount("BTC", 0.1)
170 amount2.linked_to = amount3
171 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
172
173if __name__ == '__main__':
174 unittest.main()