+ def mark_disappeared_order(self):
+ if self.status.startswith("closed") and \
+ len(self.mouvements) > 0 and \
+ self.mouvements[-1].total_in_base == 0:
+ self.status = "error_disappeared"
+
+ def mark_finished_order(self):
+ if self.status.startswith("closed") and self.market.debug:
+ self.market.report.log_debug_action("Mark {} as finished".format(self))
+ return
+ if self.status.startswith("closed"):
+ 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):
+ if self.market.debug:
+ self.market.report.log_debug_action("Fetching {}".format(self))
+ return
+ try:
+ result = self.market.ccxt.fetch_order(self.id)
+ 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.fetch_mouvements()
+
+ self.mark_disappeared_order()
+
+ self.mark_finished_order()
+ # FIXME: consider open order with dust remaining as closed
+
+ def dust_amount_remaining(self):
+ return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
+
+ def remaining_amount(self):
+ return self.amount - self.filled_amount()
+
+ def filled_amount(self, in_base_currency=False):
+ if self.status == "open":
+ self.fetch()
+ filled_amount = 0
+ for mouvement in self.mouvements:
+ if in_base_currency:
+ filled_amount += mouvement.total_in_base
+ else:
+ filled_amount += mouvement.total
+ return filled_amount
+
+ def fetch_mouvements(self):
+ try:
+ mouvements = self.market.ccxt.privatePostReturnOrderTrades({"orderNumber": self.id})
+ except ExchangeError:
+ mouvements = []
+ self.mouvements = []
+
+ for mouvement_hash in mouvements:
+ self.mouvements.append(Mouvement(self.amount.currency,
+ self.base_currency, mouvement_hash))
+ self.mouvements.sort(key= lambda x: x.date)
+
+ def cancel(self):
+ if self.market.debug:
+ self.market.report.log_debug_action("Mark {} as cancelled".format(self))
+ self.status = "canceled"
+ return
+ if self.open and self.id is not None:
+ try:
+ self.market.ccxt.cancel_order(self.id)
+ except OrderNotFound as e: # Closed inbetween
+ self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e)
+ self.fetch()
+
+ def retrieve_order(self):
+ symbol = "{}/{}".format(self.amount.currency, self.base_currency)
+ amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
+ start_timestamp = self.start_date.timestamp() - 5
+
+ similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp)
+ for order in similar_open_orders:
+ if (order["info"]["margin"] == 1 and self.account == "exchange") or\
+ (order["info"]["margin"] != 1 and self.account == "margin"):
+ i_m_tested = True # coverage bug ?!
+ continue
+ if order["info"]["side"] != self.action:
+ continue
+ amount_diff = round(
+ abs(D(order["info"]["startingAmount"]) - amount),
+ self.market.ccxt.order_precision(symbol))
+ rate_diff = round(
+ abs(D(order["info"]["rate"]) - self.rate),
+ self.market.ccxt.order_precision(symbol))
+ if amount_diff != 0 or rate_diff != 0:
+ continue
+ self.results.append({"id": order["id"]})
+ return True
+
+ similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp)
+ # FIXME: use set instead of sorted(list(...))
+ for order_id in sorted(list(map(lambda x: x["order"], similar_trades))):
+ trades = list(filter(lambda x: x["order"] == order_id, similar_trades))
+ if any(x["timestamp"] < start_timestamp for x in trades):
+ continue
+ if any(x["side"] != self.action for x in trades):
+ continue
+ if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\
+ any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades):
+ continue
+ trade_sum = sum(D(x["info"]["amount"]) for x in trades)
+ amount_diff = round(abs(trade_sum - amount),
+ self.market.ccxt.order_precision(symbol))
+ if amount_diff != 0:
+ continue
+ if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\
+ (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)):
+ continue
+ self.results.append({"id": order_id})
+ return True
+
+ return False
+
+class Mouvement:
+ def __init__(self, currency, base_currency, hash_):
+ self.currency = currency
+ self.base_currency = base_currency
+ self.id = hash_.get("tradeID")
+ self.action = hash_.get("type")
+ self.fee_rate = D(hash_.get("fee", -1))
+ try:
+ self.date = datetime.strptime(hash_.get("date", ""), '%Y-%m-%d %H:%M:%S')
+ except ValueError:
+ self.date = None
+ self.rate = D(hash_.get("rate", 0))
+ self.total = Amount(currency, hash_.get("amount", 0))
+ # rate * total = total_in_base
+ self.total_in_base = Amount(base_currency, hash_.get("total", 0))
+
+ def as_json(self):
+ return {
+ "fee_rate": self.fee_rate,
+ "date": self.date,
+ "action": self.action,
+ "total": self.total.value,
+ "currency": self.currency,
+ "total_in_base": self.total_in_base.value,
+ "base_currency": self.base_currency
+ }
+
+ def __repr__(self):
+ if self.fee_rate > 0:
+ fee_rate = " fee: {}%".format(self.fee_rate * 100)
+ else:
+ fee_rate = ""
+ if self.date is None:
+ date = "No date"
+ else:
+ date = self.date
+ return "Mouvement({} ; {} {} ({}){})".format(
+ date, self.action, self.total, self.total_in_base,
+ fee_rate)
+