Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.0.20 #83

Merged
merged 1 commit into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.0.20 - 2025-02-06
### Update
* `Bybit`: checking for the minimum/maximum Buy/Sell orders price is [excluded](https://announcements.bybit.com/article/title-adjustments-to-bybit-s-spot-trading-limit-order-mechanism-blt786c0c5abf865983/)
* Bump requirements
* Some minor improvements

## 3.0.19 - 2025-01-27
### Fix
* `on_balance_update_ex`: calculating initial balance for opposite coin in Reverse cycle
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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
* After update to `3.0.20` the config files `cli_XX_AAABBB.py` must be updated from [template](https://github.com/DogsTailFarmer/martin-binance/tree/public/martin_binance/templates)
* After update to `3.0.17`, the configuration file `exch_srv_cfg.toml` for [exchanges-wrapper](https://github.com/DogsTailFarmer/exchanges-wrapper) must be updated. [Use templates for reference.](https://github.com/DogsTailFarmer/exchanges-wrapper/blob/master/exchanges_wrapper/exch_srv_cfg.toml.template)
* After update to `3.0.17`, the configuration file `ms_cfg.toml` must be updated. [Use templates for reference.](https://github.com/DogsTailFarmer/martin-binance/blob/f0a0e5f9a7ceba3919ea0087f1b9f4e0d1bc95b6/martin_binance/templates/ms_cfg.toml)
* The config files `cli_XX_AAABBB.py` also must be updated from [template](https://github.com/DogsTailFarmer/martin-binance/tree/public/martin_binance/templates)
Expand Down
2 changes: 1 addition & 1 deletion martin_binance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.19"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"

Expand Down
57 changes: 30 additions & 27 deletions martin_binance/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.19"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = 'https://github.com/DogsTailFarmer'
##################################################################
Expand Down Expand Up @@ -129,6 +129,7 @@ def __init__(self, call_super=True):
self.tp_part_free = False # + Can use TP part amount for converting to grid orders
self.ts_grid_update = self.get_time() # - When updated grid
self.wait_wss_refresh = {} # -
self.place_grid_part_after_tp = True # -
#
schedule.every(5).minutes.do(self.event_grid_update)
schedule.every(5).seconds.do(self.event_processing)
Expand Down Expand Up @@ -460,8 +461,13 @@ def event_processing(self):
self.start_reverse_time = self.get_time()

def event_update_tp(self):
if ADAPTIVE_TRADE_CONDITION and self.stable_state() \
and self.tp_order_id and not self.tp_part_amount_first and self.get_time() - self.tp_order[3] > 60 * 15:
if (
ADAPTIVE_TRADE_CONDITION
and self.stable_state()
and self.tp_order_id
and self.get_time() - self.tp_order[3] > TP_REFRESH
and not self.tp_part_amount_first
):
self.message_log("Update TP order", color=Style.B_WHITE)
self.place_profit_order()

Expand Down Expand Up @@ -502,6 +508,7 @@ def _common_stable_conditions(self):
and not self.start_after_shift
and not self.tp_hold
and not self.tp_order_hold
and not self.tp_wait_id
and not self.orders_init
and self.command != 'stopped'
)
Expand Down Expand Up @@ -620,10 +627,24 @@ def restore_strategy_state(self, strategy_state: Dict[str, str] = None, restore=
self.message_log("Continue update grid", tlg=True)
self.grid_remove = True
self.cancel_grid()
elif not self.orders_grid and not self.orders_hold and not self.orders_save and not self.tp_order_id:
elif (
not self.orders_grid
and not self.orders_hold
and not self.orders_save
and not self.orders_init
and not self.tp_order_id
and not self.tp_wait_id
):
self.message_log("Restore, Restart", tlg=True)
self.start()
if not self.tp_order_id and self.stable_state():
if self.orders_init:
for order_id in self.orders_init.get_id_list():
self.message_log("Restore, wait grid orders", tlg=True)
self.check_created_order(order_id, "Grid order event was missed into reload")
if self.tp_wait_id:
self.message_log("Restore, wait TP order", tlg=True)
self.check_created_order(self.tp_wait_id, "TP order event was missed into reload")
elif not self.tp_order_id and self.stable_state():
self.message_log("Restore, no TP order, replace", tlg=True)
self.place_profit_order()

Expand Down Expand Up @@ -1857,8 +1878,7 @@ def place_grid_part(self) -> None:
i['buy'],
i['amount'],
i['price'],
check=True,
price_limit_rules=True
check=True
)
if waiting_order_id:
self.orders_init.append_order(waiting_order_id, i['buy'], i['amount'], i['price'])
Expand Down Expand Up @@ -2137,30 +2157,12 @@ def check_min_amount(self, amount=O_DEC, price=O_DEC, for_tp=True, by_market=Fal
_amount = self.deposit_first
return self.round_truncate(_amount, base=True) >= min_trade_amount

def place_limit_order_check(
self,
buy: bool,
amount: Decimal,
price: Decimal,
check=False,
price_limit_rules=False
) -> int:
def place_limit_order_check(self, buy: bool, amount: Decimal, price: Decimal, check=False) -> int:
"""
Before place limit order checking trade conditions and correct price
"""
if self.command == 'stopped':
return 0
if price_limit_rules:
tcm = self.get_trading_capability_manager()
_price = self.get_buffered_ticker().last_price or self.avg_rate
if ((buy and price < tcm.get_min_buy_price(_price)) or
(not buy and price > tcm.get_max_sell_price(_price))):
self.message_log(
f"{'Buy' if buy else 'Sell'} price {price} is out of trading range, will try later",
log_level=logging.WARNING,
color=Style.YELLOW
)
return 0
_price = price
if check:
order_book = self.get_buffered_order_book()
Expand Down Expand Up @@ -2572,7 +2574,7 @@ def on_place_order_success(self, place_order_id: int, order: Order) -> None:
if self.tp_hold or self.tp_cancel or self.tp_cancel_from_grid_handler:
self.cancel_order_id = self.tp_order_id
self.cancel_order(self.tp_order_id)
else:
elif self.place_grid_part_after_tp:
self.place_grid_part()
else:
self.message_log(f"Did not have waiting order {place_order_id}", logging.ERROR)
Expand All @@ -2585,6 +2587,7 @@ def on_place_order_error(self, place_order_id: int, error: str) -> None:
self.orders_init.remove(place_order_id)
self.orders_hold.orders_list.append(_order)
self.orders_hold.sort(self.cycle_buy)
self.place_grid_part_after_tp = False
if self.cancel_grid_hold:
self.message_log('Continue remove grid orders', color=Style.B_WHITE)
self.cancel_grid_hold = False
Expand Down
12 changes: 4 additions & 8 deletions martin_binance/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.2"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"

Expand Down Expand Up @@ -354,7 +354,7 @@ class TradingCapabilityManager:
"max_price",
)

def __init__(self, _exchange_info_symbol, price_limit_rules):
def __init__(self, _exchange_info_symbol):
self.base_asset_precision = int(_exchange_info_symbol.get('baseAssetPrecision'))
self.quote_asset_precision = int(_exchange_info_symbol.get('quoteAssetPrecision'))
self.min_qty = Decimal(_exchange_info_symbol['filters']['lotSize']['minQty'])
Expand All @@ -367,12 +367,8 @@ def __init__(self, _exchange_info_symbol, price_limit_rules):
self.tick_size = Decimal(_exchange_info_symbol['filters']['priceFilter']['tickSize'].rstrip('0'))
self.min_price = Decimal(_exchange_info_symbol['filters']['priceFilter']['minPrice'])
self.max_price = Decimal(_exchange_info_symbol['filters']['priceFilter']['maxPrice'])
if price_limit_rules:
self.multiplier_up = 1 + price_limit_rules / 100
self.multiplier_down = 1 - price_limit_rules / 100
else:
self.multiplier_up = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierUp'])
self.multiplier_down = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierDown'])
self.multiplier_up = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierUp'])
self.multiplier_down = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierDown'])

def __call__(self):
return self
Expand Down
8 changes: 4 additions & 4 deletions martin_binance/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.17"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"

Expand All @@ -15,9 +15,9 @@
__all__ = [
'SYMBOL', 'EXCHANGE', 'ID_EXCHANGE', 'FEE_MAKER', 'FEE_TAKER', 'FEE_FIRST', 'FEE_SECOND', 'FEE_BNB',
'SAVE_ASSET', 'GRID_MAX_COUNT', 'START_ON_BUY', 'AMOUNT_FIRST', 'USE_ALL_FUND', 'AMOUNT_SECOND',
'PRICE_SHIFT', 'PRICE_LIMIT_RULES', 'ROUND_BASE', 'ROUND_QUOTE', 'PROFIT', 'PROFIT_MAX', 'OVER_PRICE', 'ORDER_Q',
'PRICE_SHIFT', 'ROUND_BASE', 'ROUND_QUOTE', 'PROFIT', 'PROFIT_MAX', 'OVER_PRICE', 'ORDER_Q',
'MARTIN', 'SHIFT_GRID_DELAY', 'GRID_UPDATE_INTERVAL', 'STATUS_DELAY', 'GRID_ONLY', 'LOG_LEVEL',
'HOLD_TP_ORDER_TIMEOUT', 'COLLECT_ASSETS', 'GRID_ONLY_DELAY', 'ADAPTIVE_TRADE_CONDITION',
'HOLD_TP_ORDER_TIMEOUT', 'COLLECT_ASSETS', 'GRID_ONLY_DELAY', 'TP_REFRESH', 'ADAPTIVE_TRADE_CONDITION',
'BB_CANDLE_SIZE_IN_MINUTES', 'BB_NUMBER_OF_CANDLES', 'KBB', 'LINEAR_GRID_K', 'ADX_CANDLE_SIZE_IN_MINUTES',
'ADX_NUMBER_OF_CANDLES', 'ADX_PERIOD', 'ADX_THRESHOLD', 'ADX_PRICE_THRESHOLD', 'REVERSE', 'REVERSE_TARGET_AMOUNT',
'REVERSE_INIT_AMOUNT', 'REVERSE_STOP', 'HEAD_VERSION', 'LOAD_LAST_STATE', 'LAST_STATE_FILE', 'VPS_NAME', 'PARAMS',
Expand Down Expand Up @@ -48,7 +48,6 @@
USE_ALL_FUND = bool()
AMOUNT_SECOND = Decimal()
PRICE_SHIFT = Decimal()
PRICE_LIMIT_RULES = Decimal()
# Round pattern
ROUND_BASE = str()
ROUND_QUOTE = str()
Expand All @@ -67,6 +66,7 @@
HOLD_TP_ORDER_TIMEOUT = 30
COLLECT_ASSETS = bool()
GRID_ONLY_DELAY = 150 # sec delay before try restart GRID_ONLY cycle
TP_REFRESH = 60 * 10 # sec between TP refresh
#
ADAPTIVE_TRADE_CONDITION = bool()
BB_CANDLE_SIZE_IN_MINUTES = int()
Expand Down
42 changes: 26 additions & 16 deletions martin_binance/strategy_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.19"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"

Expand Down Expand Up @@ -262,6 +262,9 @@ def place_limit_order(self, buy: bool, amount: Decimal, price: Decimal) -> int:
def cancel_order(self, order_id: int, cancel_all=False) -> None:
self.tasks_manage(self.cancel_order_call(order_id, cancel_all))

def check_created_order(self, order_id: int, msg: str) -> None:
self.tasks_manage(self.fetch_created_order(order_id, msg))

def message_log(self, msg: str, log_level=logging.INFO, tlg=False, color=Style.WHITE, tlg_inline=False) -> None:
if prm.LOGGING:
if tlg and color == Style.WHITE:
Expand All @@ -277,8 +280,8 @@ def message_log(self, msg: str, log_level=logging.INFO, tlg=False, color=Style.W
tqdm.write(f"{datetime.fromtimestamp(self.get_time()).strftime('%H:%M:%S.%f')[:-3]} {color_msg}")
if prm.MODE in ('T', 'TC'):
logger.log(log_level, msg)
self.status_time = self.get_time()
if tlg and self.tlg_client:
self.status_time = self.get_time()
self.tasks_manage(
self.tlg_client.post_message(msg, inline_buttons=tlg_inline and prm.TLG_INLINE)
)
Expand Down Expand Up @@ -1013,7 +1016,7 @@ async def get_exchange_info(self, _request, _symbol):
self.message_log(f"Exception get_exchange_info: {_ex}")
else:
self.info_symbol = _exchange_info_symbol.to_pydict()
self.tcm = TradingCapabilityManager(self.info_symbol, prm.PRICE_LIMIT_RULES)
self.tcm = TradingCapabilityManager(self.info_symbol)
if prm.MODE == 'S':
break
await asyncio.sleep(600)
Expand Down Expand Up @@ -1131,15 +1134,18 @@ async def create_limit_order(self, _id: int, buy: bool, amount: str, price: str)
finally:
if prm.MODE in ('T', 'TC') and _fetch_order:
await asyncio.sleep(HEARTBEAT)
res = await self.fetch_order(0, str(_id), _filled_update_call=True)
if res.get('status') in ('NEW', 'PARTIALLY_FILLED', 'FILLED'):
await self.create_order_handler(_id, res)
else:
self.on_place_order_error(_id, msg)
await self.fetch_created_order(_id, msg)

async def fetch_created_order(self, _id, msg):
res = await self.fetch_order(0, str(_id), _filled_update_call=True)
if res.get('status') in ('NEW', 'PARTIALLY_FILLED', 'FILLED'):
await self.create_order_handler(_id, res)
else:
self.on_place_order_error(_id, msg)

async def create_order_handler(self, _id, result):
# print(f"create_order_handler.result: {result}")
if self.order_init_exist(_id) and not self.order_exist(result['orderId']):
if self.order_init_exist(_id): # and not self.order_exist(result['orderId']):
order = Order(result)
self.orders[order.id] = order
self.on_place_order_success(_id, order)
Expand Down Expand Up @@ -1437,8 +1443,13 @@ async def buffered_orders(self):
self.message_log(f"Trying set RATE_LIMITER to {self.rate_limiter}s", log_level=logging.WARNING)
await asyncio.sleep(ORDER_TIMEOUT)
try:
await self.send_request(self.stub.reset_rate_limit, mr.OpenClientConnectionId,
rate_limiter=self.rate_limiter)
res = await self.send_request(
self.stub.reset_rate_limit,
mr.OpenClientConnectionId,
rate_limiter=self.rate_limiter
)
if res and res.success:
self.message_log(f"RATE_LIMITER was set to {self.rate_limiter}s", log_level=logging.INFO)
except Exception as ex_4:
self.message_log(f"Exception buffered_orders 4: Set RATE_LIMITER failed: {ex_4}",
log_level=logging.WARNING)
Expand Down Expand Up @@ -1505,7 +1516,7 @@ async def wss_init(self):
await self.wss_wait_init()
else:
self.message_log("Init WSS failed, retry", log_level=logging.WARNING)
await asyncio.sleep(random.randint(HEARTBEAT, HEARTBEAT * 5)) # /NOSONAR
await asyncio.sleep(random.randint(HEARTBEAT, HEARTBEAT * 5)) # NOSONAR python:S2245
self.wss_fire_up = True

def wss_cancel_tasks(self):
Expand Down Expand Up @@ -1560,8 +1571,8 @@ async def main(self, _symbol): # /NOSONAR
else:
active_orders = list(map(json.loads, _active_orders.orders))
for order in active_orders:
print(f"Order: {order['orderId']}, side: {order['side']}, amount: {order['origQty']}"
f" price:{order['price']}, status: {order['status']}")
print(f"Order: {order['orderId']}({order['clientOrderId']}), side: {order['side']},"
f" amount: {order['origQty']}, price:{order['price']}, status: {order['status']}")
# Try load last strategy state from saved files
last_state = load_last_state(prm.LAST_STATE_FILE)
restore_state = bool(last_state)
Expand Down Expand Up @@ -1696,9 +1707,8 @@ async def main(self, _symbol): # /NOSONAR
)

self.orders = jsonpickle.decode(last_state.pop(MS_ORDERS, '{}'), keys=True)
orders_keys = self.orders.keys()
for _id in exch_orders_ids:
if _id not in orders_keys:
if _id not in self.orders.keys():
_order = next((_o for _o in active_orders if int(_o["orderId"]) == _id))
self.orders[_id] = Order(_order)
self.message_log(
Expand Down
9 changes: 8 additions & 1 deletion martin_binance/telegram_proxy/tlg_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2025 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.17"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"

Expand Down Expand Up @@ -107,6 +107,9 @@ async def connect(self):
delay += random.randint(1, 15) # NOSONAR python:S2245
logger.warning(f"Try connecting to Telegram proxy, retrying in {delay} second... ")
await asyncio.sleep(delay)
except ssl.SSLCertVerificationError as e:
logger.error(f"Connect to Telegram proxy failed: {e}")
break

async def post_message(self, text, inline_buttons=False, reraise=False) -> tlg.Response:
try:
Expand All @@ -125,6 +128,8 @@ async def post_message(self, text, inline_buttons=False, reraise=False) -> tlg.R
self.tasks_manage(self.connect())
elif reraise:
raise
except ssl.SSLCertVerificationError as e:
logger.error(f"Post message to Telegram proxy failed: {e}")
except (asyncio.CancelledError, KeyboardInterrupt):
pass # user interrupt

Expand All @@ -141,6 +146,8 @@ async def get_update(self) -> tlg.Response:
self.tasks_manage(self.connect())
except (asyncio.CancelledError, KeyboardInterrupt):
pass # user interrupt
except ssl.SSLCertVerificationError as e:
logger.error(f"Get update from Telegram proxy failed: {e}")

def close(self):
self.channel.close()
Expand Down
4 changes: 1 addition & 3 deletions martin_binance/templates/cli_0_BTCUSDT.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
__author__ = "Jerry Fedorenko"
__copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM"
__license__ = "MIT"
__version__ = "3.0.17"
__version__ = "3.0.20"
__maintainer__ = "Jerry Fedorenko"
__contact__ = "https://github.com/DogsTailFarmer"
"""
Expand Down Expand Up @@ -57,8 +57,6 @@
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 = Decimal('0.01') # 'No market' shift price in % from current bid/ask price
# Search next parameter on Bybit https://www.bybit.com/en/announcement-info/spot-trading-rules/
ex.PRICE_LIMIT_RULES = Decimal('0') # +-% from last ticker price. Use on Bybit only. 0 - disable
# Round pattern, set pattern 1.0123456789 or if not set used exchange settings
ex.ROUND_BASE = str()
ex.ROUND_QUOTE = str()
Expand Down
Loading
Loading