aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-04 19:12:50 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-04 19:15:03 +0100
commit006a20846236ad365ec814f848f5fbf7e3dc7d3c (patch)
treefd28f7f278d7d5156f0b7a939b35507c2a6dc4ba
parent350ed24de673dc125be9e2fdecb0f1abc7835b41 (diff)
downloadTrader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.tar.gz
Trader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.tar.zst
Trader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.zip
WIP: handle more balance information
-rw-r--r--market.py161
-rw-r--r--portfolio.py243
-rw-r--r--test.py4
3 files changed, 279 insertions, 129 deletions
diff --git a/market.py b/market.py
index 1e1e083..979fbbf 100644
--- a/market.py
+++ b/market.py
@@ -26,7 +26,18 @@ def poloniex_fetch_balance(self, params={}):
26 return self.parse_balance(result) 26 return self.parse_balance(result)
27ccxt.poloniex.fetch_balance = poloniex_fetch_balance 27ccxt.poloniex.fetch_balance = poloniex_fetch_balance
28 28
29def poloniex_fetch_margin_balances(self): 29def poloniex_fetch_margin_balance(self):
30 """
31 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
32 See DASH/BTC positions
33 {'amount': '-0.10000000', -> DASH empruntés
34 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%))
35 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur
36 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin)
37 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu)
38 'total': '0.00681856', -> valeur totale empruntée en BTC
39 'type': 'short'}
40 """
30 positions = self.privatePostGetMarginPosition({"currencyPair": "all"}) 41 positions = self.privatePostGetMarginPosition({"currencyPair": "all"})
31 parsed = {} 42 parsed = {}
32 for symbol, position in positions.items(): 43 for symbol, position in positions.items():
@@ -41,23 +52,10 @@ def poloniex_fetch_margin_balances(self):
41 "liquidationPrice": decimal.Decimal(position["liquidationPrice"]), 52 "liquidationPrice": decimal.Decimal(position["liquidationPrice"]),
42 "type": position["type"], 53 "type": position["type"],
43 "total": decimal.Decimal(position["total"]), 54 "total": decimal.Decimal(position["total"]),
44 "base_currency": base_currency, 55 "baseCurrency": base_currency,
45 } 56 }
46 return parsed 57 return parsed
47ccxt.poloniex.fetch_margin_balances = poloniex_fetch_margin_balances 58ccxt.poloniex.fetch_margin_balance = poloniex_fetch_margin_balance
48
49def poloniex_fetch_balance_with_margin(self, params={}):
50 exchange_balance = self.fetch_balance(params=params)
51 margin_balances = self.fetch_margin_balances()
52
53 for currency, balance in margin_balances.items():
54 assert exchange_balance[currency]["total"] == 0
55 assert balance["type"] == "short"
56 exchange_balance[currency]["total"] = balance["amount"]
57 exchange_balance[currency]["marginPosition"] = balance
58 return exchange_balance
59ccxt.poloniex.fetch_balance_with_margin = poloniex_fetch_balance_with_margin
60
61 59
62def poloniex_fetch_balance_per_type(self): 60def poloniex_fetch_balance_per_type(self):
63 balances = self.privatePostReturnAvailableAccountBalances() 61 balances = self.privatePostReturnAvailableAccountBalances()
@@ -72,6 +70,49 @@ def poloniex_fetch_balance_per_type(self):
72 return result 70 return result
73ccxt.poloniex.fetch_balance_per_type = poloniex_fetch_balance_per_type 71ccxt.poloniex.fetch_balance_per_type = poloniex_fetch_balance_per_type
74 72
73def poloniex_fetch_all_balances(self):
74 exchange_balances = self.fetch_balance()
75 margin_balances = self.fetch_margin_balance()
76 balances_per_type = self.fetch_balance_per_type()
77
78 all_balances = {}
79 in_positions = {}
80
81 for currency, exchange_balance in exchange_balances.items():
82 if currency in ["info", "free", "used", "total"]:
83 continue
84
85 margin_balance = margin_balances.get(currency, {})
86 balance_per_type = balances_per_type.get(currency, {})
87
88 all_balances[currency] = {
89 "total": exchange_balance["total"] + margin_balance.get("amount", 0),
90 "exchange_used": exchange_balance["used"],
91 "exchange_total": exchange_balance["total"] - balance_per_type.get("margin", 0),
92 "exchange_free": exchange_balance["free"] - balance_per_type.get("margin", 0),
93 "margin_free": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
94 "margin_borrowed": 0,
95 "margin_total": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
96 "margin_lending_fees": margin_balance.get("lendingFees", 0),
97 "margin_pending_gain": margin_balance.get("pl", 0),
98 "margin_position_type": margin_balance.get("type", None),
99 "margin_liquidation_price": margin_balance.get("liquidationPrice", 0),
100 "margin_borrowed_base_price": margin_balance.get("total", 0),
101 "margin_borrowed_base_currency": margin_balance.get("baseCurrency", None),
102 }
103 if len(margin_balance) > 0:
104 if margin_balance["baseCurrency"] not in in_positions:
105 in_positions[margin_balance["baseCurrency"]] = 0
106 in_positions[margin_balance["baseCurrency"]] += margin_balance["total"]
107
108 for currency, in_position in in_positions.items():
109 all_balances[currency]["total"] += in_position
110 all_balances[currency]["margin_total"] += in_position
111 all_balances[currency]["margin_borrowed"] += in_position
112
113 return all_balances
114ccxt.poloniex.fetch_all_balances = poloniex_fetch_all_balances
115
75def poloniex_parse_ticker(self, ticker, market=None): 116def poloniex_parse_ticker(self, ticker, market=None):
76 timestamp = self.milliseconds() 117 timestamp = self.milliseconds()
77 symbol = None 118 symbol = None
@@ -153,48 +194,54 @@ def poloniex_transfer_balance(self, currency, amount, from_account, to_account):
153 return result["success"] == 1 194 return result["success"] == 1
154ccxt.poloniex.transfer_balance = poloniex_transfer_balance 195ccxt.poloniex.transfer_balance = poloniex_transfer_balance
155 196
156# portfolio.market.create_order("DASH/BTC", "limit", "sell", 0.1, price=0.06828800, account="margin") 197def poloniex_close_margin_position(self, currency, base_currency):
157 198 """
158# portfolio.market.privatePostReturnTradableBalances() 199 closeMarginPosition({"currencyPair": "BTC_DASH"})
159# Returns tradable balances in margin 200 fermer la position au prix du marché
160# 'BTC_DASH': {'BTC': '0.01266999', 'DASH': '0.08574839'}, 201 """
161# Je peux emprunter jusqu’à 0.08574839 DASH ou 0.01266999 BTC (une position est 202 symbol = "{}_{}".format(base_currency, currency)
162# déjà ouverte) 203 self.privatePostCloseMarginPosition({"currencyPair": symbol})
163# 'BTC_CLAM': {'BTC': '0.00585143', 'CLAM': '7.79300395'}, 204ccxt.poloniex.close_margin_position = poloniex_close_margin_position
164# Je peux emprunter 7.7 CLAM pour les vendre contre des BTC, ou emprunter 205
165# 0.00585143 BTC pour acheter des CLAM 206def poloniex_tradable_balances(self):
166 207 """
167# portfolio.market.privatePostReturnMarginAccountSummary() 208 portfolio.market.privatePostReturnTradableBalances()
168# Returns current informations for margin 209 Returns tradable balances in margin
169# {'currentMargin': '1.49680968', -> marge (ne doit pas descendre sous 20% / 0.2) 210 'BTC_DASH': {'BTC': '0.01266999', 'DASH': '0.08574839'},
170# = netValue / totalBorrowedValue 211 Je peux emprunter jusqu’à 0.08574839 DASH ou 0.01266999 BTC (une position est déjà ouverte)
171# 'lendingFees': '0.00000000', -> fees totaux 212 'BTC_CLAM': {'BTC': '0.00585143', 'CLAM': '7.79300395'},
172# 'netValue': '0.01008254', -> balance + plus-value 213 Je peux emprunter 7.7 CLAM pour les vendre contre des BTC, ou emprunter 0.00585143 BTC pour acheter des CLAM
173# 'pl': '0.00008254', -> plus value latente (somme des positions) 214 """
174# 'totalBorrowedValue': '0.00673602', -> valeur en BTC empruntée 215
175# 'totalValue': '0.01000000'} -> valeur totale en compte 216 tradable_balances = self.privatePostReturnTradableBalances()
176 217 for symbol, balances in tradable_balances.items():
177 218 for currency, balance in balances.items():
178# portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"}) 219 balances[currency] = decimal.Decimal(balance)
179# See DASH/BTC positions 220 return tradable_balances
180# {'amount': '-0.10000000', -> DASH empruntés 221ccxt.poloniex.fetch_tradable_balances = poloniex_tradable_balances
181# 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%)) 222
182# 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur 223def poloniex_margin_summary(self):
183# 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin) 224 """
184# 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu) 225 portfolio.market.privatePostReturnMarginAccountSummary()
185# 'total': '0.00681856', -> valeur totale empruntée en BTC 226 Returns current informations for margin
186# 'type': 'short'} 227 {'currentMargin': '1.49680968', -> marge (ne doit pas descendre sous 20% / 0.2)
187 228 = netValue / totalBorrowedValue
188 229 'lendingFees': '0.00000000', -> fees totaux
189# closeMarginPosition({"currencyPair": "BTC_DASH"}) : fermer la position au prix 230 'netValue': '0.01008254', -> balance + plus-value
190# du marché 231 'pl': '0.00008254', -> plus value latente (somme des positions)
191# Nécessaire à la fin 232 'totalBorrowedValue': '0.00673602', -> valeur en BTC empruntée
192# portfolio.market.create_order("DASH/BTC", "limit", "buy", 0.1, price=0.06726487, account="margin") 233 'totalValue': '0.01000000'} -> valeur totale en compte
193 234 """
194# portfolio.market.fetch_balance_per_type() 235 summary = self.privatePostReturnMarginAccountSummary()
195# Ne suffit pas pour calculer les positions: ne contient que les 0.01 envoyés
196# TODO: vérifier si fetch_balance marque ces 0.01 comme disponibles -> oui
197 236
237 return {
238 "current_margin": decimal.Decimal(summary["currentMargin"]),
239 "lending_fees": decimal.Decimal(summary["lendingFees"]),
240 "gains": decimal.Decimal(summary["pl"]),
241 "total_borrowed": decimal.Decimal(summary["totalBorrowedValue"]),
242 "total": decimal.Decimal(summary["totalValue"]),
243 }
244ccxt.poloniex.margin_summary = poloniex_margin_summary
198market = ccxt.poloniex({ 245market = ccxt.poloniex({
199 "apiKey": "XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX", 246 "apiKey": "XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX",
200 "secret": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 247 "secret": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
diff --git a/portfolio.py b/portfolio.py
index d9d2d4d..0ab16fd 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -141,11 +141,22 @@ class Amount:
141 def __truediv__(self, value): 141 def __truediv__(self, value):
142 return self.__floordiv__(value) 142 return self.__floordiv__(value)
143 143
144 def __le__(self, other):
145 return self == other or self < other
146
144 def __lt__(self, other): 147 def __lt__(self, other):
148 if other == 0:
149 return self.value < 0
145 if self.currency != other.currency: 150 if self.currency != other.currency:
146 raise Exception("Comparing amounts must be done with same currencies") 151 raise Exception("Comparing amounts must be done with same currencies")
147 return self.value < other.value 152 return self.value < other.value
148 153
154 def __gt__(self, other):
155 return not self <= other
156
157 def __ge__(self, other):
158 return not self < other
159
149 def __eq__(self, other): 160 def __eq__(self, other):
150 if other == 0: 161 if other == 0:
151 return self.value == 0 162 return self.value == 0
@@ -153,6 +164,12 @@ class Amount:
153 raise Exception("Comparing amounts must be done with same currencies") 164 raise Exception("Comparing amounts must be done with same currencies")
154 return self.value == other.value 165 return self.value == other.value
155 166
167 def __ne__(self, other):
168 return not self == other
169
170 def __neg__(self):
171 return Amount(self.currency, - self.value)
172
156 def __str__(self): 173 def __str__(self):
157 if self.linked_to is None: 174 if self.linked_to is None:
158 return "{:.8f} {}".format(self.value, self.currency) 175 return "{:.8f} {}".format(self.value, self.currency)
@@ -168,15 +185,24 @@ class Amount:
168class Balance: 185class Balance:
169 known_balances = {} 186 known_balances = {}
170 187
171 def __init__(self, currency, total_value, free_value, used_value): 188 def __init__(self, currency, hash_):
172 self.currency = currency 189 self.currency = currency
173 self.total = Amount(currency, total_value) 190 for key in ["total",
174 self.free = Amount(currency, free_value) 191 "exchange_total", "exchange_used", "exchange_free",
175 self.used = Amount(currency, used_value) 192 "margin_total", "margin_borrowed", "margin_free"]:
176 193 setattr(self, key, Amount(currency, hash_.get(key, 0)))
177 @classmethod 194
178 def from_hash(cls, currency, hash_): 195 self.margin_position_type = hash_["margin_position_type"]
179 return cls(currency, hash_["total"], hash_["free"], hash_["used"]) 196
197 if hash_["margin_borrowed_base_currency"] is not None:
198 base_currency = hash_["margin_borrowed_base_currency"]
199 for key in [
200 "margin_liquidation_price",
201 "margin_pending_gain",
202 "margin_lending_fees",
203 "margin_borrowed_base_price"
204 ]:
205 setattr(self, key, Amount(base_currency, hash_[key]))
180 206
181 @classmethod 207 @classmethod
182 def in_currency(cls, other_currency, market, compute_value="average", type="total"): 208 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
@@ -193,19 +219,13 @@ class Balance:
193 return cls.known_balances.keys() 219 return cls.known_balances.keys()
194 220
195 @classmethod 221 @classmethod
196 def _fill_balances(cls, hash_):
197 for key in hash_:
198 if key in ["info", "free", "used", "total"]:
199 continue
200 if hash_[key]["total"] != 0 or key in cls.known_balances:
201 cls.known_balances[key] = cls.from_hash(key, hash_[key])
202
203 @classmethod
204 def fetch_balances(cls, market): 222 def fetch_balances(cls, market):
205 cls._fill_balances(market.fetch_balance()) 223 all_balances = market.fetch_all_balances()
224 for currency, balance in all_balances.items():
225 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
226 currency in cls.known_balances:
227 cls.known_balances[currency] = cls(currency, balance)
206 return cls.known_balances 228 return cls.known_balances
207 # FIXME:Separate balances per trade type and in position
208 # Need to check how balances in position are represented
209 229
210 230
211 @classmethod 231 @classmethod
@@ -216,31 +236,21 @@ class Balance:
216 amounts = {} 236 amounts = {}
217 for currency, (ptt, trade_type) in repartition.items(): 237 for currency, (ptt, trade_type) in repartition.items():
218 amounts[currency] = ptt * amount / sum_ratio 238 amounts[currency] = ptt * amount / sum_ratio
239 if trade_type == "short":
240 amounts[currency] = - amounts[currency]
219 if currency not in cls.known_balances: 241 if currency not in cls.known_balances:
220 cls.known_balances[currency] = cls(currency, 0, 0, 0) 242 cls.known_balances[currency] = cls(currency, 0, 0, 0)
221 return amounts 243 return amounts
222 244
223 @classmethod 245 @classmethod
224 def dispatch_trade_types(cls, repartition=None):
225 if repartition is None:
226 repartition = Portfolio.repartition()
227 trade_types = {}
228 for currency, (ptt, trade_type) in repartition.items():
229 trade_types[currency] = trade_type
230 return trade_types
231 # FIXME: once we know the repartition and sold everything, we can move
232 # the necessary part to the margin account
233
234 @classmethod
235 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"): 246 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
236 cls.fetch_balances(market) 247 cls.fetch_balances(market)
237 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
238 total_base_value = sum(values_in_base.values()) 249 total_base_value = sum(values_in_base.values())
239 new_repartition = cls.dispatch_assets(total_base_value) 250 new_repartition = cls.dispatch_assets(total_base_value)
240 trade_types = cls.dispatch_trade_types()
241 # Recompute it in case we have new currencies 251 # Recompute it in case we have new currencies
242 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 252 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
243 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market) 253 Trade.compute_trades(values_in_base, new_repartition, market=market)
244 254
245 @classmethod 255 @classmethod
246 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None): 256 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
@@ -248,8 +258,7 @@ class Balance:
248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 258 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
249 total_base_value = sum(values_in_base.values()) 259 total_base_value = sum(values_in_base.values())
250 new_repartition = cls.dispatch_assets(total_base_value) 260 new_repartition = cls.dispatch_assets(total_base_value)
251 trade_types = cls.dispatch_trade_types() 261 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)
252 Trade.compute_trades(values_in_base, new_repartition, trade_types, only=only, market=market)
253 262
254 @classmethod 263 @classmethod
255 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"): 264 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
@@ -257,11 +266,39 @@ class Balance:
257 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 266 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
258 total_base_value = sum(values_in_base.values()) 267 total_base_value = sum(values_in_base.values())
259 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) 268 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
260 trade_types = cls.dispatch_trade_types() 269 Trade.compute_trades(values_in_base, new_repartition, market=market)
261 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
262 270
263 def __repr__(self): 271 def __repr__(self):
264 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) 272 if self.exchange_total > 0:
273 if self.exchange_free > 0 and self.exchange_used > 0:
274 exchange = " Exch: [✔{} + ❌{} = {}]".format(str(self.exchange_free), str(self.exchange_used), str(self.exchange_total))
275 elif self.exchange_free > 0:
276 exchange = " Exch: [✔{}]".format(str(self.exchange_free))
277 else:
278 exchange = " Exch: [❌{}]".format(str(self.exchange_used))
279 else:
280 exchange = ""
281
282 if self.margin_total > 0:
283 if self.margin_free != 0 and self.margin_borrowed != 0:
284 margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total))
285 elif self.margin_free != 0:
286 margin = " Margin: [✔{}]".format(str(self.margin_free))
287 else:
288 margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed))
289 elif self.margin_total < 0:
290 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
291 str(self.margin_borrowed_base_price),
292 str(self.margin_lending_fees))
293 else:
294 margin = ""
295
296 if self.margin_total != 0 and self.exchange_total != 0:
297 total = " Total: [{}]".format(str(self.total))
298 else:
299 total = ""
300
301 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")"
265 302
266class Computation: 303class Computation:
267 computations = { 304 computations = {
@@ -272,19 +309,21 @@ class Computation:
272 } 309 }
273 310
274class Trade: 311class Trade:
275 trades = {} 312 trades = []
276 313
277 def __init__(self, value_from, value_to, currency, trade_type, market=None): 314 def __init__(self, value_from, value_to, currency, market=None):
278 # We have value_from of currency, and want to finish with value_to of 315 # We have value_from of currency, and want to finish with value_to of
279 # that currency. value_* may not be in currency's terms 316 # that currency. value_* may not be in currency's terms
280 self.currency = currency 317 self.currency = currency
281 self.value_from = value_from 318 self.value_from = value_from
282 self.value_to = value_to 319 self.value_to = value_to
283 self.trade_type = trade_type
284 self.orders = [] 320 self.orders = []
285 self.market = market 321 self.market = market
286 assert self.value_from.currency == self.value_to.currency 322 assert self.value_from.currency == self.value_to.currency
287 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency 323 if self.value_from != 0:
324 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
325 elif self.value_from.linked_to is None:
326 self.value_from.linked_to = Amount(self.currency, 0)
288 self.base_currency = self.value_from.currency 327 self.base_currency = self.value_from.currency
289 328
290 fees_cache = {} 329 fees_cache = {}
@@ -331,28 +370,60 @@ class Trade:
331 return cls.get_ticker(c1, c2, market) 370 return cls.get_ticker(c1, c2, market)
332 371
333 @classmethod 372 @classmethod
334 def compute_trades(cls, values_in_base, new_repartition, trade_types, only=None, market=None): 373 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
335 base_currency = sum(values_in_base.values()).currency 374 base_currency = sum(values_in_base.values()).currency
336 for currency in Balance.currencies(): 375 for currency in Balance.currencies():
337 if currency == base_currency: 376 if currency == base_currency:
338 continue 377 continue
339 trade = cls( 378 value_from = values_in_base.get(currency, Amount(base_currency, 0))
340 values_in_base.get(currency, Amount(base_currency, 0)), 379 value_to = new_repartition.get(currency, Amount(base_currency, 0))
341 new_repartition.get(currency, Amount(base_currency, 0)), 380 if value_from.value * value_to.value < 0:
342 currency, 381 trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market)
343 trade_types.get(currency, "long"), 382 if only is None or trade_1.action == only:
344 market=market 383 cls.trades.append(trade_1)
345 ) 384 trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market)
346 if only is None or trade.action == only: 385 if only is None or trade_2.action == only:
347 cls.trades[currency] = trade 386 cls.trades.append(trade_2)
387 else:
388 trade = cls(
389 value_from,
390 value_to,
391 currency,
392 market=market
393 )
394 if only is None or trade.action == only:
395 cls.trades.append(trade)
348 return cls.trades 396 return cls.trades
349 397
350 @classmethod 398 @classmethod
351 def prepare_orders(cls, only=None, compute_value="default"): 399 def prepare_orders(cls, only=None, compute_value="default"):
352 for currency, trade in cls.trades.items(): 400 for trade in cls.trades:
353 if only is None or trade.action == only: 401 if only is None or trade.action == only:
354 trade.prepare_order(compute_value=compute_value) 402 trade.prepare_order(compute_value=compute_value)
355 403
404 @classmethod
405 def move_balances(cls, market, debug=False):
406 needed_in_margin = {}
407 for trade in cls.trades:
408 if trade.trade_type == "short":
409 if trade.value_to.currency not in needed_in_margin:
410 needed_in_margin[trade.value_to.currency] = 0
411 needed_in_margin[trade.value_to.currency] += abs(trade.value_to)
412 for currency, needed in needed_in_margin.items():
413 current_balance = Balance.known_balances[currency].margin_free
414 delta = (needed - current_balance).value
415 # FIXME: don't remove too much if there are open margin position
416 if delta > 0:
417 if debug:
418 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta))
419 else:
420 market.transfer_balance(currency, delta, "exchange", "margin")
421 elif delta < 0:
422 if debug:
423 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta))
424 else:
425 market.transfer_balance(currency, -delta, "margin", "exchange")
426
356 @property 427 @property
357 def action(self): 428 def action(self):
358 if self.value_from == self.value_to: 429 if self.value_from == self.value_to:
@@ -361,17 +432,23 @@ class Trade:
361 return None 432 return None
362 433
363 if self.value_from < self.value_to: 434 if self.value_from < self.value_to:
364 return "buy" 435 return "acquire"
365 else: 436 else:
366 return "sell" 437 return "dispose"
367 438
368 def order_action(self, inverted): 439 def order_action(self, inverted):
369 # a xor b xor c 440 if (self.value_from < self.value_to) != inverted:
370 if (self.trade_type == "short") != ((self.value_from < self.value_to) != inverted):
371 return "buy" 441 return "buy"
372 else: 442 else:
373 return "sell" 443 return "sell"
374 444
445 @property
446 def trade_type(self):
447 if self.value_from + self.value_to < 0:
448 return "short"
449 else:
450 return "long"
451
375 def prepare_order(self, compute_value="default"): 452 def prepare_order(self, compute_value="default"):
376 if self.action is None: 453 if self.action is None:
377 return 454 return
@@ -382,15 +459,13 @@ class Trade:
382 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 459 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
383 # 0.1 460 # 0.1
384 461
385 # FIXME: optimize if value_to == 0 or value_from == 0?)
386
387 delta_in_base = abs(self.value_from - self.value_to) 462 delta_in_base = abs(self.value_from - self.value_to)
388 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 463 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
389 464
390 if not inverted: 465 if not inverted:
391 currency = self.base_currency 466 currency = self.base_currency
392 # BTC 467 # BTC
393 if self.action == "sell": 468 if self.action == "dispose":
394 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it 469 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
395 # At rate 1 Foo = 0.1 BTC 470 # At rate 1 Foo = 0.1 BTC
396 value_from = self.value_from.linked_to 471 value_from = self.value_from.linked_to
@@ -418,7 +493,11 @@ class Trade:
418 # I want to buy 9 / 0.1 FOO 493 # I want to buy 9 / 0.1 FOO
419 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market" 494 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
420 495
421 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.trade_type, self.market)) 496 close_if_possible = (self.value_to == 0)
497
498 self.orders.append(Order(self.order_action(inverted),
499 delta, rate, currency, self.trade_type, self.market,
500 close_if_possible=close_if_possible))
422 501
423 @classmethod 502 @classmethod
424 def compute_value(cls, ticker, action, compute_value="default"): 503 def compute_value(cls, ticker, action, compute_value="default"):
@@ -432,7 +511,7 @@ class Trade:
432 511
433 @classmethod 512 @classmethod
434 def all_orders(cls, state=None): 513 def all_orders(cls, state=None):
435 all_orders = sum(map(lambda v: v.orders, cls.trades.values()), []) 514 all_orders = sum(map(lambda v: v.orders, cls.trades), [])
436 if state is None: 515 if state is None:
437 return all_orders 516 return all_orders
438 else: 517 else:
@@ -465,16 +544,15 @@ class Trade:
465 order.get_status() 544 order.get_status()
466 545
467 def __repr__(self): 546 def __repr__(self):
468 return "Trade({} -> {} in {}, {} {})".format( 547 return "Trade({} -> {} in {}, {})".format(
469 self.value_from, 548 self.value_from,
470 self.value_to, 549 self.value_to,
471 self.currency, 550 self.currency,
472 self.action, 551 self.action)
473 self.trade_type)
474 552
475 @classmethod 553 @classmethod
476 def print_all_with_order(cls): 554 def print_all_with_order(cls):
477 for trade in cls.trades.values(): 555 for trade in cls.trades:
478 trade.print_with_order() 556 trade.print_with_order()
479 557
480 def print_with_order(self): 558 def print_with_order(self):
@@ -483,7 +561,8 @@ class Trade:
483 print("\t", order, sep="") 561 print("\t", order, sep="")
484 562
485class Order: 563class Order:
486 def __init__(self, action, amount, rate, base_currency, trade_type, market): 564 def __init__(self, action, amount, rate, base_currency, trade_type, market,
565 close_if_possible=False):
487 self.action = action 566 self.action = action
488 self.amount = amount 567 self.amount = amount
489 self.rate = rate 568 self.rate = rate
@@ -492,15 +571,17 @@ class Order:
492 self.trade_type = trade_type 571 self.trade_type = trade_type
493 self.result = None 572 self.result = None
494 self.status = "pending" 573 self.status = "pending"
574 self.close_if_possible = close_if_possible
495 575
496 def __repr__(self): 576 def __repr__(self):
497 return "Order({} {} {} at {} {} [{}])".format( 577 return "Order({} {} {} at {} {} [{}]{})".format(
498 self.action, 578 self.action,
499 self.trade_type, 579 self.trade_type,
500 self.amount, 580 self.amount,
501 self.rate, 581 self.rate,
502 self.base_currency, 582 self.base_currency,
503 self.status 583 self.status,
584 " ✂" if self.close_if_possible else "",
504 ) 585 )
505 586
506 @property 587 @property
@@ -527,8 +608,6 @@ class Order:
527 symbol, self.action, amount, self.rate, self.account)) 608 symbol, self.action, amount, self.rate, self.account))
528 else: 609 else:
529 try: 610 try:
530 if self.action == "sell" and self.trade_type == "short":
531 assert self.market.transfer_balance(self.base_currency, amount * self.rate, "exchange", "margin")
532 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account) 611 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)
533 self.status = "open" 612 self.status = "open"
534 except Exception as e: 613 except Exception as e:
@@ -542,9 +621,17 @@ class Order:
542 # other states are "closed" and "canceled" 621 # other states are "closed" and "canceled"
543 if self.status == "open": 622 if self.status == "open":
544 result = self.market.fetch_order(self.result['id']) 623 result = self.market.fetch_order(self.result['id'])
545 self.status = result["status"] 624 if result["status"] != "open":
625 self.mark_finished_order(result["status"])
546 return self.status 626 return self.status
547 627
628 def mark_finished_order(self, status):
629 if status == "closed":
630 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
631 self.market.close_margin_position(self.amount.currency, self.base_currency)
632
633 self.status = result["status"]
634
548 def cancel(self): 635 def cancel(self):
549 self.market.cancel_order(self.result['id']) 636 self.market.cancel_order(self.result['id'])
550 637
@@ -557,11 +644,23 @@ def print_orders(market, base_currency="BTC"):
557 644
558def make_orders(market, base_currency="BTC"): 645def make_orders(market, base_currency="BTC"):
559 Balance.prepare_trades(market, base_currency=base_currency) 646 Balance.prepare_trades(market, base_currency=base_currency)
560 for currency, trade in Trade.trades.items(): 647 for trade in Trade.trades:
561 print(trade) 648 print(trade)
562 for order in trade.orders: 649 for order in trade.orders:
563 print("\t", order, sep="") 650 print("\t", order, sep="")
564 order.run() 651 order.run()
565 652
653def sell_all(market, base_currency="BTC"):
654 Balance.prepare_trades_to_sell_all(market)
655 Trade.prepare_orders(compute_value="average")
656 Trade.run_orders()
657 Trade.follow_orders()
658
659 Balance.update_trades(market, only="acquire")
660 Trade.prepare_orders(only="acquire")
661 Trade.move_balances(market)
662 Trade.run_orders()
663 Trade.follow_orders()
664
566if __name__ == '__main__': 665if __name__ == '__main__':
567 print_orders(market) 666 print_orders(market)
diff --git a/test.py b/test.py
index 8240eb4..b85e39f 100644
--- a/test.py
+++ b/test.py
@@ -844,6 +844,10 @@ class AcceptanceTest(unittest.TestCase):
844 self.assertEqual("sell", all_orders[3].action) 844 self.assertEqual("sell", all_orders[3].action)
845 self.assertEqual("long", all_orders[3].trade_type) 845 self.assertEqual("long", all_orders[3].trade_type)
846 846
847 # Action 6b
848 # TODO:
849 # Move balances to margin
850
847 # Action 7 851 # Action 7
848 # TODO 852 # TODO
849 # portfolio.Trade.run_orders() 853 # portfolio.Trade.run_orders()