diff --git a/CHANGELOG.md b/CHANGELOG.md index 96eb34e..80d300a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ +## 2.0.0rc1 - 2023-11-01 +### Added for new features +* Bybit exchange V5 API support implemented. Supported account type is + [Unified Trading Account](https://testnet.bybit.com/en/help-center/article/Introduction-to-Bybit-Unified-Trading-Account), + for main and sub-accounts. Spot Trading only. + ### Update +* Lost compatibility with margin.de terminal. Scripts developed for use with the terminal can be run as +executable modules, but not the other way around. +* The logic and implementation of monitoring exceptional situations when placing and deleting orders has been updated. * Improved gRPC outage exception handling +* Up requirements ## 1.3.7.post3 - 2023-10-09 ### Fix diff --git a/README.md b/README.md index 70ae703..1d430cc 100755 --- a/README.md +++ b/README.md @@ -25,11 +25,9 @@ All risks and possible losses associated with use of this strategy lie with you. Strongly recommended that you test the strategy in the demo mode before using real bidding. ## Important notices -* For `exchanges-wrapper` `v1.3.6b4`-`v1.3.6b7` must be updated `exch_srv_cfg.toml` (see [CHANGELOG](https://github.com/DogsTailFarmer/exchanges-wrapper/blob/master/CHANGELOG.md) for details) -* Starting with version `martin-binance 1.3.4`, compatibility with `margin` will be lost, since some new parts - of the code are no longer supported by implemented `Python 3.7`. I'm focused on `Python 3.10`. - I won't rid the code of numerous compatibility elements yet, so if the margin team will update its version, - everything should work. +* For [exchanges-wrapper](https://github.com/DogsTailFarmer/exchanges-wrapper) `v>= v1.4.0` must be updated `exch_srv_cfg.toml` +* Lost compatibility with `margin.de` terminal. Scripts developed for use with the terminal can be run as + executable modules, but not the other way around. * You cannot run multiple pairs with overlapping currencies on the same account! @@ -48,16 +46,16 @@ Strongly recommended that you test the strategy in the demo mode before using re ## Referral link
-Create account on [Binance](https://accounts.binance.com/en/register?ref=QCS4OGWR) and get 10% discount on all trading -fee +Create account on [Binance](https://accounts.binance.com/en/register?ref=QCS4OGWR) and get 10% discount on all trading fee -Create account on [HUOBI](https://www.huobi.com/en-us/topic/double-reward/?invite_code=9uaw3223) and will get 50 % off -trading fees +Create account on [HUOBI](https://www.huobi.com/en-us/topic/double-reward/?invite_code=9uaw3223) and will get 50 % off trading fees Create account on [Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP) and get 6% rebate fee Create account on [OKEX](https://www.okex.com/join/2607649) and get Mystery Boxes worth up to $10,000 +Create account on [Bybit](https://www.bybit.com/invite?ref=9KEW1K) and get exclusive referral rewards + Also, you can start strategy on [Hetzner](https://hetzner.cloud/?ref=uFdrF8nsdGMc) cloud VPS only for 4.75 € per month ### Donate diff --git a/martin_binance/__init__.py b/martin_binance/__init__.py index ba83834..6f2928c 100755 --- a/martin_binance/__init__.py +++ b/martin_binance/__init__.py @@ -6,29 +6,20 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.4.0.b2" +__version__ = "2.0.0rc1" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" from pathlib import Path from shutil import copy -STANDALONE = True -if 'margin' in str(Path().resolve()): - print('margin detected') - STANDALONE = False WORK_PATH = Path(Path.home(), ".MartinBinance") CONFIG_PATH = Path(WORK_PATH, "config") CONFIG_FILE = Path(CONFIG_PATH, "ms_cfg.toml") DB_FILE = Path(WORK_PATH, "funds_rate.db") -if STANDALONE: - LOG_PATH = Path(WORK_PATH, "log") - LAST_STATE_PATH = Path(WORK_PATH, "last_state") - BACKTEST_PATH = Path(WORK_PATH, "back_test") -else: - LOG_PATH = None - LAST_STATE_PATH = None - BACKTEST_PATH = None +LOG_PATH = Path(WORK_PATH, "log") +LAST_STATE_PATH = Path(WORK_PATH, "last_state") +BACKTEST_PATH = Path(WORK_PATH, "back_test") def init(): @@ -37,19 +28,17 @@ def init(): else: print("Can't find client config file! Creating it...") CONFIG_PATH.mkdir(parents=True, exist_ok=True) - if STANDALONE: - LOG_PATH.mkdir(parents=True, exist_ok=True) - LAST_STATE_PATH.mkdir(parents=True, exist_ok=True) + LOG_PATH.mkdir(parents=True, exist_ok=True) + LAST_STATE_PATH.mkdir(parents=True, exist_ok=True) copy(Path(Path(__file__).parent.absolute(), "ms_cfg.toml.template"), CONFIG_FILE) copy(Path(Path(__file__).parent.absolute(), "funds_rate.db.template"), DB_FILE) copy(Path(Path(__file__).parent.absolute(), "cli_0_BTCUSDT.py.template"), Path(WORK_PATH, "cli_0_BTCUSDT.py")) copy(Path(Path(__file__).parent.absolute(), "cli_1_BTCUSDT.py.template"), Path(WORK_PATH, "cli_1_BTCUSDT.py")) copy(Path(Path(__file__).parent.absolute(), "cli_2_TESTBTCTESTUSDT.py.template"), Path(WORK_PATH, "cli_2_TESTBTCTESTUSDT.py")) + copy(Path(Path(__file__).parent.absolute(), "cli_3_BTCUSDT.py.template"), Path(WORK_PATH, "cli_3_BTCUSDT.py")) print(f"Before the first run, set the parameters in {CONFIG_FILE}") - if STANDALONE: - raise SystemExit(1) - raise UserWarning() + raise SystemExit(1) if __name__ == '__main__': diff --git a/martin_binance/cli_0_BTCUSDT.py.template b/martin_binance/cli_0_BTCUSDT.py.template index 0b65846..30c7b43 100644 --- a/martin_binance/cli_0_BTCUSDT.py.template +++ b/martin_binance/cli_0_BTCUSDT.py.template @@ -7,7 +7,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.3.4" +__version__ = "2.0.0" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" """ @@ -17,10 +17,7 @@ Disclaimer All risks and possible losses associated with use of this strategy lie with you. Strongly recommended that you test the strategy in the demo mode before using real bidding. ################################################################## -For standalone use set SYMBOL parameter at the TOP of this file - Check and set parameter at the TOP part of script - Verify init message in Strategy output window for no error """ ################################################################ @@ -32,7 +29,7 @@ from martin_binance.executor import * # lgtm [py/polluting-import] ################################################################ # Exchange setup and parameter settings ################################################################ -# Set trading pair for STANDALONE mode, for margin mode takes from terminal +# Set trading pair for Strategy ex.SYMBOL = 'BTCUSDT' # Exchange setup, see list of exchange in ms_cfg.toml ex.ID_EXCHANGE = 0 # See ms_cfg.toml Use for collection of statistics *and get client connection* @@ -61,7 +58,7 @@ ex.SHIFT_GRID_DELAY = 15 # sec delay for shift grid action ex.STATUS_DELAY = 5 # Minute between sending Tlg message about current status, 0 - disable ex.GRID_ONLY = False # Only place grid orders for buy/sell asset ex.LOG_LEVEL_NO_PRINT = [] # LogLevel.DEBUG Print for level over this list member -ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for STANDALONE mode on subaccount only +ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for subaccount only # Parameter for calculate grid over price and grid orders quantity in set_trade_condition() # If ADAPTIVE_TRADE_CONDITION = True, ORDER_Q / OVER_PRICE determines the density of grid orders ex.ADAPTIVE_TRADE_CONDITION = True @@ -139,5 +136,5 @@ def trade(): loop.close() -if __name__ == "__main__" and STANDALONE: +if __name__ == "__main__": trade() diff --git a/martin_binance/cli_1_BTCUSDT.py.template b/martin_binance/cli_1_BTCUSDT.py.template index 1adb991..c460c80 100644 --- a/martin_binance/cli_1_BTCUSDT.py.template +++ b/martin_binance/cli_1_BTCUSDT.py.template @@ -7,7 +7,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.3.4" +__version__ = "2.0.0" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" """ @@ -17,10 +17,7 @@ Disclaimer All risks and possible losses associated with use of this strategy lie with you. Strongly recommended that you test the strategy in the demo mode before using real bidding. ################################################################## -For standalone use set SYMBOL parameter at the TOP of this file - Check and set parameter at the TOP part of script - Verify init message in Strategy output window for no error """ ################################################################ @@ -32,7 +29,7 @@ from martin_binance.executor import * # lgtm [py/polluting-import] ################################################################ # Exchange setup and parameter settings ################################################################ -# Set trading pair for STANDALONE mode, for margin mode takes from terminal +# Set trading pair for Strategy ex.SYMBOL = 'BTCUSDT' # Exchange setup, see list of exchange in ms_cfg.toml ex.ID_EXCHANGE = 1 # See ms_cfg.toml Use for collection of statistics *and get client connection* @@ -61,7 +58,7 @@ ex.SHIFT_GRID_DELAY = 15 # sec delay for shift grid action ex.STATUS_DELAY = 5 # Minute between sending Tlg message about current status, 0 - disable ex.GRID_ONLY = False # Only place grid orders for buy/sell asset ex.LOG_LEVEL_NO_PRINT = [] # LogLevel.DEBUG Print for level over this list member -ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for STANDALONE mode on subaccount only +ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for subaccount only # Parameter for calculate grid over price and grid orders quantity in set_trade_condition() # If ADAPTIVE_TRADE_CONDITION = True, ORDER_Q / OVER_PRICE determines the density of grid orders ex.ADAPTIVE_TRADE_CONDITION = True @@ -139,5 +136,5 @@ def trade(): loop.close() -if __name__ == "__main__" and STANDALONE: +if __name__ == "__main__": trade() diff --git a/martin_binance/cli_2_TESTBTCTESTUSDT.py.template b/martin_binance/cli_2_TESTBTCTESTUSDT.py.template index b577bed..fe0e2c2 100644 --- a/martin_binance/cli_2_TESTBTCTESTUSDT.py.template +++ b/martin_binance/cli_2_TESTBTCTESTUSDT.py.template @@ -7,7 +7,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.3.4" +__version__ = "2.0.0" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" """ @@ -17,10 +17,7 @@ Disclaimer All risks and possible losses associated with use of this strategy lie with you. Strongly recommended that you test the strategy in the demo mode before using real bidding. ################################################################## -For standalone use set SYMBOL parameter at the TOP of this file - Check and set parameter at the TOP part of script - Verify init message in Strategy output window for no error """ ################################################################ @@ -32,7 +29,7 @@ from martin_binance.executor import * # lgtm [py/polluting-import] ################################################################ # Exchange setup and parameter settings ################################################################ -# Set trading pair for STANDALONE mode, for margin mode takes from terminal +# Set trading pair for Strategy ex.SYMBOL = 'TESTBTCTESTUSDT' # Exchange setup, see list of exchange in ms_cfg.toml ex.ID_EXCHANGE = 2 # See ms_cfg.toml Use for collection of statistics *and get client connection* @@ -61,7 +58,7 @@ ex.SHIFT_GRID_DELAY = 15 # sec delay for shift grid action ex.STATUS_DELAY = 5 # Minute between sending Tlg message about current status, 0 - disable ex.GRID_ONLY = False # Only place grid orders for buy/sell asset ex.LOG_LEVEL_NO_PRINT = [] # LogLevel.DEBUG Print for level over this list member -ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for STANDALONE mode on subaccount only +ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for subaccount only # Parameter for calculate grid over price and grid orders quantity in set_trade_condition() # If ADAPTIVE_TRADE_CONDITION = True, ORDER_Q / OVER_PRICE determines the density of grid orders ex.ADAPTIVE_TRADE_CONDITION = True @@ -139,5 +136,5 @@ def trade(): loop.close() -if __name__ == "__main__" and STANDALONE: +if __name__ == "__main__": trade() diff --git a/martin_binance/cli_3_BTCUSDT.py.template b/martin_binance/cli_3_BTCUSDT.py.template new file mode 100755 index 0000000..6d52902 --- /dev/null +++ b/martin_binance/cli_3_BTCUSDT.py.template @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#################################################################### +# Cyclic grid strategy based on martingale +# See README.md for detail +#################################################################### +__author__ = "Jerry Fedorenko" +__copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" +__license__ = "MIT" +__version__ = "2.0.0" +__maintainer__ = "Jerry Fedorenko" +__contact__ = "https://github.com/DogsTailFarmer" +""" +################################################################## +Disclaimer + +All risks and possible losses associated with use of this strategy lie with you. +Strongly recommended that you test the strategy in the demo mode before using real bidding. +################################################################## +Check and set parameter at the TOP part of script +Verify init message in Strategy output window for no error +""" +################################################################ +import toml +# noinspection PyUnresolvedReferences +import sys +import martin_binance.executor as ex +from martin_binance.executor import * # lgtm [py/polluting-import] +################################################################ +# Exchange setup and parameter settings +################################################################ +# Set trading pair for Strategy +ex.SYMBOL = 'BTCUSDT' +# Exchange setup, see list of exchange in ms_cfg.toml +ex.ID_EXCHANGE = 3 # See ms_cfg.toml Use for collection of statistics *and get client connection* +ex.FEE_IN_PAIR = True # Fee pays in pair +ex.FEE_MAKER = Decimal('0.1') # standard exchange Fee for maker +ex.FEE_TAKER = Decimal('0.15') # standard exchange Fee for taker +ex.FEE_SECOND = False # On KRAKEN fee always in second coin +ex.FEE_BNB_IN_PAIR = False # Binance fee in BNB and BNB is base asset +ex.GRID_MAX_COUNT = 5 # Maximum counts for placed grid orders +# Trade parameter +ex.START_ON_BUY = True # First cycle direction +ex.AMOUNT_FIRST = Decimal('0') # Deposit for Sale cycle in first currency +ex.USE_ALL_FUND = False # Use all available fund for initial cycle or alltime for GRID_ONLY +ex.AMOUNT_SECOND = Decimal('1000.0') # Deposit for Buy cycle in second currency +ex.PRICE_SHIFT = 0.01 # 'No market' shift price in % from current bid/ask price +# Round pattern, set pattern 1.0123456789 or if not set used exchange settings +ex.ROUND_BASE = str() +ex.ROUND_QUOTE = str() +ex.PROFIT = Decimal('0.1') # 0.1 - 0.85 +ex.PROFIT_MAX = Decimal('0.5') # If set it is maximum adapted cycle profit +ex.OVER_PRICE = Decimal('0.6') # Overlap price in one direction +ex.ORDER_Q = 11 # Target grid orders quantity in moment +ex.MARTIN = Decimal('10') # 5-20, % increments volume of orders in the grid +ex.SHIFT_GRID_DELAY = 1 # sec delay for shift grid action +# Other +ex.STATUS_DELAY = 60 # Minute between sending Tlg message about current status, 0 - disable +ex.GRID_ONLY = False # Only place grid orders for buy/sell asset +ex.LOG_LEVEL_NO_PRINT = [] # LogLevel.DEBUG Print for level over this list member +ex.COLLECT_ASSETS = False # Transfer free asset to main account, valid for subaccount only +# Parameter for calculate grid over price and grid orders quantity in set_trade_condition() +# If ADAPTIVE_TRADE_CONDITION = True, ORDER_Q / OVER_PRICE determines the density of grid orders +ex.ADAPTIVE_TRADE_CONDITION = True +ex.BB_CANDLE_SIZE_IN_MINUTES = 60 +ex.BB_NUMBER_OF_CANDLES = 20 +ex.KBB = 1.0 # k for Bollinger Band +# Parameter for calculate price of grid orders by logarithmic scale +# If -1 function is disabled, can take a value from 0 to infinity (in practice no more 1000) +# When 0 - logarithmic scale, increase parameter the result is approaching linear +ex.LINEAR_GRID_K = 0 # See 'Model of logarithmic grid.ods' for detail +# Average Directional Index with +DI and -DI for Reverse conditions analise +ex.ADX_CANDLE_SIZE_IN_MINUTES = 1 +ex.ADX_NUMBER_OF_CANDLES = 60 +ex.ADX_PERIOD = 14 +ex.ADX_THRESHOLD = 40 # ADX value that indicates a strong trend +ex.ADX_PRICE_THRESHOLD = 0.05 # % Max price drift before release Hold reverse cycle +# Start first as Reverse cycle, also set appropriate AMOUNT +ex.REVERSE = False +ex.REVERSE_TARGET_AMOUNT = Decimal('0') +ex.REVERSE_INIT_AMOUNT = Decimal('0') +ex.REVERSE_STOP = False # Stop after ending reverse cycle +# Backtest mode parameters +ex.MODE = 'T' # 'T' - Trade, 'TC' - Trade and Collect, 'S' - Simulate +ex.SAVE_DS = True # Save session result data (ticker, orders) for compare +ex.SAVE_PERIOD = 24 * 60 * 60 # sec, timetable for save data portion, but memory limitation consider also matter +ex.SAVED_STATE = False # Use saved state for backtesting +################################################################ +# DO NOT EDIT UNDER THIS LINE ### +################################################################ +ex.PARAMS = Path(__file__).absolute() +config = toml.load(str(ex.CONFIG_FILE)) if ex.CONFIG_FILE.exists() else None +ex.HEAD_VERSION = __version__ +ex.EXCHANGE = config.get('exchange') +ex.VPS_NAME = config.get('Exporter').get('vps_name') +# Telegram parameters +telegram = config.get('Telegram') +ex.TELEGRAM_URL = config.get('telegram_url') +for tlg in telegram: + if ex.ID_EXCHANGE in tlg.get('id_exchange'): + ex.TOKEN = tlg.get('token') + ex.CHANNEL_ID = tlg.get('channel_id') + ex.INLINE_BOT = tlg.get('inline') + break + + +def trade(): + import logging.handlers + # For autoload last state + ex.LOAD_LAST_STATE = 1 if len(sys.argv) > 1 else 0 + # + log_file = Path(ex.LOG_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.log") + ex.LAST_STATE_FILE = Path(ex.LAST_STATE_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.json") + # + _logger = logging.getLogger('logger') + _logger.setLevel(logging.DEBUG) + handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10) + handler.setFormatter(logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")) + _logger.addHandler(handler) + _logger.propagate = False + # + try: + loop.create_task(main(ex.SYMBOL)) + loop.run_forever() + except KeyboardInterrupt: + pass + finally: + try: + loop.run_until_complete(ask_exit()) + except asyncio.CancelledError: + pass + except Exception as _err: + print(f"Error: {_err}") + loop.run_until_complete(loop.shutdown_asyncgens()) + if ex.MODE in ('T', 'TC'): + loop.close() + + +if __name__ == "__main__": + trade() diff --git a/martin_binance/client.py b/martin_binance/client.py index 07a263a..0b3557f 100644 --- a/martin_binance/client.py +++ b/martin_binance/client.py @@ -4,7 +4,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.4.0.b2" +__version__ = "2.0.0rc1" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" diff --git a/martin_binance/executor.py b/martin_binance/executor.py index 245721c..3a3b095 100755 --- a/martin_binance/executor.py +++ b/martin_binance/executor.py @@ -4,14 +4,14 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.4.0.b2" +__version__ = "2.0.0rc1" __maintainer__ = "Jerry Fedorenko" __contact__ = 'https://github.com/DogsTailFarmer' ################################################################## import sys import gc import statistics -from decimal import Decimal, ROUND_FLOOR, ROUND_CEILING +from decimal import ROUND_CEILING from threading import Thread import queue import requests @@ -20,23 +20,13 @@ import traceback # lgtm [py/unused-import] import contextlib -from martin_binance import Path, STANDALONE, DB_FILE +from martin_binance import DB_FILE # noinspection PyUnresolvedReferences from martin_binance import WORK_PATH, CONFIG_FILE, LOG_PATH, LAST_STATE_PATH # lgtm [py/unused-import] -if STANDALONE: - from martin_binance.margin_wrapper import * # lgtm [py/polluting-import] - from martin_binance.margin_wrapper import __version__ as msb_ver - import psutil -else: - from margin_strategy_sdk import * # lgtm [py/polluting-import] skipcq: PY-W2000 - from typing import Dict, List - import sqlite3 - import time - import math - import simplejson as json - msb_ver = str() - psutil = None +from martin_binance.margin_wrapper import * # lgtm [py/polluting-import] +from martin_binance.margin_wrapper import __version__ as msb_ver +import psutil # region SetParameters SYMBOL = str() @@ -800,7 +790,7 @@ def __init__(self): self.start_reverse_time = None # - self.last_ticker_update = 0 # - self.grid_only_restart = None # - - self.local_time = self.get_time if STANDALONE else time.time + self.local_time = self.get_time self.wait_wss_refresh = {} # - self.start_collect = None self.restore_orders = False # + Flag when was filled grid order during grid cancellation @@ -820,8 +810,7 @@ def init(self, check_funds: bool = True) -> None: # skipcq: PYL-W0221 init_params_error = None if init_params_error: self.message_log(f"Incorrect value for {init_params_error}", log_level=LogLevel.ERROR) - if STANDALONE: - raise SystemExit(1) + raise SystemExit(1) db_management() tcm = self.get_trading_capability_manager() self.f_currency = self.get_first_currency() @@ -858,8 +847,7 @@ def init(self, check_funds: bool = True) -> None: # skipcq: PYL-W0221 self.deposit_second = self.round_truncate(f2d(ds), base=False) elif self.deposit_second > f2d(ds): self.message_log('Not enough second coin for Buy cycle!', color=Style.B_RED) - if STANDALONE: - raise SystemExit(1) + raise SystemExit(1) else: df = self.get_buffered_funds().get(self.f_currency, 0) df = df.available if df else 0 @@ -867,12 +855,10 @@ def init(self, check_funds: bool = True) -> None: # skipcq: PYL-W0221 self.deposit_first = self.round_truncate(f2d(df), base=True) elif self.deposit_first > f2d(df): self.message_log('Not enough first coin for Sell cycle!', color=Style.B_RED) - if STANDALONE: - raise SystemExit(1) + raise SystemExit(1) else: self.message_log("Can't get actual price, initialization checks stopped", log_level=LogLevel.CRITICAL) - if STANDALONE: - raise SystemExit(1) + raise SystemExit(1) # self.message_log('End Init section') @staticmethod @@ -1207,13 +1193,23 @@ def restore_strategy_state(self, strategy_state: Dict[str, str] = None, restore= self.avg_rate = f2d(self.get_buffered_ticker().last_price) # open_orders = self.get_buffered_open_orders() + tp_order = None + # Separate TP order + if self.tp_order_id: + for i, o in enumerate(open_orders): + if o.id == self.tp_order_id: + tp_order = open_orders[i] + del open_orders[i] # skipcq: PYL-E1138 + break # Possible strategy states in compare with saved one - open_orders_len = len(open_orders) - grid_hold = open_orders_len == 0 and self.orders_hold + grid_open_orders_len = len(open_orders) # - if grid_hold: + if not grid_open_orders_len and self.orders_hold: self.message_log("Restore, no grid orders, place from hold now", tlg=True) self.place_grid_part() + if not GRID_ONLY and self.shift_grid_threshold is None and not tp_order: + self.message_log("Restore, no TP order, replace", tlg=True) + self.place_profit_order() # self.message_log("Restored, go work", tlg=True) @@ -1290,7 +1286,7 @@ def start(self, profit_f: Decimal = f2d(0), profit_s: Decimal = f2d(0)) -> None: print('Send data to .db t_funds') self.queue_to_db.put(data_to_db) self.save_init_assets(ff, fs) - if STANDALONE and COLLECT_ASSETS: + if COLLECT_ASSETS: _ff, _fs = self.collect_assets() ff -= _ff fs -= _fs @@ -1415,16 +1411,13 @@ def init_warning(self, _amount_first_grid: Decimal): if self.first_run and self.order_q < 3: self.message_log(f"Depo amount {depo} not enough to set the grid with 3 or more orders", log_level=LogLevel.ERROR) - if STANDALONE: - raise SystemExit(1) - raise UserWarning + raise SystemExit(1) _amount_first_grid = (_amount_first_grid * self.avg_rate) if self.cycle_buy else _amount_first_grid if _amount_first_grid > 80 * depo / 100: self.message_log(f"Recommended size of the first grid order {_amount_first_grid:f} too large for" f" a small deposit {self.deposit_second}", log_level=LogLevel.ERROR) - if STANDALONE and self.first_run: + if self.first_run: raise SystemExit(1) - raise UserWarning if _amount_first_grid > 20 * depo / 100: self.message_log(f"Recommended size of the first grid order {_amount_first_grid:f} it is rather" f" big for a small deposit" @@ -1438,9 +1431,8 @@ def init_warning(self, _amount_first_grid: Decimal): if first_order_vlm < _amount_first_grid: self.message_log(f"Depo amount {depo}{self.s_currency} not enough for {ORDER_Q} orders", color=Style.B_RED) - if STANDALONE and self.first_run: + if self.first_run: raise SystemExit(1) - raise UserWarning def save_init_assets(self, ff, fs): if self.reverse: @@ -1471,7 +1463,6 @@ def message_log(self, msg: str, log_level=LogLevel.INFO, tlg=False, color=Style. if log_level in (LogLevel.ERROR, LogLevel.CRITICAL): tlg = True color = Style.B_RED - color = color if STANDALONE else 0 color_msg = color+msg+Style.RESET if color else msg if log_level not in LOG_LEVEL_NO_PRINT: if MODE in ('T', 'TC'): @@ -1507,10 +1498,7 @@ def start_process(self): self.message_log(str(error), log_level=LogLevel.ERROR, color=Style.B_RED) def cancel_order_exp(self, order_id: int, cancel_all=False) -> None: - if STANDALONE: - self.cancel_order(order_id, cancel_all=cancel_all) - else: - self.cancel_order(order_id) + self.cancel_order(order_id, cancel_all=cancel_all) ############################################################## # Technical analysis @@ -1829,9 +1817,7 @@ def place_grid(self, i, amount, price = order # create order for grid if i < GRID_MAX_COUNT: - waiting_order_id = self.place_limit_order(buy_side, - amount if STANDALONE else float(amount), - price if STANDALONE else float(price)) + waiting_order_id = self.place_limit_order(buy_side, amount, price) self.orders_init.append_order(waiting_order_id, buy_side, amount, price) else: self.orders_hold.append_order(i, buy_side, amount, price) @@ -2077,19 +2063,6 @@ def place_profit_order(self, by_market: bool = False) -> None: self.message_log(f"Create {'Buy' if buy_side else 'Sell'} take profit order," f" vlm: {amount}, price: {price}, profit: {profit}%") self.tp_target = target - if not STANDALONE: - _amount = float(amount) - _price = float(price) - tcm = self.get_trading_capability_manager() - if not tcm.is_limit_order_valid(buy_side, _amount, _price): - _amount = tcm.round_amount(_amount, RoundingType.FLOOR) - if buy_side: - _price = tcm.round_price(_price, RoundingType.FLOOR) - else: - _price = tcm.round_price(_price, RoundingType.CEIL) - amount = f2d(_amount) - price = f2d(_price) - self.message_log(f"Rounded amount: {amount}, price: {price}") self.tp_order = (buy_side, amount, price, self.local_time()) check = (len(self.orders_grid) + len(self.orders_hold)) > 2 self.tp_wait_id = self.place_limit_order_check(buy_side, amount, price, check=check) @@ -2826,9 +2799,7 @@ def place_limit_order_check(self, buy: bool, amount: Decimal, price: Decimal, ch elif not buy and order_book.asks: price = f2d(max(_price, order_book.asks[0].price)) - waiting_order_id = self.place_limit_order(buy, - amount if STANDALONE else float(amount), - price if STANDALONE else float(price)) + waiting_order_id = self.place_limit_order(buy, amount, price) if check and _price != float(price): self.message_log(f"For order {waiting_order_id} price was updated from {_price} to {price}", log_level=LogLevel.WARNING) diff --git a/martin_binance/margin_wrapper.py b/martin_binance/margin_wrapper.py index a82738f..5d02ef3 100755 --- a/martin_binance/margin_wrapper.py +++ b/martin_binance/margin_wrapper.py @@ -4,7 +4,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "1.4.0.b2" +__version__ = "2.0.0rc1" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" @@ -1262,6 +1262,7 @@ def _on_order_update_handler_ext(ed, cls): async def create_limit_order(_id: int, buy: bool, amount: str, price: str) -> None: cls = StrategyBase cls.wait_order_id.append(_id) + _fetch_order = False try: if ms.MODE in ('T', 'TC'): ts = time.time() @@ -1284,72 +1285,79 @@ async def create_limit_order(_id: int, buy: bool, amount: str, price: str) -> No except asyncio.CancelledError: pass # Task cancellation should not be logged as an error except grpc.RpcError as ex: - status_code = ex.code() - cls.strategy.message_log(f"Exception creating order {_id}: {status_code.name}, {ex.details()}") - if status_code == grpc.StatusCode.FAILED_PRECONDITION: - # Supress order timeout message - cls.wait_order_id.remove(_id) - cls.strategy.on_place_order_error_string(_id, error=f"FAILED_PRECONDITION: {ex.details()}") + _fetch_order = True + cls.strategy.message_log(f"Exception creating order {_id}: {ex.code().name}, {ex.details()}") except Exception as _ex: + _fetch_order = True cls.strategy.message_log(f"Exception creating order {_id}: {_ex}") else: - # cls.strategy.message_log(f"Create_limit_order.result: {result}", - # log_level=LogLevel.DEBUG, color=ms.Style.UNDERLINE) if result: - cls.wait_order_id.remove(_id) - order = Order(result) - cls.strategy.message_log( - f"Order placed {order.id}({result.get('clientOrderId') or _id}) for {result.get('side')}" - f" {any2str(order.amount)} by {any2str(order.price)}" - f" Remaining amount {any2str(order.remaining_amount)}", - color=ms.Style.GREEN) - orig_qty = Decimal(result['origQty']) - executed_qty = Decimal(result['executedQty']) - cummulative_quote_qty = Decimal(result['cummulativeQuoteQty']) - if executed_qty > 0: - price = float(cummulative_quote_qty / executed_qty) - trade = {"qty": float(executed_qty), - "isBuyer": order.buy, - "id": int(time.time() * 1000), - "orderId": order.id, - "price": price, - "time": order.timestamp} - # cls.strategy.message_log(f"place_limit_order_callback.trade: {trade}", color=ms.Style.YELLOW) - if len(cls.trades) > TRADES_LIST_LIMIT: - del cls.trades[0] - cls.trades.append(PrivateTrade(trade)) + await create_order_handler(_id, result) + else: + _fetch_order = True + finally: + if _fetch_order: + await asyncio.sleep(HEARTBEAT) + await fetch_order(0, str(_id), _filled_update_call=True) - if ms.MODE == 'TC' and cls.strategy.start_collect: - cls.strategy.s_ticker[list(cls.strategy.s_ticker)[-1]].update({'lastPrice': str(price)}) - - if executed_qty < orig_qty: - cls.orders[order.id] = order - elif ms.SAVE_TRADE_HISTORY: - row = ["TRADE_BY_MARKET", - int(time.time() * 1000), - result["side"], - result["orderId"], - result["clientOrderId"], - '-1', - result["origQty"], - result["price"], - result["executedQty"], - result["cummulativeQuoteQty"], - result["executedQty"], - result["price"], - ] - await save_trade_queue.put(row) + +async def create_order_handler(_id, result): + cls = StrategyBase + if _id in cls.wait_order_id: + cls.wait_order_id.remove(_id) + order = Order(result) + if cls.orders.get(order.id) is None: + cls.strategy.message_log( + f"Order placed {order.id}({result.get('clientOrderId') or _id}) for {result.get('side')}" + f" {any2str(order.amount)} by {any2str(order.price)}" + f" Remaining amount {any2str(order.remaining_amount)}", + color=ms.Style.GREEN) + orig_qty = Decimal(result['origQty']) + executed_qty = Decimal(result['executedQty']) + cummulative_quote_qty = Decimal(result['cummulativeQuoteQty']) + if executed_qty > 0: + price = float(cummulative_quote_qty / executed_qty) + + ''' + trade = {"qty": float(executed_qty), + "isBuyer": order.buy, + "id": int(time.time() * 1000), + "orderId": order.id, + "price": price, + "time": order.timestamp} + # cls.strategy.message_log(f"place_limit_order_callback.trade: {trade}", color=ms.Style.YELLOW) + if len(cls.trades) > TRADES_LIST_LIMIT: + del cls.trades[0] + cls.trades.append(PrivateTrade(trade)) + ''' if ms.MODE == 'TC' and cls.strategy.start_collect: - cls.strategy.open_orders_snapshot() - elif ms.MODE == 'S': - await on_funds_update() - cls.strategy.on_place_order_success(_id, order) + cls.strategy.s_ticker[list(cls.strategy.s_ticker)[-1]].update({'lastPrice': str(price)}) + if executed_qty < orig_qty: + cls.orders[order.id] = order + elif ms.SAVE_TRADE_HISTORY: + row = ["TRADE_BY_MARKET", + int(time.time() * 1000), + result["side"], + result["orderId"], + result["clientOrderId"], + '-1', + result["origQty"], + result["price"], + result["executedQty"], + result["cummulativeQuoteQty"], + result["executedQty"], + result["price"], + ] + await save_trade_queue.put(row) + if ms.MODE == 'TC' and cls.strategy.start_collect: + cls.strategy.open_orders_snapshot() + elif ms.MODE == 'S': + await on_funds_update() + cls.strategy.on_place_order_success(_id, order) async def place_limit_order_timeout(_id): - # TODO Check if order exist on exchange remove it or take in account - # /home/ubuntu/.config/JetBrains/IdeaIC2023.2/scratches/Duplicate order sent.txt cls = StrategyBase await asyncio.sleep(ORDER_TIMEOUT) if _id in cls.wait_order_id: @@ -1360,6 +1368,7 @@ async def place_limit_order_timeout(_id): async def cancel_order_call(_id: int, cancel_all: bool): cls = StrategyBase cls.canceled_order_id.append(_id) + _fetch_order = False try: if ms.MODE in ('T', 'TC'): if cancel_all: @@ -1377,19 +1386,17 @@ async def cancel_order_call(_id: int, cancel_all: bool): except asyncio.CancelledError: pass # Task cancellation should not be logged as an error. except grpc.RpcError as ex: - status_code = ex.code() - cls.strategy.message_log(f"Exception on cancel order for {_id}: {status_code.name}, {ex.details()}") - if status_code == grpc.StatusCode.UNKNOWN: - cls.canceled_order_id.remove(_id) - cls.strategy.on_cancel_order_error_string(_id, "The order has not been canceled") + _fetch_order = True + cls.strategy.message_log(f"Exception on cancel order {_id}: {ex.code().name}, {ex.details()}") except Exception as _ex: + _fetch_order = True cls.strategy.message_log(f"Exception on cancel order call for {_id}: {_ex}") logger.debug(f"Exception traceback: {traceback.format_exc()}") else: # print(f"cancel_order_call.result: {result}") # Remove from orders lists - cls.canceled_order_id.remove(_id) if result and result.get('status') == 'CANCELED': + cls.canceled_order_id.remove(_id) remove_from_orders_lists([_id]) cls.strategy.message_log(f"Cancel order {_id} success", color=ms.Style.GREEN) cls.strategy.on_cancel_order_success(_id, Order(result), cancel_all=cancel_all) @@ -1398,9 +1405,12 @@ async def cancel_order_call(_id: int, cancel_all: bool): elif ms.MODE == 'S': await on_funds_update() else: + cls.strategy.message_log(f"Cancel order {_id}: Warning, not result getting") + _fetch_order = True + finally: + if _fetch_order: await asyncio.sleep(HEARTBEAT) await fetch_order(_id, _filled_update_call=True) - cls.strategy.on_cancel_order_error_string(_id, "Warning, not result getting") async def cancel_order_timeout(_id): @@ -1411,12 +1421,13 @@ async def cancel_order_timeout(_id): cls.strategy.on_cancel_order_error_string(_id, 'Cancel order timeout') -async def fetch_order(_id: int, _filled_update_call=False): +async def fetch_order(_id: int, _client_order_id: str = None, _filled_update_call=False): cls = StrategyBase try: res = await cls.send_request(cls.stub.FetchOrder, api_pb2.FetchOrderRequest, symbol=cls.symbol, order_id=_id, + client_order_id=_client_order_id, filled_update_call=_filled_update_call) result = json_format.MessageToDict(res) except asyncio.CancelledError: @@ -1425,11 +1436,15 @@ async def fetch_order(_id: int, _filled_update_call=False): cls.strategy.message_log(f"Exception in fetch_order: {_ex}", log_level=LogLevel.ERROR) return {} else: - cls.strategy.message_log(f"For order {_id} fetched status is {result.get('status')}", + cls.strategy.message_log(f"For order {_id}({_client_order_id}) fetched status is {result.get('status')}", log_level=LogLevel.INFO, color=ms.Style.GREEN) if _filled_update_call and result.get('status') == 'CANCELED': + if _id in cls.canceled_order_id: + cls.canceled_order_id.remove(_id) remove_from_orders_lists([_id]) cls.strategy.on_cancel_order_success(_id, Order(result)) + elif _filled_update_call and result.get('status') in ('NEW', 'PARTIALLY_FILLED'): + await create_order_handler(_client_order_id, result) elif not result: cls.strategy.message_log(f"Can't get status for order {_id}", log_level=LogLevel.WARNING) return result diff --git a/martin_binance/ms_cfg.toml.template b/martin_binance/ms_cfg.toml.template index d5b892a..a063be0 100644 --- a/martin_binance/ms_cfg.toml.template +++ b/martin_binance/ms_cfg.toml.template @@ -9,13 +9,15 @@ telegram_url = "https://api.telegram.org/bot" # Accounts name wold be identically accounts.name from exchanges_wrapper/exch_srv_cfg.toml exchange = [ 'Demo - Binance', # 0 - 'Demo - OKEX', # 1 + 'Demo - OKX', # 1 "Demo - Bitfinex", # 2 - 'Binance', # 3 - 'OKEX', # 4 - 'Bitfinex', # 5 - 'Huobi', # 6 - "Binance US", # 7 + "Demo - Bybit", # 3 + "Binance", # 4 + "OKX", # 5 + "Bitfinex", # 6 + "Huobi", # 7 + "Binance US", # 8 + "Bybit", # 9 ] [Exporter] @@ -28,19 +30,25 @@ api = "********** Place API key for CoinMarketCap there ***********" rate_limit = 30 # Requests per minute [[Telegram]] -id_exchange = [0, 3, 7] # 'Binance', 'Demo - Binance', 'Binance US' +id_exchange = [0, 4, 8] # 'Binance', 'Demo - Binance', 'Binance US' token = "********** Place Token for Telegram bot there ***********" channel_id = "*** Place channel_id there ***" inline = true [[Telegram]] -id_exchange = [1, 4] # 'Demo - OKEX', 'OKEX' +id_exchange = [1, 5] # 'Demo - OKEX', 'OKEX' token = "********** Place Token for Telegram bot there ***********" channel_id = "*** Place channel_id there ***" inline = false [[Telegram]] -id_exchange = [2, 5] # "Demo - Bitfinex", "Bitfinex" +id_exchange = [2, 6] # "Demo - Bitfinex", "Bitfinex" +token = "********** Place Token for Telegram bot there ***********" +channel_id = "*** Place channel_id there ***" +inline = false + +[[Telegram]] +id_exchange = [3, 9] # "Demo - Bitfinex", "Bitfinex" token = "********** Place Token for Telegram bot there ***********" channel_id = "*** Place channel_id there ***" inline = false diff --git a/pyproject.toml b/pyproject.toml index 16de459..c912488 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,21 +17,21 @@ dynamic = ["version", "description"] requires-python = ">=3.8" dependencies = [ - "exchanges-wrapper==1.3.7.post4", + "exchanges-wrapper>=1.4.0rc3", "margin-strategy-sdk==0.0.11", "jsonpickle==3.0.2", - "psutil==5.9.5", + "psutil==5.9.6", "requests==2.31.0", "libtmux==0.23.2", "colorama==0.4.6", - "prometheus-client==0.17.1", - "optuna==3.3.0", - "plotly==5.17.0", - "pandas==2.1.1", - "dash==2.13.0", + "prometheus-client==0.18.0", + "optuna==3.4.0", + "plotly==5.18.0", + "pandas==2.1.2", + "dash==2.14.1", "future==0.18.3", "inquirer==3.1.3", - "scikit-learn~=1.3.1", + "scikit-learn==1.3.2", "tqdm==4.66.1", ] diff --git a/requirements.txt b/requirements.txt index 8147870..8a96130 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ -exchanges-wrapper==1.3.7.post4 +exchanges-wrapper>=1.4.0rc3 margin-strategy-sdk==0.0.11 jsonpickle==3.0.2 -psutil==5.9.5 +psutil==5.9.6 requests==2.31.0 libtmux==0.23.2 colorama==0.4.6 -prometheus-client==0.17.1 -optuna==3.3.0 -plotly==5.17.0 -pandas==2.1.1 -dash==2.13.0 +prometheus-client==0.18.0 +optuna==3.4.0 +plotly==5.18.0 +pandas==2.1.2 +dash==2.14.1 future==0.18.3 inquirer==3.1.3 -scikit-learn~=1.3.1 +scikit-learn==1.3.2 tqdm==4.66.1