diff --git a/Evaluator/RealTime/instant_fluctuations_evaluator.py b/Evaluator/RealTime/instant_fluctuations_evaluator.py index 514204e95..a465cc1c8 100644 --- a/Evaluator/RealTime/instant_fluctuations_evaluator.py +++ b/Evaluator/RealTime/instant_fluctuations_evaluator.py @@ -31,7 +31,8 @@ import numpy as np -from config import * +from config import ExchangeConstantsOrderBookInfoColumns, CONFIG_REFRESH_RATE, PriceIndexes, CONFIG_TIME_FRAME, \ + START_PENDING_EVAL_NOTE from evaluator.RealTime.realtime_evaluator import RealTimeTAEvaluator """ @@ -148,7 +149,7 @@ def __init__(self, exchange, symbol): async def _refresh_data(self): self.last_candle_data = await self._get_data_from_exchange(self.specific_config[CONFIG_TIME_FRAME], - limit=20, return_list=False) + limit=20, return_list=False) async def eval_impl(self): self.eval_note = 0 @@ -190,7 +191,7 @@ def __init__(self, exchange, symbol): async def _refresh_data(self): new_data = await self._get_data_from_exchange(self.specific_config[CONFIG_TIME_FRAME], - limit=20, return_list=False) + limit=20, return_list=False) self.should_eval = not self._compare_data(new_data, self.last_candle_data) self.last_candle_data = new_data @@ -240,7 +241,7 @@ def __init__(self, exchange, symbol): async def _refresh_data(self): new_data = await self._get_data_from_exchange(self.specific_config[CONFIG_TIME_FRAME], - limit=20, return_list=False) + limit=20, return_list=False) self.should_eval = not self._compare_data(new_data, self.last_candle_data) self.last_candle_data = new_data @@ -274,3 +275,40 @@ def set_default_config(self): def _should_eval(self): return self.should_eval + + +class InstantMarketMakingEvaluator(RealTimeTAEvaluator): + def __init__(self, exchange, symbol): + super().__init__(exchange, symbol) + self.last_best_bid = None + self.last_best_ask = None + self.last_order_book_data = None + self.should_eval = True + + async def _refresh_data(self): + self.last_order_book_data = await self._get_order_book_from_exchange(limit=5) + + async def eval_impl(self): + self.eval_note = "" + best_bid = self.last_best_bid + best_ask = self.last_best_ask + + if self.last_order_book_data is not None: + best_bid = self.last_order_book_data[ExchangeConstantsOrderBookInfoColumns.BIDS.value][-1] + best_ask = self.last_order_book_data[ExchangeConstantsOrderBookInfoColumns.ASKS.value][-1] + + if self.last_best_ask != best_ask or self.last_best_bid != best_bid: + self.eval_note = { + ExchangeConstantsOrderBookInfoColumns.BIDS.value: best_bid, + ExchangeConstantsOrderBookInfoColumns.ASKS.value: best_ask + } + await self.notify_evaluator_task_managers(self.__class__.__name__) + self.last_best_ask = best_ask + self.last_best_bid = best_bid + + def set_default_config(self): + super().set_default_config() + self.specific_config[CONFIG_REFRESH_RATE] = 60 + + def _should_eval(self): + return self.should_eval diff --git a/Evaluator/Strategies/SimpleMarketMakingStrategiesEvaluator.json b/Evaluator/Strategies/SimpleMarketMakingStrategiesEvaluator.json new file mode 100644 index 000000000..448321eda --- /dev/null +++ b/Evaluator/Strategies/SimpleMarketMakingStrategiesEvaluator.json @@ -0,0 +1,4 @@ +{ + "required_time_frames" : ["1m"], + "required_evaluators" : ["InstantMarketMakingEvaluator"] +} \ No newline at end of file diff --git a/Evaluator/Strategies/market_making_startegy_evaluator.py b/Evaluator/Strategies/market_making_startegy_evaluator.py new file mode 100644 index 000000000..ed977ff53 --- /dev/null +++ b/Evaluator/Strategies/market_making_startegy_evaluator.py @@ -0,0 +1,46 @@ +""" +OctoBot Tentacle + +$tentacle_description: { + "name": "market_making_startegy_evaluator", + "type": "Evaluator", + "subtype": "Strategies", + "version": "1.1.0", + "requirements": ["instant_fluctuations_evaluator"], + "config_files": ["SimpleMarketMakingStrategiesEvaluator.json"], + "tests":["test_market_making_strategies_evaluator"] +} +""" + +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +from config import EvaluatorMatrixTypes +from evaluator.Strategies import MarketMakingStrategiesEvaluator +from tentacles.Evaluator.RealTime import InstantMarketMakingEvaluator + + +class SimpleMarketMakingStrategiesEvaluator(MarketMakingStrategiesEvaluator): + DESCRIPTION = "SimpleMarketMakingStrategiesEvaluator uses to pass up to date bid and ask price to MM TM" + + INSTANT_MM_CLASS_NAME = InstantMarketMakingEvaluator.get_name() + + async def eval_impl(self) -> None: + self.finalize() + + def finalize(self): + self.eval_note = self.matrix[EvaluatorMatrixTypes.REAL_TIME][ + SimpleMarketMakingStrategiesEvaluator.INSTANT_MM_CLASS_NAME] diff --git a/Trading/Mode/MarketMakerTradingMode.json b/Trading/Mode/MarketMakerTradingMode.json new file mode 100644 index 000000000..cc5e564fe --- /dev/null +++ b/Trading/Mode/MarketMakerTradingMode.json @@ -0,0 +1,5 @@ +{ + "delta_ask": 0.00001, + "delta_bid": 0.00001, + "required_strategies": ["SimpleMarketMakingStrategiesEvaluator"] +} \ No newline at end of file diff --git a/Trading/Mode/daily_trading_mode.py b/Trading/Mode/daily_trading_mode.py index b3afcbb8d..4522fcf37 100644 --- a/Trading/Mode/daily_trading_mode.py +++ b/Trading/Mode/daily_trading_mode.py @@ -40,7 +40,6 @@ class DailyTradingMode(AbstractTradingMode): - DESCRIPTION = "DailyTradingMode is a low risk trading mode adapted to flat markets." def __init__(self, config, exchange): @@ -248,10 +247,10 @@ async def create_new_order(self, eval_note, symbol, exchange, trader, portfolio, raise e except Exception as e: - get_logger(self.__class__.__name__).error(f"Failed to create order : {e}. " - f"Order: " - f"{current_order.get_string_info() if current_order else None}") - get_logger(self.__class__.__name__).exception(e) + self.logger.error(f"Failed to create order : {e}. " + f"Order: " + f"{current_order.get_string_info() if current_order else None}") + self.logger.exception(e) return None @@ -320,12 +319,12 @@ async def _set_state(self, new_state): # create notification if self.symbol_evaluator.matrices: await self.notifier.notify_alert( - self.final_eval, - self.symbol_evaluator.get_crypto_currency_evaluator(), - self.symbol_evaluator.get_symbol(), - self.symbol_evaluator.get_trader(self.exchange), - self.state, - self.symbol_evaluator.get_matrix(self.exchange).get_matrix()) - + self.final_eval, + self.symbol_evaluator.get_crypto_currency_evaluator(), + self.symbol_evaluator.get_symbol(), + self.symbol_evaluator.get_trader(self.exchange), + self.state, + self.symbol_evaluator.get_matrix(self.exchange).get_matrix()) + # call orders creation method await self.create_final_state_orders(self.notifier, self.trading_mode.get_only_creator_key(self.symbol)) diff --git a/Trading/Mode/market_maker_trading_mode.py b/Trading/Mode/market_maker_trading_mode.py new file mode 100644 index 000000000..88d2785b2 --- /dev/null +++ b/Trading/Mode/market_maker_trading_mode.py @@ -0,0 +1,202 @@ +""" +OctoBot Tentacle + +$tentacle_description: { + "name": "market_maker_trading_mode", + "type": "Trading", + "subtype": "Mode", + "version": "1.1.0", + "requirements": ["instant_fluctuations_evaluator", "market_making_startegy_evaluator"], + "config_files": ["MarketMakerTradingMode.json"] +} +""" +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +from ccxt import InsufficientFunds + +from config import ExchangeConstantsOrderBookInfoColumns, TraderOrderType, ExchangeConstantsMarketPropertyColumns +from trading.trader.modes import AbstractTradingModeCreator, AbstractTradingModeDecider +from trading.trader.modes.abstract_trading_mode import AbstractTradingMode + + +class MarketMakerTradingMode(AbstractTradingMode): + DESCRIPTION = "MarketMakerTradingMode" + + DELTA_ASK = "delta_ask" + DELTA_BID = "delta_bid" + + def __init__(self, config, exchange): + super().__init__(config, exchange) + + self.trading_mode_config = MarketMakerTradingMode.get_trading_mode_config() + + def create_deciders(self, symbol, symbol_evaluator): + self.add_decider(symbol, MarketMakerTradingModeDecider(self, symbol_evaluator, self.exchange)) + + def create_creators(self, symbol, _): + self.add_creator(symbol, MarketMakerTradingModeCreator(self)) + + +class MarketMakerTradingModeCreator(AbstractTradingModeCreator): + LIMIT_ORDER_ATTENUATION = 10 + FEES_ATTENUATION = 2 + + def __init__(self, trading_mode): + super().__init__(trading_mode) + + self.config_delta_ask = 0 # percent + self.config_delta_bid = 0 # percent + self.fees = {} + + if MarketMakerTradingMode.DELTA_ASK not in trading_mode.trading_mode_config or \ + MarketMakerTradingMode.DELTA_BID not in trading_mode.trading_mode_config: + self.logger.error(f"Can't create any trade : some configuration is missing " + f"in {MarketMakerTradingMode.get_config_file_name()}, " + f"please check {MarketMakerTradingMode.DELTA_ASK} and {MarketMakerTradingMode.DELTA_BID}") + else: + self.config_delta_ask = trading_mode.trading_mode_config[MarketMakerTradingMode.DELTA_ASK] + self.config_delta_bid = trading_mode.trading_mode_config[MarketMakerTradingMode.DELTA_BID] + + def verify_and_adapt_delta_with_fees(self, symbol): + if symbol in self.fees: + return self.fees[symbol] + + exchange_fees = self.trading_mode.exchange.get_fees(symbol) + delta_ask = self.config_delta_ask + delta_bid = self.config_delta_bid + + # check ask -> limit_orders -> MAKER ? not sure -> max + common_fees = max(exchange_fees[ExchangeConstantsMarketPropertyColumns.TAKER.value], + exchange_fees[ExchangeConstantsMarketPropertyColumns.FEE.value], + exchange_fees[ExchangeConstantsMarketPropertyColumns.MAKER.value]) + + if delta_ask < (common_fees / self.FEES_ATTENUATION): + delta_ask = common_fees / self.FEES_ATTENUATION + + if delta_bid < (common_fees / self.FEES_ATTENUATION): + delta_bid = common_fees / self.FEES_ATTENUATION + + self.fees[symbol] = delta_ask, delta_bid + return self.fees[symbol] + + def _get_quantity_from_risk(self, trader, quantity): + return quantity * trader.get_risk() / self.LIMIT_ORDER_ATTENUATION + + @staticmethod + async def can_create_order(symbol, exchange, state, portfolio): + return True + + async def create_new_order(self, eval_note, symbol, exchange, trader, portfolio, state): + current_order = None + + try: + delta_ask, delta_bid = self.verify_and_adapt_delta_with_fees(symbol) + best_bid_price = eval_note[ExchangeConstantsOrderBookInfoColumns.BIDS.value][0] + best_ask_price = eval_note[ExchangeConstantsOrderBookInfoColumns.ASKS.value][0] + + current_symbol_holding, current_market_quantity, market_quantity, price, symbol_market = \ + await self.get_pre_order_data(exchange, symbol, portfolio) + + created_orders = [] + + # Create SHORT order + quantity = self._get_quantity_from_risk(trader, current_symbol_holding) + quantity = self.add_dusts_to_quantity_if_necessary(quantity, price, + symbol_market, current_symbol_holding) + limit_price = best_ask_price - (best_ask_price * delta_ask) + + for order_quantity, order_price in self.check_and_adapt_order_details_if_necessary(quantity, + limit_price, + symbol_market): + current_order = trader.create_order_instance(order_type=TraderOrderType.SELL_LIMIT, + symbol=symbol, + current_price=price, + quantity=order_quantity, + price=order_price) + updated_limit = await trader.create_order(current_order, portfolio) + created_orders.append(updated_limit) + + await trader.create_order(current_order, portfolio) + + # Create LONG order + quantity = self._get_quantity_from_risk(trader, market_quantity) + quantity = self.add_dusts_to_quantity_if_necessary(quantity, price, + symbol_market, current_symbol_holding) + + limit_price = best_bid_price + (best_bid_price * delta_bid) + + for order_quantity, order_price in self.check_and_adapt_order_details_if_necessary(quantity, + limit_price, + symbol_market): + current_order = trader.create_order_instance(order_type=TraderOrderType.BUY_LIMIT, + symbol=symbol, + current_price=price, + quantity=order_quantity, + price=order_price) + await trader.create_order(current_order, portfolio) + created_orders.append(current_order) + + return created_orders + + except InsufficientFunds as e: + raise e + + except Exception as e: + self.logger.error(f"Failed to create order : {e}. " + f"Order: " + f"{current_order.get_string_info() if current_order else None}") + self.logger.exception(e) + return None + + +class MarketMakerTradingModeDecider(AbstractTradingModeDecider): + @classmethod + def get_should_cancel_loaded_orders(cls): + return True + + def check_valid_market_making_note(self, eval_note) -> bool: + if ExchangeConstantsOrderBookInfoColumns.BIDS.value not in eval_note or \ + ExchangeConstantsOrderBookInfoColumns.ASKS.value not in eval_note: + self.logger.warning("Incorrect eval_note format, can't create any order.") + return False + return True + + def set_final_eval(self): + for evaluated_strategies in self.symbol_evaluator.get_strategies_eval_list(self.exchange): + strategy_eval = evaluated_strategies.get_eval_note() + if self.check_valid_market_making_note(strategy_eval): + self.final_eval = strategy_eval + return self.check_valid_market_making_note(self.final_eval) + + async def create_state(self): + # previous_state = self.state + self.logger.info(f"{self.symbol} ** REPLACING MARKET MAKING ORDERS **") + + # cancel open orders + await self.cancel_symbol_open_orders() + + # create notification + if self.symbol_evaluator.matrices: + await self.notifier.notify_alert( + "", + self.symbol_evaluator.get_crypto_currency_evaluator(), + self.symbol_evaluator.get_symbol(), + self.symbol_evaluator.get_trader(self.exchange), + "REPLACING.ORDERS", + self.symbol_evaluator.get_matrix(self.exchange).get_matrix()) + + # call orders creation method + await self.create_final_state_orders(self.notifier, self.trading_mode.get_only_creator_key(self.symbol)) diff --git a/tests/Trading/Mode/test_market_marker_trading_mode.py b/tests/Trading/Mode/test_market_marker_trading_mode.py new file mode 100644 index 000000000..531ff9b05 --- /dev/null +++ b/tests/Trading/Mode/test_market_marker_trading_mode.py @@ -0,0 +1,161 @@ +# Drakkar-Software OctoBot +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import ccxt +import pytest + +from config import CONFIG_SIMULATOR, CONFIG_SIMULATOR_FEES, ExchangeConstantsOrderBookInfoColumns +from tentacles.Trading.Mode import MarketMakerTradingModeCreator, MarketMakerTradingMode +from tests.test_utils.config import load_test_config +from trading.exchanges.exchange_manager import ExchangeManager +from trading.trader.order import * +from trading.trader.portfolio import Portfolio +from trading.trader.trader_simulator import TraderSimulator + +# All test coroutines will be treated as marked. + +pytestmark = pytest.mark.asyncio + + +async def _get_tools(): + config = load_test_config() + symbol = "BTC/USDT" + exchange_manager = ExchangeManager(config, ccxt.binance, is_simulated=True) + await exchange_manager.initialize() + exchange_inst = exchange_manager.get_exchange() + trader_inst = TraderSimulator(config, exchange_inst, 0.3) + await trader_inst.portfolio.initialize() + trader_inst.stop_order_manager() + trader_inst.portfolio.portfolio["SUB"] = { + Portfolio.TOTAL: 0.000000000000000000005, + Portfolio.AVAILABLE: 0.000000000000000000005 + } + trader_inst.portfolio.portfolio["BNB"] = { + Portfolio.TOTAL: 0.000000000000000000005, + Portfolio.AVAILABLE: 0.000000000000000000005 + } + trader_inst.portfolio.portfolio["USDT"] = { + Portfolio.TOTAL: 2000, + Portfolio.AVAILABLE: 2000 + } + + trading_mode = MarketMakerTradingMode(config, exchange_inst) + + return config, exchange_inst, trader_inst, symbol, trading_mode + + +async def test_verify_and_adapt_delta_with_fees(): + config, exchange, trader, symbol, trading_mode = await _get_tools() + + # with default exchange simulator fees {'taker': 0, 'maker': 0, 'fee': 0} + order_creator = MarketMakerTradingModeCreator(trading_mode) + + # expect to use specified fees + order_creator.config_delta_bid = 10 + order_creator.config_delta_ask = 15 + assert order_creator.verify_and_adapt_delta_with_fees(symbol) == (15, 10) + + # with modified simulator fees + config[CONFIG_SIMULATOR][CONFIG_SIMULATOR_FEES] = { + ExchangeConstantsMarketPropertyColumns.TAKER.value: 30, + ExchangeConstantsMarketPropertyColumns.MAKER.value: 40, + ExchangeConstantsMarketPropertyColumns.FEE.value: 50 + } + order_creator = MarketMakerTradingModeCreator(trading_mode) + + # expect to use specified fees + order_creator.config_delta_bid = 100 + order_creator.config_delta_ask = 200 + assert order_creator.verify_and_adapt_delta_with_fees(symbol) == (200, 100) + + # expect to use exchange fees + order_creator = MarketMakerTradingModeCreator(trading_mode) + order_creator.config_delta_bid = 1 + order_creator.config_delta_ask = 2 + assert order_creator.verify_and_adapt_delta_with_fees(symbol) == (40 / order_creator.FEES_ATTENUATION, + 40 / order_creator.FEES_ATTENUATION) + + +async def test_create_new_order(): + config, exchange, trader, symbol, trading_mode = await _get_tools() + portfolio = trader.get_portfolio() + order_creator = MarketMakerTradingModeCreator(trading_mode) + + # portfolio: "BTC": 10 "USD": 1000 + last_btc_price = 6943.01 + + # With incorrect eval_note + assert await order_creator.create_new_order(-1, symbol, exchange, trader, portfolio, None) is None + + # With correct eval_note + last_bid_price = 190 + last_ask_price = 200 + eval_note = { + ExchangeConstantsOrderBookInfoColumns.BIDS.value: [last_bid_price, 0.2], + ExchangeConstantsOrderBookInfoColumns.ASKS.value: [last_ask_price, 0.2] + } + # with modified simulator fees + config[CONFIG_SIMULATOR][CONFIG_SIMULATOR_FEES] = { + ExchangeConstantsMarketPropertyColumns.TAKER.value: 0.02, + ExchangeConstantsMarketPropertyColumns.MAKER.value: 0.02, + ExchangeConstantsMarketPropertyColumns.FEE.value: 0.02 + } + order_creator = MarketMakerTradingModeCreator(trading_mode) + orders = await order_creator.create_new_order(eval_note, symbol, exchange, trader, portfolio, None) + assert len(orders) == 2 + + # ASK ORDER + qty = 10 / order_creator.LIMIT_ORDER_ATTENUATION + order = orders[0] + assert isinstance(order, SellLimitOrder) + assert order.currency == "BTC" + assert order.symbol == "BTC/USDT" + assert order.origin_price == 198.0 + assert order.created_last_price == last_btc_price + assert order.order_type == TraderOrderType.SELL_LIMIT + assert order.side == TradeOrderSide.SELL + assert order.status == OrderStatus.OPEN + assert order.exchange == exchange + assert order.trader == trader + assert order.fee is None + assert order.market_total_fees == 0 + assert order.filled_price == 0 + assert order.origin_quantity == qty + assert order.filled_quantity == order.origin_quantity + assert order.is_simulated is True + assert order.linked_to is None + + # BID ORDER + qty = round((2000 / order_creator.LIMIT_ORDER_ATTENUATION) / last_btc_price, 8) + order = orders[1] + assert isinstance(order, BuyLimitOrder) + assert order.currency == "BTC" + assert order.symbol == "BTC/USDT" + assert round(order.origin_price, 5) == 191.9 + # round((last_bid_price - (last_bid_price * qty / order_creator.FEES_ATTENUATION)), 5) + assert order.created_last_price == last_btc_price + assert order.order_type == TraderOrderType.BUY_LIMIT + assert order.side == TradeOrderSide.BUY + assert order.status == OrderStatus.OPEN + assert order.exchange == exchange + assert order.trader == trader + assert order.fee is None + assert order.market_total_fees == 0 + assert order.filled_price == 0 + assert order.origin_quantity == qty + assert order.filled_quantity == order.origin_quantity + assert order.is_simulated is True + assert order.linked_to is None