diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-04 19:12:50 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-04 19:15:03 +0100 |
commit | 006a20846236ad365ec814f848f5fbf7e3dc7d3c (patch) | |
tree | fd28f7f278d7d5156f0b7a939b35507c2a6dc4ba | |
parent | 350ed24de673dc125be9e2fdecb0f1abc7835b41 (diff) | |
download | Trader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.tar.gz Trader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.tar.zst Trader-006a20846236ad365ec814f848f5fbf7e3dc7d3c.zip |
WIP: handle more balance information
-rw-r--r-- | market.py | 161 | ||||
-rw-r--r-- | portfolio.py | 243 | ||||
-rw-r--r-- | test.py | 4 |
3 files changed, 279 insertions, 129 deletions
@@ -26,7 +26,18 @@ def poloniex_fetch_balance(self, params={}): | |||
26 | return self.parse_balance(result) | 26 | return self.parse_balance(result) |
27 | ccxt.poloniex.fetch_balance = poloniex_fetch_balance | 27 | ccxt.poloniex.fetch_balance = poloniex_fetch_balance |
28 | 28 | ||
29 | def poloniex_fetch_margin_balances(self): | 29 | def 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 |
47 | ccxt.poloniex.fetch_margin_balances = poloniex_fetch_margin_balances | 58 | ccxt.poloniex.fetch_margin_balance = poloniex_fetch_margin_balance |
48 | |||
49 | def 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 | ||
59 | ccxt.poloniex.fetch_balance_with_margin = poloniex_fetch_balance_with_margin | ||
60 | |||
61 | 59 | ||
62 | def poloniex_fetch_balance_per_type(self): | 60 | def 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 |
73 | ccxt.poloniex.fetch_balance_per_type = poloniex_fetch_balance_per_type | 71 | ccxt.poloniex.fetch_balance_per_type = poloniex_fetch_balance_per_type |
74 | 72 | ||
73 | def 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 | ||
114 | ccxt.poloniex.fetch_all_balances = poloniex_fetch_all_balances | ||
115 | |||
75 | def poloniex_parse_ticker(self, ticker, market=None): | 116 | def 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 |
154 | ccxt.poloniex.transfer_balance = poloniex_transfer_balance | 195 | ccxt.poloniex.transfer_balance = poloniex_transfer_balance |
155 | 196 | ||
156 | # portfolio.market.create_order("DASH/BTC", "limit", "sell", 0.1, price=0.06828800, account="margin") | 197 | def 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'}, | 204 | ccxt.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 | 206 | def 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 | 221 | ccxt.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 | 223 | def 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 | } | ||
244 | ccxt.poloniex.margin_summary = poloniex_margin_summary | ||
198 | market = ccxt.poloniex({ | 245 | market = 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: | |||
168 | class Balance: | 185 | class 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 | ||
266 | class Computation: | 303 | class Computation: |
267 | computations = { | 304 | computations = { |
@@ -272,19 +309,21 @@ class Computation: | |||
272 | } | 309 | } |
273 | 310 | ||
274 | class Trade: | 311 | class 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 | ||
485 | class Order: | 563 | class 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 | ||
558 | def make_orders(market, base_currency="BTC"): | 645 | def 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 | ||
653 | def 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 | |||
566 | if __name__ == '__main__': | 665 | if __name__ == '__main__': |
567 | print_orders(market) | 666 | print_orders(market) |
@@ -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() |