aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-01 13:14:41 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-01 13:14:41 +0100
commitaca4d4372553110ab5d76740ff536de83d5617b2 (patch)
treea9bfdca4226daf422273da97a9e139721469c9f1
parent2033e7fef780298be2ec15455a0ec1d26515de55 (diff)
downloadTrader-aca4d4372553110ab5d76740ff536de83d5617b2.tar.gz
Trader-aca4d4372553110ab5d76740ff536de83d5617b2.tar.zst
Trader-aca4d4372553110ab5d76740ff536de83d5617b2.zip
Various fixes/improvementsv0.3
- Use pending gains to compute the move_balance - Use ttl_cache for tickers
-rw-r--r--ccxt_wrapper.py109
-rw-r--r--market.py69
-rw-r--r--portfolio.py99
-rw-r--r--requirements.txt1
-rw-r--r--store.py17
-rw-r--r--test.py344
6 files changed, 394 insertions, 245 deletions
diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py
index b79cd37..903bfc4 100644
--- a/ccxt_wrapper.py
+++ b/ccxt_wrapper.py
@@ -38,12 +38,13 @@ class poloniexE(poloniex):
38 """ 38 """
39 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"}) 39 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
40 See DASH/BTC positions 40 See DASH/BTC positions
41 {'amount': '-0.10000000', -> DASH empruntés 41 {'amount': '-0.10000000', -> DASH empruntés (à rendre)
42 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%)) 42 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%))
43 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur 43 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur en intérêts
44 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin) 44 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin)
45 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu) 45 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu)
46 'total': '0.00681856', -> valeur totale empruntée en BTC 46 'total': '0.00681856', -> valeur totale empruntée en BTC (au moment de l'échange)
47 = amount * basePrice à erreur d'arrondi près
47 'type': 'short'} 48 'type': 'short'}
48 """ 49 """
49 positions = self.privatePostGetMarginPosition({"currencyPair": "all"}) 50 positions = self.privatePostGetMarginPosition({"currencyPair": "all"})
@@ -70,8 +71,7 @@ class poloniexE(poloniex):
70 for key, balance in balances.items(): 71 for key, balance in balances.items():
71 result[key] = {} 72 result[key] = {}
72 for currency, amount in balance.items(): 73 for currency, amount in balance.items():
73 if currency not in result: 74 result.setdefault(currency, {})
74 result[currency] = {}
75 result[currency][key] = decimal.Decimal(amount) 75 result[currency][key] = decimal.Decimal(amount)
76 result[key][currency] = decimal.Decimal(amount) 76 result[key][currency] = decimal.Decimal(amount)
77 return result 77 return result
@@ -83,6 +83,7 @@ class poloniexE(poloniex):
83 83
84 all_balances = {} 84 all_balances = {}
85 in_positions = {} 85 in_positions = {}
86 pending_pl = {}
86 87
87 for currency, exchange_balance in exchange_balances.items(): 88 for currency, exchange_balance in exchange_balances.items():
88 if currency in ["info", "free", "used", "total"]: 89 if currency in ["info", "free", "used", "total"]:
@@ -96,25 +97,106 @@ class poloniexE(poloniex):
96 "exchange_used": exchange_balance["used"], 97 "exchange_used": exchange_balance["used"],
97 "exchange_total": exchange_balance["total"] - balance_per_type.get("margin", 0), 98 "exchange_total": exchange_balance["total"] - balance_per_type.get("margin", 0),
98 "exchange_free": exchange_balance["free"] - balance_per_type.get("margin", 0), 99 "exchange_free": exchange_balance["free"] - balance_per_type.get("margin", 0),
99 "margin_free": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0), 100 # Disponible sur le compte margin
100 "margin_borrowed": 0, 101 "margin_available": balance_per_type.get("margin", 0),
102 # Bloqué en position
103 "margin_in_position": 0,
104 # Emprunté
105 "margin_borrowed": -margin_balance.get("amount", 0),
106 # Total
101 "margin_total": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0), 107 "margin_total": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
108 "margin_pending_gain": 0,
102 "margin_lending_fees": margin_balance.get("lendingFees", 0), 109 "margin_lending_fees": margin_balance.get("lendingFees", 0),
103 "margin_pending_gain": margin_balance.get("pl", 0), 110 "margin_pending_base_gain": margin_balance.get("pl", 0),
104 "margin_position_type": margin_balance.get("type", None), 111 "margin_position_type": margin_balance.get("type", None),
105 "margin_liquidation_price": margin_balance.get("liquidationPrice", 0), 112 "margin_liquidation_price": margin_balance.get("liquidationPrice", 0),
106 "margin_borrowed_base_price": margin_balance.get("total", 0), 113 "margin_borrowed_base_price": margin_balance.get("total", 0),
107 "margin_borrowed_base_currency": margin_balance.get("baseCurrency", None), 114 "margin_borrowed_base_currency": margin_balance.get("baseCurrency", None),
108 } 115 }
109 if len(margin_balance) > 0: 116 if len(margin_balance) > 0:
110 if margin_balance["baseCurrency"] not in in_positions: 117 in_positions.setdefault(margin_balance["baseCurrency"], 0)
111 in_positions[margin_balance["baseCurrency"]] = 0
112 in_positions[margin_balance["baseCurrency"]] += margin_balance["total"] 118 in_positions[margin_balance["baseCurrency"]] += margin_balance["total"]
113 119
120 pending_pl.setdefault(margin_balance["baseCurrency"], 0)
121 pending_pl[margin_balance["baseCurrency"]] += margin_balance["pl"]
122
123 # J’emprunte 0.12062983 que je revends à 0.06003598 BTC/DASH, soit 0.00724213 BTC.
124 # Sur ces 0.00724213 BTC je récupère 0.00724213*(1-0.0015) = 0.00723127 BTC
125 #
126 # -> ordertrades ne tient pas compte des fees
127 # amount = montant vendu (un seul mouvement)
128 # rate = à ce taux
129 # total = total en BTC (pour ce mouvement)
130 # -> marginposition:
131 # amount = ce que je dois rendre
132 # basePrice = prix de vente en tenant compte des fees
133 # (amount * basePrice = la quantité de BTC que j’ai effectivement
134 # reçue à erreur d’arrondi près, utiliser plutôt "total")
135 # total = la quantité de BTC que j’ai reçue
136 # pl = plus value actuelle si je rachetais tout de suite
137 # -> marginaccountsummary:
138 # currentMargin = La marge actuelle (= netValue/totalBorrowedValue)
139 # totalValue = BTC actuellement en margin (déposé)
140 # totalBorrowedValue = sum (amount * ticker[lowestAsk])
141 # pl = sum(pl)
142 # netValue = BTC actuellement en margin (déposé) + pl
143 # Exemple:
144 # In [38]: m.ccxt.private_post_returnordertrades({"orderNumber": "XXXXXXXXXXXX"})
145 # Out[38]:
146 # [{'amount': '0.11882982',
147 # 'currencyPair': 'BTC_DASH',
148 # 'date': '2018-02-26 22:48:35',
149 # 'fee': '0.00150000',
150 # 'globalTradeID': 348891380,
151 # 'rate': '0.06003598',
152 # 'total': '0.00713406',
153 # 'tradeID': 9634443,
154 # 'type': 'sell'},
155 # {'amount': '0.00180000',
156 # 'currencyPair': 'BTC_DASH',
157 # 'date': '2018-02-26 22:48:30',
158 # 'fee': '0.00150000',
159 # 'globalTradeID': 348891375,
160 # 'rate': '0.06003598',
161 # 'total': '0.00010806',
162 # 'tradeID': 9634442,
163 # 'type': 'sell'}]
164 #
165 # In [51]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
166 # Out[51]:
167 # {'amount': '-0.12062982',
168 # 'basePrice': '0.05994587',
169 # 'lendingFees': '0.00000000',
170 # 'liquidationPrice': '0.15531479',
171 # 'pl': '0.00000122',
172 # 'total': '0.00723126',
173 # 'type': 'short'}
174 # In [52]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_BTS"})
175 # Out[52]:
176 # {'amount': '-332.97159188',
177 # 'basePrice': '0.00002171',
178 # 'lendingFees': '0.00000000',
179 # 'liquidationPrice': '0.00005543',
180 # 'pl': '0.00029548',
181 # 'total': '0.00723127',
182 # 'type': 'short'}
183 #
184 # In [53]: m.ccxt.privatePostReturnMarginAccountSummary()
185 # Out[53]:
186 # {'currentMargin': '1.04341991',
187 # 'lendingFees': '0.00000000',
188 # 'netValue': '0.01478093',
189 # 'pl': '0.00029666',
190 # 'totalBorrowedValue': '0.01416585',
191 # 'totalValue': '0.01448427'}
192
114 for currency, in_position in in_positions.items(): 193 for currency, in_position in in_positions.items():
115 all_balances[currency]["total"] += in_position 194 all_balances[currency]["total"] += in_position
195 all_balances[currency]["margin_in_position"] += in_position
116 all_balances[currency]["margin_total"] += in_position 196 all_balances[currency]["margin_total"] += in_position
117 all_balances[currency]["margin_borrowed"] += in_position 197
198 for currency, pl in pending_pl.items():
199 all_balances[currency]["margin_pending_gain"] += pl
118 200
119 return all_balances 201 return all_balances
120 202
@@ -228,8 +310,9 @@ class poloniexE(poloniex):
228 'lendingFees': '0.00000000', -> fees totaux 310 'lendingFees': '0.00000000', -> fees totaux
229 'netValue': '0.01008254', -> balance + plus-value 311 'netValue': '0.01008254', -> balance + plus-value
230 'pl': '0.00008254', -> plus value latente (somme des positions) 312 'pl': '0.00008254', -> plus value latente (somme des positions)
231 'totalBorrowedValue': '0.00673602', -> valeur en BTC empruntée 313 'totalBorrowedValue': '0.00673602', -> valeur empruntée convertie en BTC.
232 'totalValue': '0.01000000'} -> valeur totale en compte 314 (= sum(amount * ticker[lowerAsk]) pour amount dans marginposition)
315 'totalValue': '0.01000000'} -> balance (collateral déposé en margin)
233 """ 316 """
234 summary = self.privatePostReturnMarginAccountSummary() 317 summary = self.privatePostReturnMarginAccountSummary()
235 318
diff --git a/market.py b/market.py
index 0cb3e67..9122fc0 100644
--- a/market.py
+++ b/market.py
@@ -1,7 +1,8 @@
1from ccxt import ExchangeError 1from ccxt import ExchangeError, NotSupported
2import ccxt_wrapper as ccxt 2import ccxt_wrapper as ccxt
3import time 3import time
4from store import * 4from store import *
5from cachetools.func import ttl_cache
5 6
6class Market: 7class Market:
7 debug = False 8 debug = False
@@ -42,19 +43,17 @@ class Market:
42 needed_in_margin = {} 43 needed_in_margin = {}
43 moving_to_margin = {} 44 moving_to_margin = {}
44 45
45 for currency in self.balances.all: 46 for currency, balance in self.balances.all.items():
46 if self.balances.all[currency].margin_free != 0: 47 needed_in_margin[currency] = balance.margin_in_position - balance.margin_pending_gain
47 needed_in_margin[currency] = 0 48 for trade in self.trades.pending:
48 for trade in self.trades.all: 49 needed_in_margin.setdefault(trade.base_currency, 0)
49 if trade.value_to.currency not in needed_in_margin:
50 needed_in_margin[trade.value_to.currency] = 0
51 if trade.trade_type == "short": 50 if trade.trade_type == "short":
52 needed_in_margin[trade.value_to.currency] += abs(trade.value_to) 51 needed_in_margin[trade.base_currency] -= trade.delta
53 for currency, needed in needed_in_margin.items(): 52 for currency, needed in needed_in_margin.items():
54 current_balance = self.balances.all[currency].margin_free 53 current_balance = self.balances.all[currency].margin_available
55 moving_to_margin[currency] = (needed - current_balance) 54 moving_to_margin[currency] = (needed - current_balance)
56 delta = moving_to_margin[currency].value 55 delta = moving_to_margin[currency].value
57 if self.debug: 56 if self.debug and delta != 0:
58 self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) 57 self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency]))
59 continue 58 continue
60 if delta > 0: 59 if delta > 0:
@@ -65,14 +64,18 @@ class Market:
65 64
66 self.balances.fetch_balances() 65 self.balances.fetch_balances()
67 66
68 fees_cache = {} 67 @ttl_cache(ttl=3600)
69 def fetch_fees(self): 68 def fetch_fees(self):
70 if self.ccxt.__class__ not in self.fees_cache: 69 return self.ccxt.fetch_fees()
71 self.fees_cache[self.ccxt.__class__] = self.ccxt.fetch_fees() 70
72 return self.fees_cache[self.ccxt.__class__] 71 @ttl_cache(maxsize=20, ttl=5)
72 def get_tickers(self, refresh=False):
73 try:
74 return self.ccxt.fetch_tickers()
75 except NotSupported:
76 return None
73 77
74 ticker_cache = {} 78 @ttl_cache(maxsize=20, ttl=5)
75 ticker_cache_timestamp = time.time()
76 def get_ticker(self, c1, c2, refresh=False): 79 def get_ticker(self, c1, c2, refresh=False):
77 def invert(ticker): 80 def invert(ticker):
78 return { 81 return {
@@ -86,25 +89,25 @@ class Market:
86 "average": (ticker["bid"] + ticker["ask"] ) / 2, 89 "average": (ticker["bid"] + ticker["ask"] ) / 2,
87 }) 90 })
88 91
89 if time.time() - self.ticker_cache_timestamp > 5: 92 tickers = self.get_tickers()
90 self.ticker_cache = {} 93 if tickers is None:
91 self.ticker_cache_timestamp = time.time()
92 elif not refresh:
93 if (c1, c2, self.ccxt.__class__) in self.ticker_cache:
94 return self.ticker_cache[(c1, c2, self.ccxt.__class__)]
95 if (c2, c1, self.ccxt.__class__) in self.ticker_cache:
96 return invert(self.ticker_cache[(c2, c1, self.ccxt.__class__)])
97
98 try:
99 self.ticker_cache[(c1, c2, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c1, c2))
100 augment_ticker(self.ticker_cache[(c1, c2, self.ccxt.__class__)])
101 except ExchangeError:
102 try: 94 try:
103 self.ticker_cache[(c2, c1, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c2, c1)) 95 ticker = self.ccxt.fetch_ticker("{}/{}".format(c1, c2))
104 augment_ticker(self.ticker_cache[(c2, c1, self.ccxt.__class__)]) 96 augment_ticker(ticker)
105 except ExchangeError: 97 except ExchangeError:
106 self.ticker_cache[(c1, c2, self.ccxt.__class__)] = None 98 try:
107 return self.get_ticker(c1, c2) 99 ticker = invert(self.ccxt.fetch_ticker("{}/{}".format(c2, c1)))
100 except ExchangeError:
101 ticker = None
102 else:
103 if "{}/{}".format(c1, c2) in tickers:
104 ticker = tickers["{}/{}".format(c1, c2)]
105 augment_ticker(ticker)
106 elif "{}/{}".format(c2, c1) in tickers:
107 ticker = invert(tickers["{}/{}".format(c2, c1)])
108 else:
109 ticker = None
110 return ticker
108 111
109 def follow_orders(self, sleep=None): 112 def follow_orders(self, sleep=None):
110 if sleep is None: 113 if sleep is None:
diff --git a/portfolio.py b/portfolio.py
index eb3390e..b77850b 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4from json import JSONDecodeError 4from json import JSONDecodeError
5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError 5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder 6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached
7from retry import retry 7from retry import retry
8import requests 8import requests
9 9
@@ -226,8 +226,8 @@ class Amount:
226 226
227class Balance: 227class Balance:
228 base_keys = ["total", "exchange_total", "exchange_used", 228 base_keys = ["total", "exchange_total", "exchange_used",
229 "exchange_free", "margin_total", "margin_borrowed", 229 "exchange_free", "margin_total", "margin_in_position",
230 "margin_free"] 230 "margin_available", "margin_borrowed", "margin_pending_gain"]
231 231
232 def __init__(self, currency, hash_): 232 def __init__(self, currency, hash_):
233 self.currency = currency 233 self.currency = currency
@@ -240,8 +240,8 @@ class Balance:
240 base_currency = hash_["margin_borrowed_base_currency"] 240 base_currency = hash_["margin_borrowed_base_currency"]
241 for key in [ 241 for key in [
242 "margin_liquidation_price", 242 "margin_liquidation_price",
243 "margin_pending_gain",
244 "margin_lending_fees", 243 "margin_lending_fees",
244 "margin_pending_base_gain",
245 "margin_borrowed_base_price" 245 "margin_borrowed_base_price"
246 ]: 246 ]:
247 setattr(self, key, Amount(base_currency, hash_.get(key, 0))) 247 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
@@ -261,12 +261,12 @@ class Balance:
261 exchange = "" 261 exchange = ""
262 262
263 if self.margin_total > 0: 263 if self.margin_total > 0:
264 if self.margin_free != 0 and self.margin_borrowed != 0: 264 if self.margin_available != 0 and self.margin_in_position != 0:
265 margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total)) 265 margin = " Margin: [✔{} + ❌{} = {}]".format(str(self.margin_available), str(self.margin_in_position), str(self.margin_total))
266 elif self.margin_free != 0: 266 elif self.margin_available != 0:
267 margin = " Margin: [✔{}]".format(str(self.margin_free)) 267 margin = " Margin: [✔{}]".format(str(self.margin_available))
268 else: 268 else:
269 margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed)) 269 margin = " Margin: [{}]".format(str(self.margin_in_position))
270 elif self.margin_total < 0: 270 elif self.margin_total < 0:
271 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total), 271 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
272 str(self.margin_borrowed_base_price), 272 str(self.margin_borrowed_base_price),
@@ -290,6 +290,7 @@ class Trade:
290 self.value_to = value_to 290 self.value_to = value_to
291 self.orders = [] 291 self.orders = []
292 self.market = market 292 self.market = market
293 assert self.value_from.value * self.value_to.value >= 0
293 assert self.value_from.currency == self.value_to.currency 294 assert self.value_from.currency == self.value_to.currency
294 if self.value_from != 0: 295 if self.value_from != 0:
295 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency 296 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
@@ -298,6 +299,10 @@ class Trade:
298 self.base_currency = self.value_from.currency 299 self.base_currency = self.value_from.currency
299 300
300 @property 301 @property
302 def delta(self):
303 return self.value_to - self.value_from
304
305 @property
301 def action(self): 306 def action(self):
302 if self.value_from == self.value_to: 307 if self.value_from == self.value_to:
303 return None 308 return None
@@ -322,6 +327,10 @@ class Trade:
322 else: 327 else:
323 return "long" 328 return "long"
324 329
330 @property
331 def is_fullfiled(self):
332 return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
333
325 def filled_amount(self, in_base_currency=False): 334 def filled_amount(self, in_base_currency=False):
326 filled_amount = 0 335 filled_amount = 0
327 for order in self.orders: 336 for order in self.orders:
@@ -329,34 +338,36 @@ class Trade:
329 return filled_amount 338 return filled_amount
330 339
331 def update_order(self, order, tick): 340 def update_order(self, order, tick):
332 new_order = None 341 actions = {
333 if tick in [0, 1, 3, 4, 6]: 342 0: ["waiting", None],
343 1: ["waiting", None],
344 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
345 3: ["waiting", None],
346 4: ["waiting", None],
347 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
348 6: ["waiting", None],
349 7: ["market_fallback", "default"],
350 }
351
352 if tick in actions:
353 update, compute_value = actions[tick]
354 elif tick % 3 == 1:
355 update = "market_adjust"
356 compute_value = "default"
357 else:
334 update = "waiting" 358 update = "waiting"
335 compute_value = None 359 compute_value = None
336 elif tick == 2: 360
337 update = "adjusting" 361 if compute_value is not None:
338 compute_value = 'lambda x, y: (x[y] + x["average"]) / 2' 362 order.cancel()
339 new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2) 363 new_order = self.prepare_order(compute_value=compute_value)
340 elif tick ==5: 364 else:
341 update = "adjusting" 365 new_order = None
342 compute_value = 'lambda x, y: (x[y]*2 + x["average"]) / 3'
343 new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
344 elif tick >= 7:
345 if (tick - 7) % 3 == 0:
346 new_order = self.prepare_order(compute_value="default")
347 update = "market_adjust"
348 compute_value = "default"
349 else:
350 update = "waiting"
351 compute_value = None
352 if tick == 7:
353 update = "market_fallback"
354 366
355 self.market.report.log_order(order, tick, update=update, 367 self.market.report.log_order(order, tick, update=update,
356 compute_value=compute_value, new_order=new_order) 368 compute_value=compute_value, new_order=new_order)
357 369
358 if new_order is not None: 370 if new_order is not None:
359 order.cancel()
360 new_order.run() 371 new_order.run()
361 self.market.report.log_order(order, tick, new_order=new_order) 372 self.market.report.log_order(order, tick, new_order=new_order)
362 373
@@ -369,10 +380,9 @@ class Trade:
369 ticker = ticker["original"] 380 ticker = ticker["original"]
370 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 381 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
371 382
372 #TODO: store when the order is considered filled
373 # FIXME: Dust amount should be removed from there if they werent 383 # FIXME: Dust amount should be removed from there if they werent
374 # honored in other sales 384 # honored in other sales
375 delta_in_base = abs(self.value_from - self.value_to) 385 delta_in_base = abs(self.delta)
376 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 386 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
377 387
378 if not inverted: 388 if not inverted:
@@ -477,7 +487,6 @@ class Order:
477 self.trade = trade 487 self.trade = trade
478 self.close_if_possible = close_if_possible 488 self.close_if_possible = close_if_possible
479 self.id = None 489 self.id = None
480 self.fetch_cache_timestamp = None
481 self.tries = 0 490 self.tries = 0
482 491
483 def as_json(self): 492 def as_json(self):
@@ -578,21 +587,19 @@ class Order:
578 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: 587 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
579 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency) 588 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
580 589
581 def fetch(self, force=False): 590 def fetch(self):
582 if self.market.debug: 591 if self.market.debug:
583 self.market.report.log_debug_action("Fetching {}".format(self)) 592 self.market.report.log_debug_action("Fetching {}".format(self))
584 return 593 return
585 if (not force and self.fetch_cache_timestamp is not None 594 try:
586 and time.time() - self.fetch_cache_timestamp < 10): 595 result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency)
587 return 596 self.results.append(result)
588 self.fetch_cache_timestamp = time.time() 597 self.status = result["status"]
589 598 # Time at which the order started
590 result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency) 599 self.timestamp = result["datetime"]
591 self.results.append(result) 600 except OrderNotCached:
601 self.status = "closed_unknown"
592 602
593 self.status = result["status"]
594 # Time at which the order started
595 self.timestamp = result["datetime"]
596 self.fetch_mouvements() 603 self.fetch_mouvements()
597 604
598 # FIXME: consider open order with dust remaining as closed 605 # FIXME: consider open order with dust remaining as closed
@@ -601,8 +608,6 @@ class Order:
601 return self.remaining_amount() < Amount(self.amount.currency, D("0.001")) 608 return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
602 609
603 def remaining_amount(self): 610 def remaining_amount(self):
604 if self.status == "open":
605 self.fetch()
606 return self.amount - self.filled_amount() 611 return self.amount - self.filled_amount()
607 612
608 def filled_amount(self, in_base_currency=False): 613 def filled_amount(self, in_base_currency=False):
@@ -633,7 +638,7 @@ class Order:
633 self.status = "canceled" 638 self.status = "canceled"
634 return 639 return
635 self.market.ccxt.cancel_order(self.id) 640 self.market.ccxt.cancel_order(self.id)
636 self.fetch(force=True) 641 self.fetch()
637 642
638class Mouvement: 643class Mouvement:
639 def __init__(self, currency, base_currency, hash_): 644 def __init__(self, currency, base_currency, hash_):
diff --git a/requirements.txt b/requirements.txt
index 5a25dbf..1bc76ec 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@ requests>=2.18.4
4requests_mock==1.4.0 4requests_mock==1.4.0
5psycopg2==2.7.4 5psycopg2==2.7.4
6retry==0.9.2 6retry==0.9.2
7cachetools==2.0.1
diff --git a/store.py b/store.py
index bebd127..c581608 100644
--- a/store.py
+++ b/store.py
@@ -2,6 +2,7 @@ import portfolio
2import simplejson as json 2import simplejson as json
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4from datetime import date, datetime 4from datetime import date, datetime
5import inspect
5 6
6__all__ = ["BalanceStore", "ReportStore", "TradeStore"] 7__all__ = ["BalanceStore", "ReportStore", "TradeStore"]
7 8
@@ -55,6 +56,9 @@ class ReportStore:
55 compute_value, type): 56 compute_value, type):
56 values = {} 57 values = {}
57 rates = {} 58 rates = {}
59 if callable(compute_value):
60 compute_value = inspect.getsource(compute_value).strip()
61
58 for currency, amount in amounts.items(): 62 for currency, amount in amounts.items():
59 values[currency] = amount.as_json()["value"] 63 values[currency] = amount.as_json()["value"]
60 rates[currency] = amount.rate 64 rates[currency] = amount.rate
@@ -92,6 +96,8 @@ class ReportStore:
92 }) 96 })
93 97
94 def log_orders(self, orders, tick=None, only=None, compute_value=None): 98 def log_orders(self, orders, tick=None, only=None, compute_value=None):
99 if callable(compute_value):
100 compute_value = inspect.getsource(compute_value).strip()
95 self.print_log("[Orders]") 101 self.print_log("[Orders]")
96 self.market.trades.print_all_with_order(ind="\t") 102 self.market.trades.print_all_with_order(ind="\t")
97 self.add_log({ 103 self.add_log({
@@ -104,6 +110,8 @@ class ReportStore:
104 110
105 def log_order(self, order, tick, finished=False, update=None, 111 def log_order(self, order, tick, finished=False, update=None,
106 new_order=None, compute_value=None): 112 new_order=None, compute_value=None):
113 if callable(compute_value):
114 compute_value = inspect.getsource(compute_value).strip()
107 if finished: 115 if finished:
108 self.print_log("[Order] Finished {}".format(order)) 116 self.print_log("[Order] Finished {}".format(order))
109 elif update == "waiting": 117 elif update == "waiting":
@@ -201,8 +209,7 @@ class BalanceStore:
201 amounts[currency] = ptt * amount / sum_ratio 209 amounts[currency] = ptt * amount / sum_ratio
202 if trade_type == "short": 210 if trade_type == "short":
203 amounts[currency] = - amounts[currency] 211 amounts[currency] = - amounts[currency]
204 if currency not in self.all: 212 self.all.setdefault(currency, portfolio.Balance(currency, {}))
205 self.all[currency] = portfolio.Balance(currency, {})
206 self.market.report.log_dispatch(amount, amounts, liquidity, repartition) 213 self.market.report.log_dispatch(amount, amounts, liquidity, repartition)
207 return amounts 214 return amounts
208 215
@@ -214,6 +221,10 @@ class TradeStore:
214 self.market = market 221 self.market = market
215 self.all = [] 222 self.all = []
216 223
224 @property
225 def pending(self):
226 return list(filter(lambda t: not t.is_fullfiled, self.all))
227
217 def compute_trades(self, values_in_base, new_repartition, only=None): 228 def compute_trades(self, values_in_base, new_repartition, only=None):
218 computed_trades = [] 229 computed_trades = []
219 base_currency = sum(values_in_base.values()).currency 230 base_currency = sum(values_in_base.values()).currency
@@ -248,7 +259,7 @@ class TradeStore:
248 259
249 def prepare_orders(self, only=None, compute_value="default"): 260 def prepare_orders(self, only=None, compute_value="default"):
250 orders = [] 261 orders = []
251 for trade in self.all: 262 for trade in self.pending:
252 if only is None or trade.action == only: 263 if only is None or trade.action == only:
253 orders.append(trade.prepare_order(compute_value=compute_value)) 264 orders.append(trade.prepare_order(compute_value=compute_value))
254 self.market.report.log_orders(orders, only, compute_value) 265 self.market.report.log_orders(orders, only, compute_value)
diff --git a/test.py b/test.py
index 778fc14..141c9e0 100644
--- a/test.py
+++ b/test.py
@@ -35,10 +35,6 @@ class WebMockTestCase(unittest.TestCase):
35 mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), 35 mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}),
36 mock.patch.multiple(portfolio.Computation, 36 mock.patch.multiple(portfolio.Computation,
37 computations=portfolio.Computation.computations), 37 computations=portfolio.Computation.computations),
38 mock.patch.multiple(market.Market,
39 fees_cache={},
40 ticker_cache={},
41 ticker_cache_timestamp=self.time.time()),
42 ] 38 ]
43 for patcher in self.patchers: 39 for patcher in self.patchers:
44 patcher.start() 40 patcher.start()
@@ -472,8 +468,9 @@ class BalanceTest(WebMockTestCase):
472 "exchange_free": "0.35", 468 "exchange_free": "0.35",
473 "exchange_used": "0.30", 469 "exchange_used": "0.30",
474 "margin_total": "-10", 470 "margin_total": "-10",
475 "margin_borrowed": "-10", 471 "margin_borrowed": "10",
476 "margin_free": "0", 472 "margin_available": "0",
473 "margin_in_position": "0",
477 "margin_position_type": "short", 474 "margin_position_type": "short",
478 "margin_borrowed_base_currency": "USDT", 475 "margin_borrowed_base_currency": "USDT",
479 "margin_liquidation_price": "1.20", 476 "margin_liquidation_price": "1.20",
@@ -489,11 +486,11 @@ class BalanceTest(WebMockTestCase):
489 self.assertEqual("BTC", balance.exchange_total.currency) 486 self.assertEqual("BTC", balance.exchange_total.currency)
490 487
491 self.assertEqual(portfolio.D("-10"), balance.margin_total.value) 488 self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
492 self.assertEqual(portfolio.D("-10"), balance.margin_borrowed.value) 489 self.assertEqual(portfolio.D("10"), balance.margin_borrowed.value)
493 self.assertEqual(portfolio.D("0"), balance.margin_free.value) 490 self.assertEqual(portfolio.D("0"), balance.margin_available.value)
494 self.assertEqual("BTC", balance.margin_total.currency) 491 self.assertEqual("BTC", balance.margin_total.currency)
495 self.assertEqual("BTC", balance.margin_borrowed.currency) 492 self.assertEqual("BTC", balance.margin_borrowed.currency)
496 self.assertEqual("BTC", balance.margin_free.currency) 493 self.assertEqual("BTC", balance.margin_available.currency)
497 494
498 self.assertEqual("BTC", balance.currency) 495 self.assertEqual("BTC", balance.currency)
499 496
@@ -511,10 +508,10 @@ class BalanceTest(WebMockTestCase):
511 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance)) 508 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
512 509
513 balance = portfolio.Balance("BTX", { "margin_total": 3, 510 balance = portfolio.Balance("BTX", { "margin_total": 3,
514 "margin_borrowed": 1, "margin_free": 2 }) 511 "margin_in_position": 1, "margin_available": 2 })
515 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance)) 512 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
516 513
517 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_free": 2 }) 514 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_available": 2 })
518 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance)) 515 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
519 516
520 balance = portfolio.Balance("BTX", { "margin_total": -3, 517 balance = portfolio.Balance("BTX", { "margin_total": -3,
@@ -524,8 +521,8 @@ class BalanceTest(WebMockTestCase):
524 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance)) 521 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
525 522
526 balance = portfolio.Balance("BTX", { "margin_total": 1, 523 balance = portfolio.Balance("BTX", { "margin_total": 1,
527 "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2}) 524 "margin_in_position": 1, "exchange_free": 2, "exchange_total": 2})
528 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance)) 525 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [❌1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
529 526
530 def test_as_json(self): 527 def test_as_json(self):
531 balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 }) 528 balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })
@@ -536,7 +533,7 @@ class BalanceTest(WebMockTestCase):
536 self.assertEqual(D(2), as_json["exchange_free"]) 533 self.assertEqual(D(2), as_json["exchange_free"])
537 self.assertEqual(D(0), as_json["exchange_used"]) 534 self.assertEqual(D(0), as_json["exchange_used"])
538 self.assertEqual(D(0), as_json["margin_total"]) 535 self.assertEqual(D(0), as_json["margin_total"])
539 self.assertEqual(D(0), as_json["margin_free"]) 536 self.assertEqual(D(0), as_json["margin_available"])
540 self.assertEqual(D(0), as_json["margin_borrowed"]) 537 self.assertEqual(D(0), as_json["margin_borrowed"])
541 538
542@unittest.skipUnless("unit" in limits, "Unit skipped") 539@unittest.skipUnless("unit" in limits, "Unit skipped")
@@ -583,77 +580,69 @@ class MarketTest(WebMockTestCase):
583 m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True) 580 m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True)
584 self.assertEqual(True, m.debug) 581 self.assertEqual(True, m.debug)
585 582
586 def test_get_ticker(self): 583 def test_get_tickers(self):
587 m = market.Market(self.ccxt) 584 self.ccxt.fetch_tickers.side_effect = [
588 self.ccxt.fetch_ticker.side_effect = [ 585 "tickers",
589 { "bid": 1, "ask": 3 }, 586 market.NotSupported
590 market.ExchangeError("foo"),
591 { "bid": 10, "ask": 40 },
592 market.ExchangeError("foo"),
593 market.ExchangeError("foo"),
594 ] 587 ]
595 588
596 ticker = m.get_ticker("ETH", "ETC") 589 m = market.Market(self.ccxt)
597 self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") 590 self.assertEqual("tickers", m.get_tickers())
598 self.assertEqual(1, ticker["bid"]) 591 self.assertEqual("tickers", m.get_tickers())
599 self.assertEqual(3, ticker["ask"]) 592 self.ccxt.fetch_tickers.assert_called_once()
600 self.assertEqual(2, ticker["average"])
601 self.assertFalse(ticker["inverted"])
602
603 ticker = m.get_ticker("ETH", "XVG")
604 self.assertEqual(0.0625, ticker["average"])
605 self.assertTrue(ticker["inverted"])
606 self.assertIn("original", ticker)
607 self.assertEqual(10, ticker["original"]["bid"])
608
609 ticker = m.get_ticker("XVG", "XMR")
610 self.assertIsNone(ticker)
611
612 self.ccxt.fetch_ticker.assert_has_calls([
613 mock.call("ETH/ETC"),
614 mock.call("ETH/XVG"),
615 mock.call("XVG/ETH"),
616 mock.call("XVG/XMR"),
617 mock.call("XMR/XVG"),
618 ])
619 593
620 self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) 594 self.assertIsNone(m.get_tickers(refresh=self.time.time()))
621 m1b = market.Market(self.ccxt) 595
622 m1b.get_ticker("ETH", "ETC") 596 def test_get_ticker(self):
623 self.ccxt.fetch_ticker.assert_not_called() 597 with self.subTest(get_tickers=True):
624 598 self.ccxt.fetch_tickers.return_value = {
625 self.ccxt = mock.Mock(spec=market.ccxt.poloniex) 599 "ETH/ETC": { "bid": 1, "ask": 3 },
626 m2 = market.Market(self.ccxt) 600 "XVG/ETH": { "bid": 10, "ask": 40 },
627 self.ccxt.fetch_ticker.side_effect = [ 601 }
628 { "bid": 1, "ask": 3 }, 602 m = market.Market(self.ccxt)
629 { "bid": 1.2, "ask": 3.5 }, 603
630 ] 604 ticker = m.get_ticker("ETH", "ETC")
631 ticker1 = m2.get_ticker("ETH", "ETC") 605 self.assertEqual(1, ticker["bid"])
632 ticker2 = m2.get_ticker("ETH", "ETC") 606 self.assertEqual(3, ticker["ask"])
633 ticker3 = m2.get_ticker("ETC", "ETH") 607 self.assertEqual(2, ticker["average"])
634 self.ccxt.fetch_ticker.assert_called_once_with("ETH/ETC") 608 self.assertFalse(ticker["inverted"])
635 self.assertEqual(1, ticker1["bid"]) 609
636 self.assertDictEqual(ticker1, ticker2) 610 ticker = m.get_ticker("ETH", "XVG")
637 self.assertDictEqual(ticker1, ticker3["original"]) 611 self.assertEqual(0.0625, ticker["average"])
638 612 self.assertTrue(ticker["inverted"])
639 ticker4 = m2.get_ticker("ETH", "ETC", refresh=True) 613 self.assertIn("original", ticker)
640 ticker5 = m2.get_ticker("ETH", "ETC") 614 self.assertEqual(10, ticker["original"]["bid"])
641 self.assertEqual(1.2, ticker4["bid"]) 615
642 self.assertDictEqual(ticker4, ticker5) 616 ticker = m.get_ticker("XVG", "XMR")
643 617 self.assertIsNone(ticker)
644 self.ccxt = mock.Mock(spec=market.ccxt.binance) 618
645 m3 = market.Market(self.ccxt) 619 with self.subTest(get_tickers=False):
646 self.ccxt.fetch_ticker.side_effect = [ 620 self.ccxt.fetch_tickers.return_value = None
647 { "bid": 1, "ask": 3 }, 621 self.ccxt.fetch_ticker.side_effect = [
648 { "bid": 1.2, "ask": 3.5 }, 622 { "bid": 1, "ask": 3 },
649 ] 623 market.ExchangeError("foo"),
650 ticker6 = m3.get_ticker("ETH", "ETC") 624 { "bid": 10, "ask": 40 },
651 m3.ticker_cache_timestamp -= 4 625 market.ExchangeError("foo"),
652 ticker7 = m3.get_ticker("ETH", "ETC") 626 market.ExchangeError("foo"),
653 m3.ticker_cache_timestamp -= 2 627 ]
654 ticker8 = m3.get_ticker("ETH", "ETC") 628
655 self.assertDictEqual(ticker6, ticker7) 629 m = market.Market(self.ccxt)
656 self.assertEqual(1.2, ticker8["bid"]) 630
631 ticker = m.get_ticker("ETH", "ETC")
632 self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
633 self.assertEqual(1, ticker["bid"])
634 self.assertEqual(3, ticker["ask"])
635 self.assertEqual(2, ticker["average"])
636 self.assertFalse(ticker["inverted"])
637
638 ticker = m.get_ticker("ETH", "XVG")
639 self.assertEqual(0.0625, ticker["average"])
640 self.assertTrue(ticker["inverted"])
641 self.assertIn("original", ticker)
642 self.assertEqual(10, ticker["original"]["bid"])
643
644 ticker = m.get_ticker("XVG", "XMR")
645 self.assertIsNone(ticker)
657 646
658 def test_fetch_fees(self): 647 def test_fetch_fees(self):
659 m = market.Market(self.ccxt) 648 m = market.Market(self.ccxt)
@@ -906,9 +895,9 @@ class MarketTest(WebMockTestCase):
906 trade3 = portfolio.Trade(value_from, value_to, "XVG", m) 895 trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
907 896
908 m.trades.all = [trade1, trade2, trade3] 897 m.trades.all = [trade1, trade2, trade3]
909 balance1 = portfolio.Balance("BTC", { "margin_free": "0" }) 898 balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
910 balance2 = portfolio.Balance("USDT", { "margin_free": "100" }) 899 balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
911 balance3 = portfolio.Balance("ETC", { "margin_free": "10" }) 900 balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
912 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} 901 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
913 902
914 m.move_balances() 903 m.move_balances()
@@ -921,8 +910,8 @@ class MarketTest(WebMockTestCase):
921 self.assertEqual(3, m.report.log_debug_action.call_count) 910 self.assertEqual(3, m.report.log_debug_action.call_count)
922 else: 911 else:
923 self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") 912 self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin")
924 self.ccxt.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange") 913 self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
925 self.ccxt.transfer_balance.assert_any_call("ETC", 10, "margin", "exchange") 914 self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
926 915
927@unittest.skipUnless("unit" in limits, "Unit skipped") 916@unittest.skipUnless("unit" in limits, "Unit skipped")
928class TradeStoreTest(WebMockTestCase): 917class TradeStoreTest(WebMockTestCase):
@@ -1001,16 +990,24 @@ class TradeStoreTest(WebMockTestCase):
1001 990
1002 trade_mock1 = mock.Mock() 991 trade_mock1 = mock.Mock()
1003 trade_mock2 = mock.Mock() 992 trade_mock2 = mock.Mock()
993 trade_mock3 = mock.Mock()
1004 994
1005 trade_mock1.prepare_order.return_value = 1 995 trade_mock1.prepare_order.return_value = 1
1006 trade_mock2.prepare_order.return_value = 2 996 trade_mock2.prepare_order.return_value = 2
997 trade_mock3.prepare_order.return_value = 3
998
999 trade_mock1.is_fullfiled = False
1000 trade_mock2.is_fullfiled = False
1001 trade_mock3.is_fullfiled = True
1007 1002
1008 trade_store.all.append(trade_mock1) 1003 trade_store.all.append(trade_mock1)
1009 trade_store.all.append(trade_mock2) 1004 trade_store.all.append(trade_mock2)
1005 trade_store.all.append(trade_mock3)
1010 1006
1011 trade_store.prepare_orders() 1007 trade_store.prepare_orders()
1012 trade_mock1.prepare_order.assert_called_with(compute_value="default") 1008 trade_mock1.prepare_order.assert_called_with(compute_value="default")
1013 trade_mock2.prepare_order.assert_called_with(compute_value="default") 1009 trade_mock2.prepare_order.assert_called_with(compute_value="default")
1010 trade_mock3.prepare_order.assert_not_called()
1014 self.m.report.log_orders.assert_called_once_with([1, 2], None, "default") 1011 self.m.report.log_orders.assert_called_once_with([1, 2], None, "default")
1015 1012
1016 self.m.report.log_orders.reset_mock() 1013 self.m.report.log_orders.reset_mock()
@@ -1108,6 +1105,21 @@ class TradeStoreTest(WebMockTestCase):
1108 order_mock2.get_status.assert_called() 1105 order_mock2.get_status.assert_called()
1109 order_mock3.get_status.assert_called() 1106 order_mock3.get_status.assert_called()
1110 1107
1108 def test_pending(self):
1109 trade_mock1 = mock.Mock()
1110 trade_mock1.is_fullfiled = False
1111 trade_mock2 = mock.Mock()
1112 trade_mock2.is_fullfiled = False
1113 trade_mock3 = mock.Mock()
1114 trade_mock3.is_fullfiled = True
1115
1116 trade_store = market.TradeStore(self.m)
1117
1118 trade_store.all.append(trade_mock1)
1119 trade_store.all.append(trade_mock2)
1120 trade_store.all.append(trade_mock3)
1121
1122 self.assertEqual([trade_mock1, trade_mock2], trade_store.pending)
1111 1123
1112@unittest.skipUnless("unit" in limits, "Unit skipped") 1124@unittest.skipUnless("unit" in limits, "Unit skipped")
1113class BalanceStoreTest(WebMockTestCase): 1125class BalanceStoreTest(WebMockTestCase):
@@ -1301,13 +1313,16 @@ class TradeTest(WebMockTestCase):
1301 self.assertEqual(self.m, trade.market) 1313 self.assertEqual(self.m, trade.market)
1302 1314
1303 with self.assertRaises(AssertionError): 1315 with self.assertRaises(AssertionError):
1304 portfolio.Trade(value_from, value_to, "ETC", self.m) 1316 portfolio.Trade(value_from, -value_to, "ETH", self.m)
1305 with self.assertRaises(AssertionError): 1317 with self.assertRaises(AssertionError):
1306 value_from.linked_to = None 1318 portfolio.Trade(value_from, value_to, "ETC", self.m)
1307 portfolio.Trade(value_from, value_to, "ETH", self.m)
1308 with self.assertRaises(AssertionError): 1319 with self.assertRaises(AssertionError):
1309 value_from.currency = "ETH" 1320 value_from.currency = "ETH"
1310 portfolio.Trade(value_from, value_to, "ETH", self.m) 1321 portfolio.Trade(value_from, value_to, "ETH", self.m)
1322 value_from.currency = "BTC"
1323 with self.assertRaises(AssertionError):
1324 value_from2 = portfolio.Amount("BTC", "1.0")
1325 portfolio.Trade(value_from2, value_to, "ETH", self.m)
1311 1326
1312 value_from = portfolio.Amount("BTC", 0) 1327 value_from = portfolio.Amount("BTC", 0)
1313 trade = portfolio.Trade(value_from, value_to, "ETH", self.m) 1328 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
@@ -1374,6 +1389,28 @@ class TradeTest(WebMockTestCase):
1374 1389
1375 self.assertEqual("short", trade.trade_type) 1390 self.assertEqual("short", trade.trade_type)
1376 1391
1392 def test_is_fullfiled(self):
1393 value_from = portfolio.Amount("BTC", "0.5")
1394 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1395 value_to = portfolio.Amount("BTC", "1.0")
1396 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1397
1398 order1 = mock.Mock()
1399 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
1400
1401 order2 = mock.Mock()
1402 order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
1403 trade.orders.append(order1)
1404 trade.orders.append(order2)
1405
1406 self.assertFalse(trade.is_fullfiled)
1407
1408 order3 = mock.Mock()
1409 order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
1410 trade.orders.append(order3)
1411
1412 self.assertTrue(trade.is_fullfiled)
1413
1377 def test_filled_amount(self): 1414 def test_filled_amount(self):
1378 value_from = portfolio.Amount("BTC", "0.5") 1415 value_from = portfolio.Amount("BTC", "0.5")
1379 value_from.linked_to = portfolio.Amount("ETH", "10.0") 1416 value_from.linked_to = portfolio.Amount("ETH", "10.0")
@@ -1588,7 +1625,7 @@ class TradeTest(WebMockTestCase):
1588 self.assertEqual(2, self.m.report.log_order.call_count) 1625 self.assertEqual(2, self.m.report.log_order.call_count)
1589 calls = [ 1626 calls = [
1590 mock.call(order_mock, 2, update="adjusting", 1627 mock.call(order_mock, 2, update="adjusting",
1591 compute_value='lambda x, y: (x[y] + x["average"]) / 2', 1628 compute_value=mock.ANY,
1592 new_order=new_order_mock), 1629 new_order=new_order_mock),
1593 mock.call(order_mock, 2, new_order=new_order_mock), 1630 mock.call(order_mock, 2, new_order=new_order_mock),
1594 ] 1631 ]
@@ -1607,7 +1644,7 @@ class TradeTest(WebMockTestCase):
1607 self.m.report.log_order.assert_called() 1644 self.m.report.log_order.assert_called()
1608 calls = [ 1645 calls = [
1609 mock.call(order_mock, 5, update="adjusting", 1646 mock.call(order_mock, 5, update="adjusting",
1610 compute_value='lambda x, y: (x[y]*2 + x["average"]) / 3', 1647 compute_value=mock.ANY,
1611 new_order=new_order_mock), 1648 new_order=new_order_mock),
1612 mock.call(order_mock, 5, new_order=new_order_mock), 1649 mock.call(order_mock, 5, new_order=new_order_mock),
1613 ] 1650 ]
@@ -1831,7 +1868,7 @@ class OrderTest(WebMockTestCase):
1831 1868
1832 order.cancel() 1869 order.cancel()
1833 self.m.ccxt.cancel_order.assert_called_with(42) 1870 self.m.ccxt.cancel_order.assert_called_with(42)
1834 fetch.assert_called_once_with(force=True) 1871 fetch.assert_called_once_with()
1835 self.m.report.log_debug_action.assert_not_called() 1872 self.m.report.log_debug_action.assert_not_called()
1836 1873
1837 def test_dust_amount_remaining(self): 1874 def test_dust_amount_remaining(self):
@@ -1850,11 +1887,9 @@ class OrderTest(WebMockTestCase):
1850 D("0.1"), "BTC", "long", self.m, "trade") 1887 D("0.1"), "BTC", "long", self.m, "trade")
1851 1888
1852 self.assertEqual(9, order.remaining_amount().value) 1889 self.assertEqual(9, order.remaining_amount().value)
1853 order.fetch.assert_not_called()
1854 1890
1855 order.status = "open" 1891 order.status = "open"
1856 self.assertEqual(9, order.remaining_amount().value) 1892 self.assertEqual(9, order.remaining_amount().value)
1857 fetch.assert_called_once()
1858 1893
1859 @mock.patch.object(portfolio.Order, "fetch") 1894 @mock.patch.object(portfolio.Order, "fetch")
1860 def test_filled_amount(self, fetch): 1895 def test_filled_amount(self, fetch):
@@ -1957,61 +1992,38 @@ class OrderTest(WebMockTestCase):
1957 1992
1958 @mock.patch.object(portfolio.Order, "fetch_mouvements") 1993 @mock.patch.object(portfolio.Order, "fetch_mouvements")
1959 def test_fetch(self, fetch_mouvements): 1994 def test_fetch(self, fetch_mouvements):
1960 time = self.time.time() 1995 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1961 with mock.patch.object(portfolio.time, "time") as time_mock: 1996 D("0.1"), "BTC", "long", self.m, "trade")
1962 order = portfolio.Order("buy", portfolio.Amount("ETH", 10), 1997 order.id = 45
1963 D("0.1"), "BTC", "long", self.m, "trade") 1998 with self.subTest(debug=True):
1964 order.id = 45 1999 self.m.debug = True
1965 with self.subTest(debug=True): 2000 order.fetch()
1966 self.m.debug = True 2001 self.m.report.log_debug_action.assert_called_once()
1967 order.fetch() 2002 self.m.report.log_debug_action.reset_mock()
1968 time_mock.assert_not_called() 2003 self.m.ccxt.fetch_order.assert_not_called()
1969 self.m.report.log_debug_action.assert_called_once() 2004 fetch_mouvements.assert_not_called()
1970 self.m.report.log_debug_action.reset_mock()
1971 order.fetch(force=True)
1972 time_mock.assert_not_called()
1973 self.m.ccxt.fetch_order.assert_not_called()
1974 fetch_mouvements.assert_not_called()
1975 self.m.report.log_debug_action.assert_called_once()
1976 self.m.report.log_debug_action.reset_mock()
1977 self.assertIsNone(order.fetch_cache_timestamp)
1978
1979 with self.subTest(debug=False):
1980 self.m.debug = False
1981 time_mock.return_value = time
1982 self.m.ccxt.fetch_order.return_value = {
1983 "status": "foo",
1984 "datetime": "timestamp"
1985 }
1986 order.fetch()
1987
1988 self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
1989 fetch_mouvements.assert_called_once()
1990 self.assertEqual("foo", order.status)
1991 self.assertEqual("timestamp", order.timestamp)
1992 self.assertEqual(time, order.fetch_cache_timestamp)
1993 self.assertEqual(1, len(order.results))
1994
1995 self.m.ccxt.fetch_order.reset_mock()
1996 fetch_mouvements.reset_mock()
1997
1998 time_mock.return_value = time + 8
1999 order.fetch()
2000 self.m.ccxt.fetch_order.assert_not_called()
2001 fetch_mouvements.assert_not_called()
2002 2005
2003 order.fetch(force=True) 2006 with self.subTest(debug=False):
2004 self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH") 2007 self.m.debug = False
2005 fetch_mouvements.assert_called_once() 2008 self.m.ccxt.fetch_order.return_value = {
2009 "status": "foo",
2010 "datetime": "timestamp"
2011 }
2012 order.fetch()
2006 2013
2007 self.m.ccxt.fetch_order.reset_mock() 2014 self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
2008 fetch_mouvements.reset_mock() 2015 fetch_mouvements.assert_called_once()
2016 self.assertEqual("foo", order.status)
2017 self.assertEqual("timestamp", order.timestamp)
2018 self.assertEqual(1, len(order.results))
2019 self.m.report.log_debug_action.assert_not_called()
2009 2020
2010 time_mock.return_value = time + 19 2021 with self.subTest(missing_order=True):
2022 self.m.ccxt.fetch_order.side_effect = [
2023 portfolio.OrderNotCached,
2024 ]
2011 order.fetch() 2025 order.fetch()
2012 self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH") 2026 self.assertEqual("closed_unknown", order.status)
2013 fetch_mouvements.assert_called_once()
2014 self.m.report.log_debug_action.assert_not_called()
2015 2027
2016 @mock.patch.object(portfolio.Order, "fetch") 2028 @mock.patch.object(portfolio.Order, "fetch")
2017 @mock.patch.object(portfolio.Order, "mark_finished_order") 2029 @mock.patch.object(portfolio.Order, "mark_finished_order")
@@ -2315,6 +2327,25 @@ class ReportStoreTest(WebMockTestCase):
2315 'total': D('10.3') 2327 'total': D('10.3')
2316 }) 2328 })
2317 2329
2330 add_log.reset_mock()
2331 compute_value = lambda x: x["bid"]
2332 report_store.log_tickers(amounts, "BTC", compute_value, "total")
2333 add_log.assert_called_once_with({
2334 'type': 'tickers',
2335 'compute_value': 'compute_value = lambda x: x["bid"]',
2336 'balance_type': 'total',
2337 'currency': 'BTC',
2338 'balances': {
2339 'BTC': D('10'),
2340 'ETH': D('0.3')
2341 },
2342 'rates': {
2343 'BTC': None,
2344 'ETH': D('0.1')
2345 },
2346 'total': D('10.3')
2347 })
2348
2318 @mock.patch.object(market.ReportStore, "print_log") 2349 @mock.patch.object(market.ReportStore, "print_log")
2319 @mock.patch.object(market.ReportStore, "add_log") 2350 @mock.patch.object(market.ReportStore, "add_log")
2320 def test_log_dispatch(self, add_log, print_log): 2351 def test_log_dispatch(self, add_log, print_log):
@@ -2393,6 +2424,20 @@ class ReportStoreTest(WebMockTestCase):
2393 'orders': ['order1', 'order2'] 2424 'orders': ['order1', 'order2']
2394 }) 2425 })
2395 2426
2427 add_log.reset_mock()
2428 def compute_value(x, y):
2429 return x[y]
2430 report_store.log_orders(orders, tick="tick",
2431 only="only", compute_value=compute_value)
2432 add_log.assert_called_with({
2433 'type': 'orders',
2434 'only': 'only',
2435 'compute_value': 'def compute_value(x, y):\n return x[y]',
2436 'tick': 'tick',
2437 'orders': ['order1', 'order2']
2438 })
2439
2440
2396 @mock.patch.object(market.ReportStore, "print_log") 2441 @mock.patch.object(market.ReportStore, "print_log")
2397 @mock.patch.object(market.ReportStore, "add_log") 2442 @mock.patch.object(market.ReportStore, "add_log")
2398 def test_log_order(self, add_log, print_log): 2443 def test_log_order(self, add_log, print_log):
@@ -2436,16 +2481,17 @@ class ReportStoreTest(WebMockTestCase):
2436 add_log.reset_mock() 2481 add_log.reset_mock()
2437 print_log.reset_mock() 2482 print_log.reset_mock()
2438 with self.subTest(update="adjusting"): 2483 with self.subTest(update="adjusting"):
2484 compute_value = lambda x: (x["bid"] + x["ask"]*2)/3
2439 report_store.log_order(order_mock, 3, 2485 report_store.log_order(order_mock, 3,
2440 update="adjusting", new_order=new_order_mock, 2486 update="adjusting", new_order=new_order_mock,
2441 compute_value="default") 2487 compute_value=compute_value)
2442 print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") 2488 print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock")
2443 add_log.assert_called_once_with({ 2489 add_log.assert_called_once_with({
2444 'type': 'order', 2490 'type': 'order',
2445 'tick': 3, 2491 'tick': 3,
2446 'update': 'adjusting', 2492 'update': 'adjusting',
2447 'order': 'order', 2493 'order': 'order',
2448 'compute_value': "default", 2494 'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3',
2449 'new_order': 'new_order' 2495 'new_order': 'new_order'
2450 }) 2496 })
2451 2497