]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/commitdiff
Various fixes/improvements v0.3
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Thu, 1 Mar 2018 12:14:41 +0000 (13:14 +0100)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Thu, 1 Mar 2018 12:14:41 +0000 (13:14 +0100)
- Use pending gains to compute the move_balance
- Use ttl_cache for tickers

ccxt_wrapper.py
market.py
portfolio.py
requirements.txt
store.py
test.py

index b79cd37468c4c58654288acd8bcb1a3c7f409741..903bfc489958a4539f69ae2deb5183bda6c2a29d 100644 (file)
@@ -38,12 +38,13 @@ class poloniexE(poloniex):
         """
         portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
         See DASH/BTC positions
-        {'amount': '-0.10000000',          -> DASH empruntés
+        {'amount': '-0.10000000',         -> DASH empruntés (à rendre)
         'basePrice': '0.06818560',        -> à ce prix là (0.06828800 demandé * (1-0.15%))
-        'lendingFees': '0.00000000',      -> ce que je dois à mon créditeur
+        'lendingFees': '0.00000000',      -> ce que je dois à mon créditeur en intérêts
         'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin)
         'pl': '-0.00000371',              -> plus-value latente si je rachète tout de suite (négatif = perdu)
-        'total': '0.00681856',            -> valeur totale empruntée en BTC
+        'total': '0.00681856',            -> valeur totale empruntée en BTC (au moment de l'échange)
+                                             = amount * basePrice à erreur d'arrondi près
         'type': 'short'}
         """
         positions = self.privatePostGetMarginPosition({"currencyPair": "all"})
@@ -70,8 +71,7 @@ class poloniexE(poloniex):
         for key, balance in balances.items():
             result[key] = {}
             for currency, amount in balance.items():
-                if currency not in result:
-                    result[currency] = {}
+                result.setdefault(currency, {})
                 result[currency][key] = decimal.Decimal(amount)
                 result[key][currency] = decimal.Decimal(amount)
         return result
@@ -83,6 +83,7 @@ class poloniexE(poloniex):
 
         all_balances = {}
         in_positions = {}
+        pending_pl = {}
 
         for currency, exchange_balance in exchange_balances.items():
             if currency in ["info", "free", "used", "total"]:
@@ -96,25 +97,106 @@ class poloniexE(poloniex):
                 "exchange_used": exchange_balance["used"],
                 "exchange_total": exchange_balance["total"] - balance_per_type.get("margin", 0),
                 "exchange_free": exchange_balance["free"] - balance_per_type.get("margin", 0),
-                "margin_free": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
-                "margin_borrowed": 0,
+                # Disponible sur le compte margin
+                "margin_available": balance_per_type.get("margin", 0),
+                # Bloqué en position
+                "margin_in_position": 0,
+                # Emprunté
+                "margin_borrowed": -margin_balance.get("amount", 0),
+                # Total
                 "margin_total": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
+                "margin_pending_gain": 0,
                 "margin_lending_fees": margin_balance.get("lendingFees", 0),
-                "margin_pending_gain": margin_balance.get("pl", 0),
+                "margin_pending_base_gain": margin_balance.get("pl", 0),
                 "margin_position_type": margin_balance.get("type", None),
                 "margin_liquidation_price": margin_balance.get("liquidationPrice", 0),
                 "margin_borrowed_base_price": margin_balance.get("total", 0),
                 "margin_borrowed_base_currency": margin_balance.get("baseCurrency", None),
                 }
             if len(margin_balance) > 0:
-                if margin_balance["baseCurrency"] not in in_positions:
-                    in_positions[margin_balance["baseCurrency"]] = 0
+                in_positions.setdefault(margin_balance["baseCurrency"], 0)
                 in_positions[margin_balance["baseCurrency"]] += margin_balance["total"]
 
