def nanoseconds():
return int(time.time() * 1000000000)
+ def is_dust_trade(self, amount, rate):
+ if abs(amount) < decimal.Decimal("0.000001"):
+ return True
+ if abs(amount * rate) < decimal.Decimal("0.0001"):
+ return True
+ return False
+
def fetch_margin_balance(self):
"""
portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
return all_balances
- def create_exchange_order(self, symbol, type, side, amount, price=None, params={}):
- return super().create_order(symbol, type, side, amount, price=price, params=params)
-
- def create_margin_order(self, symbol, type, side, amount, price=None, lending_rate=None, params={}):
- if type == 'market':
- raise ExchangeError(self.id + ' allows limit orders only')
- self.load_markets()
- method = 'privatePostMargin' + self.capitalize(side)
- market = self.market(symbol)
- price = float(price)
- amount = float(amount)
- if lending_rate is not None:
- params = self.extend({"lendingRate": lending_rate}, params)
- response = getattr(self, method)(self.extend({
- 'currencyPair': market['id'],
- 'rate': self.price_to_precision(symbol, price),
- 'amount': self.amount_to_precision(symbol, amount),
- }, params))
- timestamp = self.milliseconds()
- order = self.parse_order(self.extend({
- 'timestamp': timestamp,
- 'status': 'open',
- 'type': type,
- 'side': side,
- 'price': price,
- 'amount': amount,
- }, response), market)
- id = order['id']
- self.orders[id] = order
- return self.extend({'info': response}, order)
-
def order_precision(self, symbol):
- return 8
+ self.load_markets()
+ return self.markets[symbol]['precision']['price']
def transfer_balance(self, currency, amount, from_account, to_account):
result = self.privatePostTransferBalance({
def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}):
"""
- Wrapped to handle margin and exchange accounts
+ Wrapped to handle margin and exchange accounts, and get decimals
"""
+ if type == 'market':
+ raise ExchangeError(self.id + ' allows limit orders only')
+ self.load_markets()
if account == "exchange":
- return self.create_exchange_order(symbol, type, side, amount, price=price, params=params)
+ method = 'privatePost' + self.capitalize(side)
elif account == "margin":
- return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params)
+ method = 'privatePostMargin' + self.capitalize(side)
+ if lending_rate is not None:
+ params = self.extend({"lendingRate": lending_rate}, params)
else:
raise NotImplementedError
+ market = self.market(symbol)
+ response = getattr(self, method)(self.extend({
+ 'currencyPair': market['id'],
+ 'rate': self.price_to_precision(symbol, price),
+ 'amount': self.amount_to_precision(symbol, amount),
+ }, params))
+ timestamp = self.milliseconds()
+ order = self.parse_order(self.extend({
+ 'timestamp': timestamp,
+ 'status': 'open',
+ 'type': type,
+ 'side': side,
+ 'price': price,
+ 'amount': amount,
+ }, response), market)
+ id = order['id']
+ self.orders[id] = order
+ return self.extend({'info': response}, order)
+
+ def price_to_precision(self, symbol, price):
+ """
+ Wrapped to avoid float
+ """
+ return ('{:.' + str(self.markets[symbol]['precision']['price']) + 'f}').format(price).rstrip("0").rstrip(".")
+
+ def amount_to_precision(self, symbol, amount):
+ """
+ Wrapped to avoid float
+ """
+ return ('{:.' + str(self.markets[symbol]['precision']['amount']) + 'f}').format(amount).rstrip("0").rstrip(".")
def common_currency_code(self, currency):
"""
if debug:
args.append("--debug")
args = parse_args(args)
- pg_config = parse_config(args)
+ pg_config, redis_config = parse_config(args)
market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0]
return market.Market.from_config(market_config, args,
pg_config=pg_config, market_id=market_id,
process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"])
self.market.report.log_stage("{}_begin".format(process_name))
if "begin" in step.get("fetch_balances", []):
- self.market.balances.fetch_balances(tag="{}_begin".format(process_name))
+ self.market.balances.fetch_balances(tag="{}_begin".format(process_name), log_tickers=True)
for action in self.ordered_actions:
if action in step:
self.run_action(action, step[action], kwargs)
if "end" in step.get("fetch_balances", []):
- self.market.balances.fetch_balances(tag="{}_end".format(process_name))
+ self.market.balances.fetch_balances(tag="{}_end".format(process_name), log_tickers=True)
self.market.report.log_stage("{}_end".format(process_name))
def method_arguments(self, action):
ticker = ticker["original"]
rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value)
- # FIXME: Dust amount should be removed from there if they werent
- # honored in other sales
delta_in_base = abs(self.delta)
# 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
self.fetch_mouvements()
self.mark_disappeared_order()
-
+ self.mark_dust_amount_remaining_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 mark_dust_amount_remaining_order(self):
+ if self.market.ccxt.is_dust_trade(self.remaining_amount().value, self.rate):
+ self.status = "closed_dust_remaining"
def remaining_amount(self):
return self.amount - self.filled_amount()
self.market.report.log_debug_action("Mark {} as cancelled".format(self))
self.status = "canceled"
return
- if self.open and self.id is not None:
+ if (self.status == "closed_dust_remaining" or self.open) and self.id is not None:
try:
self.market.ccxt.cancel_order(self.id)
except OrderNotFound as e: # Closed inbetween
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):
"args": args,
})
- def log_balances(self, tag=None):
+ def log_balances(self, tag=None, tickers=None,
+ ticker_currency=None, compute_value=None, type=None):
self.print_log("[Balance]")
for currency, balance in self.market.balances.all.items():
self.print_log("\t{}".format(balance))
"balances": self.market.balances.as_json()
}
+ if tickers is not None:
+ log["tickers"] = self._ticker_hash(tickers, ticker_currency,
+ compute_value, type)
+
self.add_log(log.copy())
self.add_redis_status(log)
def log_tickers(self, amounts, other_currency,
compute_value, type):
+ log = self._ticker_hash(amounts, other_currency, compute_value,
+ type)
+ log["type"] = "tickers"
+
+ self.add_log(log)
+
+ def _ticker_hash(self, amounts, other_currency, compute_value, type):
values = {}
rates = {}
if callable(compute_value):
for currency, amount in amounts.items():
values[currency] = amount.as_json()["value"]
rates[currency] = amount.rate
- log = {
- "type": "tickers",
+ return {
"compute_value": compute_value,
"balance_type": type,
"currency": other_currency,
"total": sum(amounts.values()).as_json()["value"]
}
- self.add_log(log.copy())
- self.add_redis_status(log)
-
def log_dispatch(self, amount, amounts, liquidity, repartition):
self.add_log({
"type": "dispatch",
compute_value, type)
return amounts
- def fetch_balances(self, tag=None):
+ def fetch_balances(self, tag=None, log_tickers=False,
+ ticker_currency="BTC", ticker_compute_value="average", ticker_type="total"):
all_balances = self.market.ccxt.fetch_all_balances()
for currency, balance in all_balances.items():
if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
currency in self.all:
self.all[currency] = portfolio.Balance(currency, balance)
- self.market.report.log_balances(tag=tag)
+ if log_tickers:
+ tickers = self.in_currency(ticker_currency, compute_value=ticker_compute_value, type=ticker_type)
+ self.market.report.log_balances(tag=tag,
+ tickers=tickers, ticker_currency=ticker_currency,
+ compute_value=ticker_compute_value, type=ticker_type)
+ else:
+ self.market.report.log_balances(tag=tag)
def dispatch_assets(self, amount, liquidity="medium", repartition=None):
if repartition is None:
retry_call.assert_not_called()
def test_order_precision(self):
- self.assertEqual(8, self.s.order_precision("FOO"))
+ self.s.markets = {
+ "FOO": {
+ "precision": {
+ "price": 5,
+ "amount": 6,
+ }
+ },
+ "BAR": {
+ "precision": {
+ "price": 7,
+ "amount": 8,
+ }
+ }
+ }
+ with mock.patch.object(self.s, "load_markets") as load_markets:
+ self.assertEqual(5, self.s.order_precision("FOO"))
+ load_markets.assert_called_once()
def test_transfer_balance(self):
with self.subTest(success=True),\
}
self.assertEqual(expected, self.s.margin_summary())
- def test_create_order(self):
- with mock.patch.object(self.s, "create_exchange_order") as exchange,\
- mock.patch.object(self.s, "create_margin_order") as margin:
- with self.subTest(account="unspecified"):
- self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params")
- exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
- margin.assert_not_called()
- exchange.reset_mock()
- margin.reset_mock()
-
- with self.subTest(account="exchange"):
- self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params")
- exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
- margin.assert_not_called()
- exchange.reset_mock()
- margin.reset_mock()
-
- with self.subTest(account="margin"):
- self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params")
- margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params")
- exchange.assert_not_called()
- exchange.reset_mock()
- margin.reset_mock()
-
- with self.subTest(account="unknown"), self.assertRaises(NotImplementedError):
- self.s.create_order("symbol", "type", "side", "amount", account="unknown")
-
def test_parse_ticker(self):
ticker = {
"high24hr": "12",
self.assertEqual(expected_doge, result["DOGE"])
self.assertEqual(expected_btc, result["BTC"])
- def test_create_margin_order(self):
- with self.assertRaises(market.ExchangeError):
- self.s.create_margin_order("FOO", "market", "buy", "10")
+ def test_create_order(self):
+ with self.subTest(type="market"),\
+ self.assertRaises(market.ExchangeError):
+ self.s.create_order("FOO", "market", "buy", "10")
- with mock.patch.object(self.s, "load_markets") as load_markets,\
+ with self.subTest(type="limit", account="margin"),\
+ mock.patch.object(self.s, "load_markets") as load_markets,\
mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\
mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\
mock.patch.object(self.s, "market") as market_mock,\
ptp.return_value = D("0.1")
atp.return_value = D("12")
- order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1")
+ order = self.s.create_order("BTC_ETC", "limit", "buy", "12",
+ account="margin", price="0.1")
self.assertEqual(123, order["id"])
margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
margin_sell.assert_not_called()
margin_buy.reset_mock()
margin_sell.reset_mock()
- order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1")
+ order = self.s.create_order("BTC_ETC", "limit", "sell",
+ "12", account="margin", lending_rate="0.01", price="0.1")
self.assertEqual(456, order["id"])
margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"})
margin_buy.assert_not_called()
- def test_create_exchange_order(self):
- with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order:
- self.s.create_order("symbol", "type", "side", "amount", price="price", params="params")
+ with self.subTest(type="limit", account="exchange"),\
+ mock.patch.object(self.s, "load_markets") as load_markets,\
+ mock.patch.object(self.s, "privatePostBuy") as exchange_buy,\
+ mock.patch.object(self.s, "privatePostSell") as exchange_sell,\
+ mock.patch.object(self.s, "market") as market_mock,\
+ mock.patch.object(self.s, "price_to_precision") as ptp,\
+ mock.patch.object(self.s, "amount_to_precision") as atp:
+
+ exchange_buy.return_value = {
+ "orderNumber": 123
+ }
+ exchange_sell.return_value = {
+ "orderNumber": 456
+ }
+ market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" }
+ ptp.return_value = D("0.1")
+ atp.return_value = D("12")
+
+ order = self.s.create_order("BTC_ETC", "limit", "buy", "12",
+ account="exchange", price="0.1")
+ self.assertEqual(123, order["id"])
+ exchange_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
+ exchange_sell.assert_not_called()
+ exchange_buy.reset_mock()
+ exchange_sell.reset_mock()
- create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
+ order = self.s.create_order("BTC_ETC", "limit", "sell",
+ "12", account="exchange", lending_rate="0.01", price="0.1")
+ self.assertEqual(456, order["id"])
+ exchange_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
+ exchange_buy.assert_not_called()
+
+ with self.subTest(account="unknown"), self.assertRaises(NotImplementedError),\
+ mock.patch.object(self.s, "load_markets") as load_markets:
+ self.s.create_order("symbol", "type", "side", "amount", account="unknown")
def test_common_currency_code(self):
self.assertEqual("FOO", self.s.common_currency_code("FOO"))
def test_currency_id(self):
self.assertEqual("FOO", self.s.currency_id("FOO"))
+
+ def test_amount_to_precision(self):
+ self.s.markets = {
+ "FOO": {
+ "precision": {
+ "price": 5,
+ "amount": 6,
+ }
+ },
+ "BAR": {
+ "precision": {
+ "price": 7,
+ "amount": 8,
+ }
+ }
+ }
+ self.assertEqual("0.0001", self.s.amount_to_precision("FOO", D("0.0001")))
+ self.assertEqual("0.0000001", self.s.amount_to_precision("BAR", D("0.0000001")))
+ self.assertEqual("0.000001", self.s.amount_to_precision("FOO", D("0.000001")))
+
+ def test_price_to_precision(self):
+ self.s.markets = {
+ "FOO": {
+ "precision": {
+ "price": 5,
+ "amount": 6,
+ }
+ },
+ "BAR": {
+ "precision": {
+ "price": 7,
+ "amount": 8,
+ }
+ }
+ }
+ self.assertEqual("0.0001", self.s.price_to_precision("FOO", D("0.0001")))
+ self.assertEqual("0.0000001", self.s.price_to_precision("BAR", D("0.0000001")))
+ self.assertEqual("0", self.s.price_to_precision("FOO", D("0.000001")))
+
+ def test_is_dust_trade(self):
+ self.assertTrue(self.s.is_dust_trade(D("0.0000009"), D("1000")))
+ self.assertTrue(self.s.is_dust_trade(D("0.000001"), D("10")))
+ self.assertFalse(self.s.is_dust_trade(D("0.000001"), D("100")))
mock.patch("main.parse_config") as main_parse_config:
with self.subTest(debug=False):
main_parse_args.return_value = self.market_args()
- main_parse_config.return_value = "pg_config"
+ main_parse_config.return_value = ["pg_config", "redis_config"]
main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
m = main.get_user_market("config_path.ini", 1)
main_parse_args.reset_mock()
with self.subTest(debug=True):
main_parse_args.return_value = self.market_args(debug=True)
- main_parse_config.return_value = "pg_config"
+ main_parse_config.return_value = ["pg_config", "redis_config"]
main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
m = main.get_user_market("config_path.ini", 1, debug=True)
mock.call("process_foo__1_sell_end"),
])
self.m.balances.fetch_balances.assert_has_calls([
- mock.call(tag="process_foo__1_sell_begin"),
- mock.call(tag="process_foo__1_sell_end"),
+ mock.call(tag="process_foo__1_sell_begin", log_tickers=True),
+ mock.call(tag="process_foo__1_sell_end", log_tickers=True),
])
self.assertEqual(5, run_action.call_count)
order.cancel()
self.m.ccxt.cancel_order.assert_not_called()
- def test_dust_amount_remaining(self):
+ def test_mark_dust_amount_remaining(self):
order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
D("0.1"), "BTC", "long", self.m, "trade")
- order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", 1))
- self.assertFalse(order.dust_amount_remaining())
+ self.m.ccxt.is_dust_trade.return_value = False
+ order.mark_dust_amount_remaining_order()
+ self.assertEqual("pending", order.status)
- order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", D("0.0001")))
- self.assertTrue(order.dust_amount_remaining())
+ self.m.ccxt.is_dust_trade.return_value = True
+ order.mark_dust_amount_remaining_order()
+ self.assertEqual("closed_dust_remaining", order.status)
@mock.patch.object(portfolio.Order, "fetch")
@mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1))
"status": "foo",
"datetime": "timestamp"
}
+ self.m.ccxt.is_dust_trade.return_value = False
order.fetch()
self.m.ccxt.fetch_order.assert_called_once_with(45)
balance_store = market.BalanceStore(self.m)
- balance_store.fetch_balances()
- self.assertNotIn("ETC", balance_store.currencies())
- self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies()))
-
- balance_store.all["ETC"] = portfolio.Balance("ETC", {
- "exchange_total": "1", "exchange_free": "0",
- "exchange_used": "1" })
- balance_store.fetch_balances(tag="foo")
- self.assertEqual(0, balance_store.all["ETC"].total)
- self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
- self.m.report.log_balances.assert_called_with(tag="foo")
+ with self.subTest(log_tickers=False):
+ balance_store.fetch_balances()
+ self.assertNotIn("ETC", balance_store.currencies())
+ self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies()))
+
+ balance_store.all["ETC"] = portfolio.Balance("ETC", {
+ "exchange_total": "1", "exchange_free": "0",
+ "exchange_used": "1" })
+ balance_store.fetch_balances(tag="foo")
+ self.assertEqual(0, balance_store.all["ETC"].total)
+ self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
+ self.m.report.log_balances.assert_called_with(tag="foo")
+
+ with self.subTest(log_tickers=True),\
+ mock.patch.object(balance_store, "in_currency") as in_currency:
+ in_currency.return_value = "tickers"
+ balance_store.fetch_balances(log_tickers=True, ticker_currency="FOO",
+ ticker_compute_value="compute", ticker_type="type")
+ self.m.report.log_balances.assert_called_with(compute_value='compute',
+ tag=None, ticker_currency='FOO', tickers='tickers',
+ type='type')
@mock.patch.object(market.Portfolio, "repartition")
def test_dispatch_assets(self, repartition):
self.m.balances.as_json.return_value = "json"
self.m.balances.all = { "FOO": "bar", "BAR": "baz" }
- report_store.log_balances(tag="tag")
- print_log.assert_has_calls([
- mock.call("[Balance]"),
- mock.call("\tbar"),
- mock.call("\tbaz"),
- ])
- add_log.assert_called_once_with({
- 'type': 'balance',
- 'balances': 'json',
- 'tag': 'tag'
- })
- add_redis_status.assert_called_once_with({
- 'type': 'balance',
- 'balances': 'json',
- 'tag': 'tag'
- })
+ with self.subTest(tickers=None):
+ report_store.log_balances(tag="tag")
+ print_log.assert_has_calls([
+ mock.call("[Balance]"),
+ mock.call("\tbar"),
+ mock.call("\tbaz"),
+ ])
+ add_log.assert_called_once_with({
+ 'type': 'balance',
+ 'balances': 'json',
+ 'tag': 'tag'
+ })
+ add_redis_status.assert_called_once_with({
+ 'type': 'balance',
+ 'balances': 'json',
+ 'tag': 'tag'
+ })
+ add_log.reset_mock()
+ add_redis_status.reset_mock()
+ with self.subTest(tickers="present"):
+ amounts = {
+ "BTC": portfolio.Amount("BTC", 10),
+ "ETH": portfolio.Amount("BTC", D("0.3"))
+ }
+ amounts["ETH"].rate = D("0.1")
+
+ report_store.log_balances(tag="tag", tickers=amounts,
+ ticker_currency="BTC", compute_value="default",
+ type="total")
+ add_log.assert_called_once_with({
+ 'type': 'balance',
+ 'balances': 'json',
+ 'tag': 'tag',
+ 'tickers': {
+ 'compute_value': 'default',
+ 'balance_type': 'total',
+ 'currency': 'BTC',
+ 'balances': {
+ 'BTC': D('10'),
+ 'ETH': D('0.3')
+ },
+ 'rates': {
+ 'BTC': None,
+ 'ETH': D('0.1')
+ },
+ 'total': D('10.3')
+ },
+ })
+ add_redis_status.assert_called_once_with({
+ 'type': 'balance',
+ 'balances': 'json',
+ 'tag': 'tag',
+ 'tickers': {
+ 'compute_value': 'default',
+ '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")
- @mock.patch.object(market.ReportStore, "add_redis_status")
- def test_log_tickers(self, add_redis_status, add_log, print_log):
+ def test_log_tickers(self, add_log, print_log):
report_store = market.ReportStore(self.m)
amounts = {
"BTC": portfolio.Amount("BTC", 10),
},
'total': D('10.3')
})
- add_redis_status.assert_called_once_with({
- 'type': 'tickers',
- 'compute_value': 'default',
- 'balance_type': 'total',
- 'currency': 'BTC',
- 'balances': {
- 'BTC': D('10'),
- 'ETH': D('0.3')
- },
- 'rates': {
- 'BTC': None,
- 'ETH': D('0.1')
- },
- 'total': D('10.3')
- })
add_log.reset_mock()
compute_value = lambda x: x["bid"]