diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-01 13:14:41 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-01 13:14:41 +0100 |
commit | aca4d4372553110ab5d76740ff536de83d5617b2 (patch) | |
tree | a9bfdca4226daf422273da97a9e139721469c9f1 | |
parent | 2033e7fef780298be2ec15455a0ec1d26515de55 (diff) | |
download | Trader-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.py | 109 | ||||
-rw-r--r-- | market.py | 69 | ||||
-rw-r--r-- | portfolio.py | 99 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | store.py | 17 | ||||
-rw-r--r-- | test.py | 344 |
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 | ||
@@ -1,7 +1,8 @@ | |||
1 | from ccxt import ExchangeError | 1 | from ccxt import ExchangeError, NotSupported |
2 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
3 | import time | 3 | import time |
4 | from store import * | 4 | from store import * |
5 | from cachetools.func import ttl_cache | ||
5 | 6 | ||
6 | class Market: | 7 | class 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 | |||
3 | from decimal import Decimal as D, ROUND_DOWN | 3 | from decimal import Decimal as D, ROUND_DOWN |
4 | from json import JSONDecodeError | 4 | from json import JSONDecodeError |
5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | 5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError |
6 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder | 6 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached |
7 | from retry import retry | 7 | from retry import retry |
8 | import requests | 8 | import requests |
9 | 9 | ||
@@ -226,8 +226,8 @@ class Amount: | |||
226 | 226 | ||
227 | class Balance: | 227 | class 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 | ||
638 | class Mouvement: | 643 | class 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 | |||
4 | requests_mock==1.4.0 | 4 | requests_mock==1.4.0 |
5 | psycopg2==2.7.4 | 5 | psycopg2==2.7.4 |
6 | retry==0.9.2 | 6 | retry==0.9.2 |
7 | cachetools==2.0.1 | ||
@@ -2,6 +2,7 @@ import portfolio | |||
2 | import simplejson as json | 2 | import simplejson as json |
3 | from decimal import Decimal as D, ROUND_DOWN | 3 | from decimal import Decimal as D, ROUND_DOWN |
4 | from datetime import date, datetime | 4 | from datetime import date, datetime |
5 | import 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) |
@@ -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") |
928 | class TradeStoreTest(WebMockTestCase): | 917 | class 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") |
1113 | class BalanceStoreTest(WebMockTestCase): | 1125 | class 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 | ||