+                pending_pl.setdefault(margin_balance["baseCurrency"], 0)
+                pending_pl[margin_balance["baseCurrency"]] += margin_balance["pl"]
+
+            # J’emprunte 0.12062983 que je revends à 0.06003598 BTC/DASH, soit 0.00724213 BTC.
+            # Sur ces 0.00724213 BTC je récupère 0.00724213*(1-0.0015) = 0.00723127 BTC
+            # 
+            # -> ordertrades ne tient pas compte des fees
+            #     amount = montant vendu (un seul mouvement)
+            #     rate = à ce taux
+            #     total = total en BTC (pour ce mouvement)
+            # -> marginposition:
+            #     amount = ce que je dois rendre
+            #     basePrice = prix de vente en tenant compte des fees
+            #     (amount * basePrice = la quantité de BTC que j’ai effectivement
+            #     reçue à erreur d’arrondi près, utiliser plutôt "total")
+            #     total = la quantité de BTC que j’ai reçue
+            #     pl = plus value actuelle si je rachetais tout de suite
+            # -> marginaccountsummary:
+            #     currentMargin = La marge actuelle (= netValue/totalBorrowedValue)
+            #     totalValue = BTC actuellement en margin (déposé)
+            #     totalBorrowedValue = sum (amount * ticker[lowestAsk])
+            #     pl = sum(pl)
+            #     netValue = BTC actuellement en margin (déposé) + pl
+            # Exemple:
+            # In [38]: m.ccxt.private_post_returnordertrades({"orderNumber": "XXXXXXXXXXXX"})
+            # Out[38]:
+            # [{'amount': '0.11882982',
+            #   'currencyPair': 'BTC_DASH',
+            #   'date': '2018-02-26 22:48:35',
+            #   'fee': '0.00150000',
+            #   'globalTradeID': 348891380,
+            #   'rate': '0.06003598',
+            #   'total': '0.00713406',
+            #   'tradeID': 9634443,
+            #   'type': 'sell'},
+            #  {'amount': '0.00180000',
+            #   'currencyPair': 'BTC_DASH',
+            #   'date': '2018-02-26 22:48:30',
+            #   'fee': '0.00150000',
+            #   'globalTradeID': 348891375,
+            #   'rate': '0.06003598',
+            #   'total': '0.00010806',
+            #   'tradeID': 9634442,
+            #   'type': 'sell'}]
+            # 
+            # In [51]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
+            # Out[51]:
+            # {'amount': '-0.12062982',
+            #  'basePrice': '0.05994587',
+            #  'lendingFees': '0.00000000',
+            #  'liquidationPrice': '0.15531479',
+            #  'pl': '0.00000122',
+            #  'total': '0.00723126',
+            #  'type': 'short'}
+            # In [52]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_BTS"})
+            # Out[52]:
+            # {'amount': '-332.97159188',
+            #  'basePrice': '0.00002171',
+            #  'lendingFees': '0.00000000',
+            #  'liquidationPrice': '0.00005543',
+            #  'pl': '0.00029548',
+            #  'total': '0.00723127',
+            #  'type': 'short'}
+            # 
+            # In [53]: m.ccxt.privatePostReturnMarginAccountSummary()
+            # Out[53]:
+            # {'currentMargin': '1.04341991',
+            #  'lendingFees': '0.00000000',
+            #  'netValue': '0.01478093',
+            #  'pl': '0.00029666',
+            #  'totalBorrowedValue': '0.01416585',
+            #  'totalValue': '0.01448427'}
+
         for currency, in_position in in_positions.items():
             all_balances[currency]["total"] += in_position
+            all_balances[currency]["margin_in_position"] += in_position
             all_balances[currency]["margin_total"] += in_position
-            all_balances[currency]["margin_borrowed"] += in_position
+
+        for currency, pl in pending_pl.items():
+            all_balances[currency]["margin_pending_gain"] += pl
 
         return all_balances
 
@@ -228,8 +310,9 @@ class poloniexE(poloniex):
         'lendingFees': '0.00000000',        -> fees totaux
         'netValue': '0.01008254',           -> balance + plus-value
         'pl': '0.00008254',                 -> plus value latente (somme des positions)
-        'totalBorrowedValue': '0.00673602', -> valeur en BTC empruntée
-        'totalValue': '0.01000000'}         -> valeur totale en compte
+        'totalBorrowedValue': '0.00673602', -> valeur empruntée convertie en BTC.
+                (= sum(amount * ticker[lowerAsk]) pour amount dans marginposition)
+        'totalValue': '0.01000000'}         -> balance (collateral déposé en margin)
         """
         summary = self.privatePostReturnMarginAccountSummary()
 
index 0cb3e67e5fc8bd1efd3542f4a06c6b0f08a46285..9122fc07256ce7d30e9e3490912ea54a15c842d7 100644 (file)
--- a/market.py
+++ b/market.py
@@ -1,7 +1,8 @@
-from ccxt import ExchangeError
+from ccxt import ExchangeError, NotSupported
 import ccxt_wrapper as ccxt
 import time
 from store import *
+from cachetools.func import ttl_cache
 
 class Market:
     debug = False
@@ -42,19 +43,17 @@ class Market:
         needed_in_margin = {} 
         moving_to_margin = {}
 
-        for currency in self.balances.all:
-            if self.balances.all[currency].margin_free != 0:
-                needed_in_margin[currency] = 0
-        for trade in self.trades.all:
-            if trade.value_to.currency not in needed_in_margin:
-                needed_in_margin[trade.value_to.currency] = 0
+        for currency, balance in self.balances.all.items():
+            needed_in_margin[currency] = balance.margin_in_position - balance.margin_pending_gain
+        for trade in self.trades.pending:
+            needed_in_margin.setdefault(trade.base_currency, 0)
             if trade.trade_type == "short":
-                needed_in_margin[trade.value_to.currency] += abs(trade.value_to)
+                needed_in_margin[trade.base_currency] -= trade.delta
         for currency, needed in needed_in_margin.items():
-            current_balance = self.balances.all[currency].margin_free
+            current_balance = self.balances.all[currency].margin_available
             moving_to_margin[currency] = (needed - current_balance)
             delta = moving_to_margin[currency].value
-            if self.debug:
+            if self.debug and delta != 0:
                 self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency]))
                 continue
             if delta > 0:
@@ -65,14 +64,18 @@ class Market:
 
         self.balances.fetch_balances()
 
-    fees_cache = {}
+    @ttl_cache(ttl=3600)
     def fetch_fees(self):
-        if self.ccxt.__class__ not in self.fees_cache:
-            self.fees_cache[self.ccxt.__class__] = self.ccxt.fetch_fees()
-        return self.fees_cache[self.ccxt.__class__]
+        return self.ccxt.fetch_fees()
+
+    @ttl_cache(maxsize=20, ttl=5)
+    def get_tickers(self, refresh=False):
+        try:
+            return self.ccxt.fetch_tickers()
+        except NotSupported:
+            return None
 
-    ticker_cache = {}
-    ticker_cache_timestamp = time.time()
+    @ttl_cache(maxsize=20, ttl=5)
     def get_ticker(self, c1, c2, refresh=False):
         def invert(ticker):
             return {
@@ -86,25 +89,25 @@ class Market:
                 "average": (ticker["bid"] + ticker["ask"] ) / 2,
                 })
 
-        if time.time() - self.ticker_cache_timestamp > 5:
-            self.ticker_cache = {}
-            self.ticker_cache_timestamp = time.time()
-        elif not refresh:
-            if (c1, c2, self.ccxt.__class__) in self.ticker_cache:
-                return self.ticker_cache[(c1, c2, self.ccxt.__class__)]
-            if (c2, c1, self.ccxt.__class__) in self.ticker_cache:
-                return invert(self.ticker_cache[(c2, c1, self.ccxt.__class__)])
-
-        try:
-            self.ticker_cache[(c1, c2, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c1, c2))
-            augment_ticker(self.ticker_cache[(c1, c2, self.ccxt.__class__)])
-        except ExchangeError:
+        tickers = self.get_tickers()
+        if tickers is None:
             try:
-                self.ticker_cache[(c2, c1, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c2, c1))
-                augment_ticker(self.ticker_cache[(c2, c1, self.ccxt.__class__)])
+                ticker = self.ccxt.fetch_ticker("{}/{}".format(c1, c2))
+                augment_ticker(ticker)
             except ExchangeError:
-                self.ticker_cache[(c1, c2, self.ccxt.__class__)] = None
-        return self.get_ticker(c1, c2)
+                try:
+                    ticker = invert(self.ccxt.fetch_ticker("{}/{}".format(c2, c1)))
+                except ExchangeError:
+                    ticker = None
+        else:
+            if "{}/{}".format(c1, c2) in tickers:
+                ticker = tickers["{}/{}".format(c1, c2)]
+                augment_ticker(ticker)
+            elif "{}/{}".format(c2, c1) in tickers:
+                ticker = invert(tickers["{}/{}".format(c2, c1)])
+            else:
+                ticker = None
+        return ticker
 
     def follow_orders(self, sleep=None):
         if sleep is None:
index eb3390ed70d2c156f90002afa994e91a14ddf651..b77850bfbe1021b30e704dbd34b7c41d19ec5316 100644 (file)
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
 from decimal import Decimal as D, ROUND_DOWN
 from json import JSONDecodeError
 from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
-from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder
+from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached
 from retry import retry
 import requests
 
@@ -226,8 +226,8 @@ class Amount:
 
 class Balance:
     base_keys = ["total", "exchange_total", "exchange_used",
-            "exchange_free", "margin_total", "margin_borrowed",
-            "margin_free"]
+            "exchange_free", "margin_total", "margin_in_position",
+            "margin_available", "margin_borrowed", "margin_pending_gain"]
 
     def __init__(self, currency, hash_):
         self.currency = currency
@@ -240,8 +240,8 @@ class Balance:
             base_currency = hash_["margin_borrowed_base_currency"]
             for key in [
                     "margin_liquidation_price",
-                    "margin_pending_gain",
                     "margin_lending_fees",
+                    "margin_pending_base_gain",
                     "margin_borrowed_base_price"
                     ]:
                 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
@@ -261,12 +261,12 @@ class Balance:
             exchange = ""
 
         if self.margin_total > 0:
-            if self.margin_free != 0 and self.margin_borrowed != 0:
-                margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total))
-            elif self.margin_free != 0:
-                margin = " Margin: [✔{}]".format(str(self.margin_free))
+            if self.margin_available != 0 and self.margin_in_position != 0:
+                margin = " Margin: [✔{} + ❌{} = {}]".format(str(self.margin_available), str(self.margin_in_position), str(self.margin_total))
+            elif self.margin_available != 0:
+                margin = " Margin: [✔{}]".format(str(self.margin_available))
             else:
-                margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed))
+                margin = " Margin: [❌{}]".format(str(self.margin_in_position))
         elif self.margin_total < 0:
             margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
                     str(self.margin_borrowed_base_price),
@@ -290,6 +290,7 @@ class Trade:
         self.value_to = value_to
         self.orders = []
         self.market = market
+        assert self.value_from.value * self.value_to.value >= 0
         assert self.value_from.currency == self.value_to.currency
         if self.value_from != 0:
             assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
@@ -297,6 +298,10 @@ class Trade:
             self.value_from.linked_to = Amount(self.currency, 0)
         self.base_currency = self.value_from.currency
 
+    @property
+    def delta(self):
+        return self.value_to - self.value_from
+
     @property
     def action(self):
         if self.value_from == self.value_to:
@@ -322,6 +327,10 @@ class Trade:
         else:
             return "long"
 
+    @property
+    def is_fullfiled(self):
+        return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
+
     def filled_amount(self, in_base_currency=False):
         filled_amount = 0
         for order in self.orders:
@@ -329,34 +338,36 @@ class Trade:
         return filled_amount
 
     def update_order(self, order, tick):
-        new_order = None
-        if tick in [0, 1, 3, 4, 6]:
+        actions = {
+                0: ["waiting", None],
+                1: ["waiting", None],
+                2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
+                3: ["waiting", None],
+                4: ["waiting", None],
+                5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
+                6: ["waiting", None],
+                7: ["market_fallback", "default"],
+                }
+
+        if tick in actions:
+            update, compute_value = actions[tick]
+        elif tick % 3 == 1:
+            update = "market_adjust"
+            compute_value = "default"
+        else:
             update = "waiting"
             compute_value = None
-        elif tick == 2:
-            update = "adjusting"
-            compute_value = 'lambda x, y: (x[y] + x["average"]) / 2'
-            new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2)
-        elif tick ==5:
-            update = "adjusting"
-            compute_value = 'lambda x, y: (x[y]*2 + x["average"]) / 3'
-            new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
-        elif tick >= 7:
-            if (tick - 7) % 3 == 0:
-                new_order = self.prepare_order(compute_value="default")
-                update = "market_adjust"
-                compute_value = "default"
-            else:
-                update = "waiting"
-                compute_value = None
-            if tick == 7:
-                update = "market_fallback"
+
+        if compute_value is not None:
+            order.cancel()
+            new_order = self.prepare_order(compute_value=compute_value)
+        else:
+            new_order = None
 
         self.market.report.log_order(order, tick, update=update,
                 compute_value=compute_value, new_order=new_order)
 
         if new_order is not None:
-            order.cancel()
             new_order.run()
             self.market.report.log_order(order, tick, new_order=new_order)
 
@@ -369,10 +380,9 @@ class Trade:
             ticker = ticker["original"]
         rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
 
-        #TODO: store when the order is considered filled
         # FIXME: Dust amount should be removed from there if they werent
         # honored in other sales
-        delta_in_base = abs(self.value_from - self.value_to)
+        delta_in_base = abs(self.delta)
         # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
 
         if not inverted:
@@ -477,7 +487,6 @@ class Order:
         self.trade = trade
         self.close_if_possible = close_if_possible
         self.id = None
-        self.fetch_cache_timestamp = None
         self.tries = 0
 
     def as_json(self):
@@ -578,21 +587,19 @@ class Order:
             if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
                 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
 
-    def fetch(self, force=False):
+    def fetch(self):
         if self.market.debug:
             self.market.report.log_debug_action("Fetching {}".format(self))
             return
-        if (not force and self.fetch_cache_timestamp is not None
-                and time.time() - self.fetch_cache_timestamp < 10):
-            return
-        self.fetch_cache_timestamp = time.time()
-
-        result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency)
-        self.results.append(result)
+        try:
+            result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency)
+            self.results.append(result)
+            self.status = result["status"]
+            # Time at which the order started
+            self.timestamp = result["datetime"]
+        except OrderNotCached:
+            self.status = "closed_unknown"
 
-        self.status = result["status"]
-        # Time at which the order started
-        self.timestamp = result["datetime"]
         self.fetch_mouvements()
 
         # FIXME: consider open order with dust remaining as closed
@@ -601,8 +608,6 @@ class Order:
         return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
 
     def remaining_amount(self):
-        if self.status == "open":
-            self.fetch()
         return self.amount - self.filled_amount()
 
     def filled_amount(self, in_base_currency=False):
@@ -633,7 +638,7 @@ class Order:
             self.status = "canceled"
             return
         self.market.ccxt.cancel_order(self.id)
-        self.fetch(force=True)
+        self.fetch()
 
 class Mouvement:
     def __init__(self, currency, base_currency, hash_):
index 5a25dbf86977b325e24ea83edebfe8e34337517a..1bc76ec67f0b9289c791cab50ece5625d277e90c 100644 (file)
@@ -4,3 +4,4 @@ requests>=2.18.4
 requests_mock==1.4.0
 psycopg2==2.7.4
 retry==0.9.2
+cachetools==2.0.1
index bebd127f14f482010f52eb73407484b214f96cce..c581608ca9ad67338cea98a2ae86457011d375b2 100644 (file)
--- a/store.py
+++ b/store.py
@@ -2,6 +2,7 @@ import portfolio
 import simplejson as json
 from decimal import Decimal as D, ROUND_DOWN
 from datetime import date, datetime
+import inspect
 
 __all__ = ["BalanceStore", "ReportStore", "TradeStore"]
 
@@ -55,6 +56,9 @@ class ReportStore:
             compute_value, type):
         values = {}
         rates = {}
+        if callable(compute_value):
+            compute_value = inspect.getsource(compute_value).strip()
+
         for currency, amount in amounts.items():
             values[currency] = amount.as_json()["value"]
             rates[currency] = amount.rate
@@ -92,6 +96,8 @@ class ReportStore:
             })
 
     def log_orders(self, orders, tick=None, only=None, compute_value=None):
+        if callable(compute_value):
+            compute_value = inspect.getsource(compute_value).strip()
         self.print_log("[Orders]")
         self.market.trades.print_all_with_order(ind="\t")
         self.add_log({
@@ -104,6 +110,8 @@ class ReportStore:
 
     def log_order(self, order, tick, finished=False, update=None,
             new_order=None, compute_value=None):
+        if callable(compute_value):
+            compute_value = inspect.getsource(compute_value).strip()
         if finished:
             self.print_log("[Order] Finished {}".format(order))
         elif update == "waiting":
@@ -201,8 +209,7 @@ class BalanceStore:
             amounts[currency] = ptt * amount / sum_ratio
             if trade_type == "short":
                 amounts[currency] = - amounts[currency]
-            if currency not in self.all:
-                self.all[currency] = portfolio.Balance(currency, {})
+            self.all.setdefault(currency, portfolio.Balance(currency, {}))
         self.market.report.log_dispatch(amount, amounts, liquidity, repartition)
         return amounts
 
@@ -214,6 +221,10 @@ class TradeStore:
         self.market = market
         self.all = []
 
+    @property
+    def pending(self):
+        return list(filter(lambda t: not t.is_fullfiled, self.all))
+
     def compute_trades(self, values_in_base, new_repartition, only=None):
         computed_trades = []
         base_currency = sum(values_in_base.values()).currency
@@ -248,7 +259,7 @@ class TradeStore:
 
     def prepare_orders(self, only=None, compute_value="default"):
         orders = []
-        for trade in self.all:
+        for trade in self.pending:
             if only is None or trade.action == only:
                 orders.append(trade.prepare_order(compute_value=compute_value))
         self.market.report.log_orders(orders, only, compute_value)
diff --git a/test.py b/test.py
index 778fc14961cf27011a352624936dcbe0b7780a2f..141c9e062d9cd6ea9a7ecfdb98bcdf3bdef93862 100644 (file)
--- a/test.py
+++ b/test.py
@@ -35,10 +35,6 @@ class WebMockTestCase(unittest.TestCase):
                 mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}),
                 mock.patch.multiple(portfolio.Computation,
                     computations=portfolio.Computation.computations),
-                mock.patch.multiple(market.Market,
-                    fees_cache={},
-                    ticker_cache={},
-                    ticker_cache_timestamp=self.time.time()),
                 ]
         for patcher in self.patchers:
             patcher.start()
@@ -472,8 +468,9 @@ class BalanceTest(WebMockTestCase):
             "exchange_free": "0.35",
             "exchange_used": "0.30",
             "margin_total": "-10",
-            "margin_borrowed": "-10",
-            "margin_free": "0",
+            "margin_borrowed": "10",
+            "margin_available": "0",
+            "margin_in_position": "0",
             "margin_position_type": "short",
             "margin_borrowed_base_currency": "USDT",
             "margin_liquidation_price": "1.20",
@@ -489,11 +486,11 @@ class BalanceTest(WebMockTestCase):
         self.assertEqual("BTC", balance.exchange_total.currency)
 
         self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
-        self.assertEqual(portfolio.D("-10"), balance.margin_borrowed.value)
-        self.assertEqual(portfolio.D("0"), balance.margin_free.value)
+        self.assertEqual(portfolio.D("10"), balance.margin_borrowed.value)
+        self.assertEqual(portfolio.D("0"), balance.margin_available.value)
         self.assertEqual("BTC", balance.margin_total.currency)
         self.assertEqual("BTC", balance.margin_borrowed.currency)
-        self.assertEqual("BTC", balance.margin_free.currency)
+        self.assertEqual("BTC", balance.margin_available.currency)
 
         self.assertEqual("BTC", balance.currency)
 
@@ -511,10 +508,10 @@ class BalanceTest(WebMockTestCase):
         self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
 
         balance = portfolio.Balance("BTX", { "margin_total": 3,
-            "margin_borrowed": 1, "margin_free": 2 })
-        self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
+            "margin_in_position": 1, "margin_available": 2 })
+        self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
 
-        balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_free": 2 })
+        balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_available": 2 })
         self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
 
         balance = portfolio.Balance("BTX", { "margin_total": -3,
@@ -524,8 +521,8 @@ class BalanceTest(WebMockTestCase):
         self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
 
         balance = portfolio.Balance("BTX", { "margin_total": 1,
-            "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2})
-        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
+            "margin_in_position": 1, "exchange_free": 2, "exchange_total": 2})
+        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
 
     def test_as_json(self):
         balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })
@@ -536,7 +533,7 @@ class BalanceTest(WebMockTestCase):
         self.assertEqual(D(2), as_json["exchange_free"])
         self.assertEqual(D(0), as_json["exchange_used"])
         self.assertEqual(D(0), as_json["margin_total"])
-        self.assertEqual(D(0), as_json["margin_free"])
+        self.assertEqual(D(0), as_json["margin_available"])
         self.assertEqual(D(0), as_json["margin_borrowed"])
 
 @unittest.skipUnless("unit" in limits, "Unit skipped")
@@ -583,77 +580,69 @@ class MarketTest(WebMockTestCase):
         m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True)
         self.assertEqual(True, m.debug)
 
-    def test_get_ticker(self):
-        m = market.Market(self.ccxt)
-        self.ccxt.fetch_ticker.side_effect = [
-                { "bid": 1, "ask": 3 },
-                market.ExchangeError("foo"),
-                { "bid": 10, "ask": 40 },
-                market.ExchangeError("foo"),
-                market.ExchangeError("foo"),
+    def test_get_tickers(self):
+        self.ccxt.fetch_tickers.side_effect = [
+                "tickers",
+                market.NotSupported
                 ]
 
-        ticker = m.get_ticker("ETH", "ETC")
-        self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
-        self.assertEqual(1, ticker["bid"])
-        self.assertEqual(3, ticker["ask"])
-        self.assertEqual(2, ticker["average"])
-        self.assertFalse(ticker["inverted"])
-
-        ticker = m.get_ticker("ETH", "XVG")
-        self.assertEqual(0.0625, ticker["average"])
-        self.assertTrue(ticker["inverted"])
-        self.assertIn("original", ticker)
-        self.assertEqual(10, ticker["original"]["bid"])
-
-        ticker = m.get_ticker("XVG", "XMR")
-        self.assertIsNone(ticker)
-
-        self.ccxt.fetch_ticker.assert_has_calls([
-            mock.call("ETH/ETC"),
-            mock.call("ETH/XVG"),
-            mock.call("XVG/ETH"),
-            mock.call("XVG/XMR"),
-            mock.call("XMR/XVG"),
-            ])
+        m = market.Market(self.ccxt)
+        self.assertEqual("tickers", m.get_tickers())
+        self.assertEqual("tickers", m.get_tickers())
+        self.ccxt.fetch_tickers.assert_called_once()
 
-        self.ccxt = mock.Mock(spec=market.ccxt.poloniexE)
-        m1b = market.Market(self.ccxt)
-        m1b.get_ticker("ETH", "ETC")
-        self.ccxt.fetch_ticker.assert_not_called()
-
-        self.ccxt = mock.Mock(spec=market.ccxt.poloniex)
-        m2 = market.Market(self.ccxt)
-        self.ccxt.fetch_ticker.side_effect = [
-                { "bid": 1, "ask": 3 },
-                { "bid": 1.2, "ask": 3.5 },
-                ]
-        ticker1 = m2.get_ticker("ETH", "ETC")
-        ticker2 = m2.get_ticker("ETH", "ETC")
-        ticker3 = m2.get_ticker("ETC", "ETH")
-        self.ccxt.fetch_ticker.assert_called_once_with("ETH/ETC")
-        self.assertEqual(1, ticker1["bid"])
-        self.assertDictEqual(ticker1, ticker2)
-        self.assertDictEqual(ticker1, ticker3["original"])
-
-        ticker4 = m2.get_ticker("ETH", "ETC", refresh=True)
-        ticker5 = m2.get_ticker("ETH", "ETC")
-        self.assertEqual(1.2, ticker4["bid"])
-        self.assertDictEqual(ticker4, ticker5)
-
-        self.ccxt = mock.Mock(spec=market.ccxt.binance)
-        m3 = market.Market(self.ccxt)
-        self.ccxt.fetch_ticker.side_effect = [
-                { "bid": 1, "ask": 3 },
-                { "bid": 1.2, "ask": 3.5 },
-                ]
-        ticker6 = m3.get_ticker("ETH", "ETC")
-        m3.ticker_cache_timestamp -= 4
-        ticker7 = m3.get_ticker("ETH", "ETC")
-        m3.ticker_cache_timestamp -= 2
-        ticker8 = m3.get_ticker("ETH", "ETC")
-        self.assertDictEqual(ticker6, ticker7)
-        self.assertEqual(1.2, ticker8["bid"])
+        self.assertIsNone(m.get_tickers(refresh=self.time.time()))
+
+    def test_get_ticker(self):
+        with self.subTest(get_tickers=True):
+            self.ccxt.fetch_tickers.return_value = {
+                    "ETH/ETC": { "bid": 1, "ask": 3 },
+                    "XVG/ETH": { "bid": 10, "ask": 40 },
+                    }
+            m = market.Market(self.ccxt)
+
+            ticker = m.get_ticker("ETH", "ETC")
+            self.assertEqual(1, ticker["bid"])
+            self.assertEqual(3, ticker["ask"])
+            self.assertEqual(2, ticker["average"])
+            self.assertFalse(ticker["inverted"])
+
+            ticker = m.get_ticker("ETH", "XVG")
+            self.assertEqual(0.0625, ticker["average"])
+            self.assertTrue(ticker["inverted"])
+            self.assertIn("original", ticker)
+            self.assertEqual(10, ticker["original"]["bid"])
+
+            ticker = m.get_ticker("XVG", "XMR")
+            self.assertIsNone(ticker)
+
+        with self.subTest(get_tickers=False):
+            self.ccxt.fetch_tickers.return_value = None
+            self.ccxt.fetch_ticker.side_effect = [
+                    { "bid": 1, "ask": 3 },
+                    market.ExchangeError("foo"),
+                    { "bid": 10, "ask": 40 },
+                    market.ExchangeError("foo"),
+                    market.ExchangeError("foo"),
+                    ]
+
+            m = market.Market(self.ccxt)
+
+            ticker = m.get_ticker("ETH", "ETC")
+            self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
+            self.assertEqual(1, ticker["bid"])
+            self.assertEqual(3, ticker["ask"])
+            self.assertEqual(2, ticker["average"])
+            self.assertFalse(ticker["inverted"])
+
+            ticker = m.get_ticker("ETH", "XVG")
+            self.assertEqual(0.0625, ticker["average"])
+            self.assertTrue(ticker["inverted"])
+            self.assertIn("original", ticker)
+            self.assertEqual(10, ticker["original"]["bid"])
+
+            ticker = m.get_ticker("XVG", "XMR")
+            self.assertIsNone(ticker)
 
     def test_fetch_fees(self):
         m = market.Market(self.ccxt)
@@ -906,9 +895,9 @@ class MarketTest(WebMockTestCase):
                 trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
 
                 m.trades.all = [trade1, trade2, trade3]
-                balance1 = portfolio.Balance("BTC", { "margin_free": "0" })
-                balance2 = portfolio.Balance("USDT", { "margin_free": "100" })
-                balance3 = portfolio.Balance("ETC", { "margin_free": "10" })
+                balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
+                balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
+                balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
                 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
 
                 m.move_balances()
@@ -921,8 +910,8 @@ class MarketTest(WebMockTestCase):
                     self.assertEqual(3, m.report.log_debug_action.call_count)
                 else:
                     self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin")
-                    self.ccxt.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange")
-                    self.ccxt.transfer_balance.assert_any_call("ETC", 10, "margin", "exchange")
+                    self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
+                    self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
   
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class TradeStoreTest(WebMockTestCase):
@@ -1001,16 +990,24 @@ class TradeStoreTest(WebMockTestCase):
 
         trade_mock1 = mock.Mock()
         trade_mock2 = mock.Mock()
+        trade_mock3 = mock.Mock()
 
         trade_mock1.prepare_order.return_value = 1
         trade_mock2.prepare_order.return_value = 2
+        trade_mock3.prepare_order.return_value = 3
+
+        trade_mock1.is_fullfiled = False
+        trade_mock2.is_fullfiled = False
+        trade_mock3.is_fullfiled = True
 
         trade_store.all.append(trade_mock1)
         trade_store.all.append(trade_mock2)
+        trade_store.all.append(trade_mock3)
 
         trade_store.prepare_orders()
         trade_mock1.prepare_order.assert_called_with(compute_value="default")
         trade_mock2.prepare_order.assert_called_with(compute_value="default")
+        trade_mock3.prepare_order.assert_not_called()
         self.m.report.log_orders.assert_called_once_with([1, 2], None, "default")
 
         self.m.report.log_orders.reset_mock()
@@ -1108,6 +1105,21 @@ class TradeStoreTest(WebMockTestCase):
             order_mock2.get_status.assert_called()
             order_mock3.get_status.assert_called()
 
+    def test_pending(self):
+        trade_mock1 = mock.Mock()
+        trade_mock1.is_fullfiled = False
+        trade_mock2 = mock.Mock()
+        trade_mock2.is_fullfiled = False
+        trade_mock3 = mock.Mock()
+        trade_mock3.is_fullfiled = True
+
+        trade_store = market.TradeStore(self.m)
+
+        trade_store.all.append(trade_mock1)
+        trade_store.all.append(trade_mock2)
+        trade_store.all.append(trade_mock3)
+
+        self.assertEqual([trade_mock1, trade_mock2], trade_store.pending)
 
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class BalanceStoreTest(WebMockTestCase):
@@ -1301,13 +1313,16 @@ class TradeTest(WebMockTestCase):
         self.assertEqual(self.m, trade.market)
 
         with self.assertRaises(AssertionError):
-            portfolio.Trade(value_from, value_to, "ETC", self.m)
+            portfolio.Trade(value_from, -value_to, "ETH", self.m)
         with self.assertRaises(AssertionError):
-            value_from.linked_to = None
-            portfolio.Trade(value_from, value_to, "ETH", self.m)
+            portfolio.Trade(value_from, value_to, "ETC", self.m)
         with self.assertRaises(AssertionError):
             value_from.currency = "ETH"
             portfolio.Trade(value_from, value_to, "ETH", self.m)
+            value_from.currency = "BTC"
+        with self.assertRaises(AssertionError):
+            value_from2 = portfolio.Amount("BTC", "1.0")
+            portfolio.Trade(value_from2, value_to, "ETH", self.m)
 
         value_from = portfolio.Amount("BTC", 0)
         trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
@@ -1374,6 +1389,28 @@ class TradeTest(WebMockTestCase):
 
         self.assertEqual("short", trade.trade_type)
 
+    def test_is_fullfiled(self):
+        value_from = portfolio.Amount("BTC", "0.5")
+        value_from.linked_to = portfolio.Amount("ETH", "10.0")
+        value_to = portfolio.Amount("BTC", "1.0")
+        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
+
+        order1 = mock.Mock()
+        order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
+
+        order2 = mock.Mock()
+        order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
+        trade.orders.append(order1)
+        trade.orders.append(order2)
+
+        self.assertFalse(trade.is_fullfiled)
+
+        order3 = mock.Mock()
+        order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
+        trade.orders.append(order3)
+
+        self.assertTrue(trade.is_fullfiled)
+
     def test_filled_amount(self):
         value_from = portfolio.Amount("BTC", "0.5")
         value_from.linked_to = portfolio.Amount("ETH", "10.0")
@@ -1588,7 +1625,7 @@ class TradeTest(WebMockTestCase):
         self.assertEqual(2, self.m.report.log_order.call_count)
         calls = [
                 mock.call(order_mock, 2, update="adjusting",
-                    compute_value='lambda x, y: (x[y] + x["average"]) / 2',
+                    compute_value=mock.ANY,
                     new_order=new_order_mock),
                 mock.call(order_mock, 2, new_order=new_order_mock),
                 ]
@@ -1607,7 +1644,7 @@ class TradeTest(WebMockTestCase):
         self.m.report.log_order.assert_called()
         calls = [
                 mock.call(order_mock, 5, update="adjusting",
-                    compute_value='lambda x, y: (x[y]*2 + x["average"]) / 3',
+                    compute_value=mock.ANY,
                     new_order=new_order_mock),
                 mock.call(order_mock, 5, new_order=new_order_mock),
                 ]
@@ -1831,7 +1868,7 @@ class OrderTest(WebMockTestCase):
 
         order.cancel()
         self.m.ccxt.cancel_order.assert_called_with(42)
-        fetch.assert_called_once_with(force=True)
+        fetch.assert_called_once_with()
         self.m.report.log_debug_action.assert_not_called()
 
     def test_dust_amount_remaining(self):
@@ -1850,11 +1887,9 @@ class OrderTest(WebMockTestCase):
                 D("0.1"), "BTC", "long", self.m, "trade")
 
         self.assertEqual(9, order.remaining_amount().value)
-        order.fetch.assert_not_called()
 
         order.status = "open"
         self.assertEqual(9, order.remaining_amount().value)
-        fetch.assert_called_once()
 
     @mock.patch.object(portfolio.Order, "fetch")
     def test_filled_amount(self, fetch):
@@ -1957,61 +1992,38 @@ class OrderTest(WebMockTestCase):
 
     @mock.patch.object(portfolio.Order, "fetch_mouvements")
     def test_fetch(self, fetch_mouvements):
-        time = self.time.time()
-        with mock.patch.object(portfolio.time, "time") as time_mock:
-            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
-                    D("0.1"), "BTC", "long", self.m, "trade")
-            order.id = 45
-            with self.subTest(debug=True):
-                self.m.debug = True
-                order.fetch()
-                time_mock.assert_not_called()
-                self.m.report.log_debug_action.assert_called_once()
-                self.m.report.log_debug_action.reset_mock()
-                order.fetch(force=True)
-                time_mock.assert_not_called()
-                self.m.ccxt.fetch_order.assert_not_called()
-                fetch_mouvements.assert_not_called()
-                self.m.report.log_debug_action.assert_called_once()
-                self.m.report.log_debug_action.reset_mock()
-            self.assertIsNone(order.fetch_cache_timestamp)
-
-            with self.subTest(debug=False):
-                self.m.debug = False
-                time_mock.return_value = time
-                self.m.ccxt.fetch_order.return_value = {
-                        "status": "foo",
-                        "datetime": "timestamp"
-                        }
-                order.fetch()
-
-                self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
-                fetch_mouvements.assert_called_once()
-                self.assertEqual("foo", order.status)
-                self.assertEqual("timestamp", order.timestamp)
-                self.assertEqual(time, order.fetch_cache_timestamp)
-                self.assertEqual(1, len(order.results))
-
-                self.m.ccxt.fetch_order.reset_mock()
-                fetch_mouvements.reset_mock()
-
-                time_mock.return_value = time + 8
-                order.fetch()
-                self.m.ccxt.fetch_order.assert_not_called()
-                fetch_mouvements.assert_not_called()
+        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+                D("0.1"), "BTC", "long", self.m, "trade")
+        order.id = 45
+        with self.subTest(debug=True):
+            self.m.debug = True
+            order.fetch()
+            self.m.report.log_debug_action.assert_called_once()
+            self.m.report.log_debug_action.reset_mock()
+            self.m.ccxt.fetch_order.assert_not_called()
+            fetch_mouvements.assert_not_called()
 
-                order.fetch(force=True)
-                self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
-                fetch_mouvements.assert_called_once()
+        with self.subTest(debug=False):
+            self.m.debug = False
+            self.m.ccxt.fetch_order.return_value = {
+                    "status": "foo",
+                    "datetime": "timestamp"
+                    }
+            order.fetch()
 
-                self.m.ccxt.fetch_order.reset_mock()
-                fetch_mouvements.reset_mock()
+            self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
+            fetch_mouvements.assert_called_once()
+            self.assertEqual("foo", order.status)
+            self.assertEqual("timestamp", order.timestamp)
+            self.assertEqual(1, len(order.results))
+            self.m.report.log_debug_action.assert_not_called()
 
-                time_mock.return_value = time + 19
+            with self.subTest(missing_order=True):
+                self.m.ccxt.fetch_order.side_effect = [
+                        portfolio.OrderNotCached,
+                        ]
                 order.fetch()
-                self.m.ccxt.fetch_order.assert_called_once_with(45, symbol="ETH")
-                fetch_mouvements.assert_called_once()
-                self.m.report.log_debug_action.assert_not_called()
+                self.assertEqual("closed_unknown", order.status)
 
     @mock.patch.object(portfolio.Order, "fetch")
     @mock.patch.object(portfolio.Order, "mark_finished_order")
@@ -2315,6 +2327,25 @@ class ReportStoreTest(WebMockTestCase):
             'total': D('10.3')
             })
 
+        add_log.reset_mock()
+        compute_value = lambda x: x["bid"]
+        report_store.log_tickers(amounts, "BTC", compute_value, "total")
+        add_log.assert_called_once_with({
+            'type': 'tickers',
+            'compute_value': 'compute_value = lambda x: x["bid"]',
+            'balance_type': 'total',
+            'currency': 'BTC',
+            'balances': {
+                'BTC': D('10'),
+                'ETH': D('0.3')
+                },
+            'rates': {
+                'BTC': None,
+                'ETH': D('0.1')
+                },
+            'total': D('10.3')
+            })
+
     @mock.patch.object(market.ReportStore, "print_log")
     @mock.patch.object(market.ReportStore, "add_log")
     def test_log_dispatch(self, add_log, print_log):
@@ -2393,6 +2424,20 @@ class ReportStoreTest(WebMockTestCase):
             'orders': ['order1', 'order2']
             })
 
+        add_log.reset_mock()
+        def compute_value(x, y):
+            return x[y]
+        report_store.log_orders(orders, tick="tick",
+                only="only", compute_value=compute_value)
+        add_log.assert_called_with({
+            'type': 'orders',
+            'only': 'only',
+            'compute_value': 'def compute_value(x, y):\n            return x[y]',
+            'tick': 'tick',
+            'orders': ['order1', 'order2']
+            })
+
+
     @mock.patch.object(market.ReportStore, "print_log")
     @mock.patch.object(market.ReportStore, "add_log")
     def test_log_order(self, add_log, print_log):
@@ -2436,16 +2481,17 @@ class ReportStoreTest(WebMockTestCase):
         add_log.reset_mock()
         print_log.reset_mock()
         with self.subTest(update="adjusting"):
+            compute_value = lambda x: (x["bid"] + x["ask"]*2)/3
             report_store.log_order(order_mock, 3,
                     update="adjusting", new_order=new_order_mock,
-                    compute_value="default")
+                    compute_value=compute_value)
             print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock")
             add_log.assert_called_once_with({
                 'type': 'order',
                 'tick': 3,
                 'update': 'adjusting',
                 'order': 'order',
-                'compute_value': "default",
+                'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3',
                 'new_order': 'new_order'
                 })