pybinbot 2.1.0__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pybinbot-2.1.0/pybinbot.egg-info → pybinbot-2.1.2}/PKG-INFO +18 -2
- {pybinbot-2.1.0 → pybinbot-2.1.2}/README.md +16 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/__init__.py +2 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/futures.py +27 -8
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/bot_base.py +31 -1
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/enums.py +5 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/socket_manager.py +3 -3
- {pybinbot-2.1.0 → pybinbot-2.1.2/pybinbot.egg-info}/PKG-INFO +18 -2
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/SOURCES.txt +1 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/requires.txt +1 -1
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pyproject.toml +2 -2
- pybinbot-2.1.2/tests/test_conditional_entry.py +45 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_futures_place_order.py +113 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_kucoin_api.py +51 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/LICENSE +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/MANIFEST.in +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binance/base.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binance/exceptions.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binbot/base.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binbot/exceptions.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/coingecko.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/base.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/exceptions.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/market.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/orders.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/rest.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/__init__.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/autotrade_settings.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/bot.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/deal.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/derivatives.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/grid_ladder.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/order.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/routes.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/signals.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/symbol.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/py.typed +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/__init__.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/beta.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/breadth.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/cache.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/candles.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/handlers.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/heikin_ashi.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/indicators.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/logging_config.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/maths.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/price_crossings.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/telegram_delivery.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/timestamps.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/types.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/utils.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/async_socket_client.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/socket_client.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/kucoin/kucoin_async_client.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/dependency_links.txt +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/top_level.txt +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/setup.cfg +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_beta.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_binbot_api.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_bot_futures_leverage.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_breadth.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_candles.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_coingecko.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_conditional_futures_orders.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_deal_model.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_derivatives_api.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_grid_ladder.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_handlers.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_heikin_ashi.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_indicators.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_logging_config.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_matching_engine.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_maths.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_price_crossings.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_symbol_model.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_telegram_delivery.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_timestamps.py +0 -0
- {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybinbot
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Utility functions for the binbot project.
|
|
5
5
|
Author-email: Carlos Wu <carkodw@gmail.com>
|
|
6
6
|
Requires-Python: >=3.15
|
|
@@ -11,7 +11,7 @@ Requires-Dist: numpy==2.5.4
|
|
|
11
11
|
Requires-Dist: pandas==3.1.1
|
|
12
12
|
Requires-Dist: pandas-stubs==3.1.0.261029
|
|
13
13
|
Requires-Dist: requests>=2.32.5
|
|
14
|
-
Requires-Dist: kucoin-universal-sdk>=1.3.
|
|
14
|
+
Requires-Dist: kucoin-universal-sdk>=1.3.2
|
|
15
15
|
Requires-Dist: aiohttp<3.15,>=3.14.1
|
|
16
16
|
Requires-Dist: websocket-client>=1.9.0
|
|
17
17
|
Requires-Dist: python-dotenv>=1.2.1
|
|
@@ -54,6 +54,22 @@ uv sync --extra dev
|
|
|
54
54
|
|
|
55
55
|
`--extra dev` also installs development tools like ruff and mypy
|
|
56
56
|
|
|
57
|
+
## Futures order triggers
|
|
58
|
+
|
|
59
|
+
`KucoinFutures.place_futures_order()` defaults every conditional order to KuCoin's
|
|
60
|
+
last traded price (`stopPriceType=TP`) when `stop_price_type` is omitted or `None`.
|
|
61
|
+
This applies to both buy and sell orders, stop-market and stop-limit orders, and
|
|
62
|
+
opening and reducing orders. Explicit mark/index trigger types remain supported.
|
|
63
|
+
Orders without `stop` do not send a trigger-price type. Trigger levels, limit
|
|
64
|
+
prices, quantities, leverage, and fill handling are unchanged.
|
|
65
|
+
|
|
66
|
+
Last price can fluctuate more than mark price, but selecting it does not guarantee
|
|
67
|
+
the highest selling price or lowest buying price; fills depend on the order book.
|
|
68
|
+
|
|
69
|
+
`KucoinFutures.get_ticker_price(symbol)` returns the last traded contract price
|
|
70
|
+
from the futures ticker. The spot client exposes the same method for spot prices;
|
|
71
|
+
`KucoinFutures.get_mark_price(symbol)` remains available for mark prices.
|
|
72
|
+
|
|
57
73
|
## Telegram delivery
|
|
58
74
|
|
|
59
75
|
Import `TelegramDelivery` from `pybinbot.shared.telegram_delivery`. Initialization
|
|
@@ -25,6 +25,22 @@ uv sync --extra dev
|
|
|
25
25
|
|
|
26
26
|
`--extra dev` also installs development tools like ruff and mypy
|
|
27
27
|
|
|
28
|
+
## Futures order triggers
|
|
29
|
+
|
|
30
|
+
`KucoinFutures.place_futures_order()` defaults every conditional order to KuCoin's
|
|
31
|
+
last traded price (`stopPriceType=TP`) when `stop_price_type` is omitted or `None`.
|
|
32
|
+
This applies to both buy and sell orders, stop-market and stop-limit orders, and
|
|
33
|
+
opening and reducing orders. Explicit mark/index trigger types remain supported.
|
|
34
|
+
Orders without `stop` do not send a trigger-price type. Trigger levels, limit
|
|
35
|
+
prices, quantities, leverage, and fill handling are unchanged.
|
|
36
|
+
|
|
37
|
+
Last price can fluctuate more than mark price, but selecting it does not guarantee
|
|
38
|
+
the highest selling price or lowest buying price; fills depend on the order book.
|
|
39
|
+
|
|
40
|
+
`KucoinFutures.get_ticker_price(symbol)` returns the last traded contract price
|
|
41
|
+
from the futures ticker. The spot client exposes the same method for spot prices;
|
|
42
|
+
`KucoinFutures.get_mark_price(symbol)` remains available for mark prices.
|
|
43
|
+
|
|
28
44
|
## Telegram delivery
|
|
29
45
|
|
|
30
46
|
Import `TelegramDelivery` from `pybinbot.shared.telegram_delivery`. Initialization
|
|
@@ -38,6 +38,7 @@ from pybinbot.shared.enums import (
|
|
|
38
38
|
BinanceOrderModel,
|
|
39
39
|
CloseConditions,
|
|
40
40
|
DealType,
|
|
41
|
+
EntryMode,
|
|
41
42
|
ExchangeId,
|
|
42
43
|
KucoinKlineIntervals,
|
|
43
44
|
MarketDominance,
|
|
@@ -282,6 +283,7 @@ __all__ = [
|
|
|
282
283
|
# enums
|
|
283
284
|
"CloseConditions",
|
|
284
285
|
"DealType",
|
|
286
|
+
"EntryMode",
|
|
285
287
|
"BinanceOrderModel",
|
|
286
288
|
"Status",
|
|
287
289
|
"Position",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from decimal import Decimal
|
|
3
|
+
from math import isfinite
|
|
3
4
|
from pybinbot import KucoinRest, KucoinKlineIntervals, OrderType, OrderStatus, DealType
|
|
4
5
|
from requests import HTTPError, request
|
|
5
6
|
from uuid import uuid4
|
|
@@ -27,6 +28,7 @@ from kucoin_universal_sdk.generate.futures.market import (
|
|
|
27
28
|
GetMarkPriceReqBuilder,
|
|
28
29
|
GetSymbolReqBuilder,
|
|
29
30
|
GetSymbolResp,
|
|
31
|
+
GetTickerReqBuilder,
|
|
30
32
|
)
|
|
31
33
|
from kucoin_universal_sdk.generate.futures.fundingfees import (
|
|
32
34
|
GetPublicFundingHistoryReqBuilder,
|
|
@@ -204,6 +206,23 @@ class KucoinFutures(KucoinRest):
|
|
|
204
206
|
if item.funding_rate is not None
|
|
205
207
|
]
|
|
206
208
|
|
|
209
|
+
def get_ticker_price(self, symbol: str) -> float:
|
|
210
|
+
"""Return the last traded futures price from the contract ticker."""
|
|
211
|
+
req = GetTickerReqBuilder().set_symbol(symbol).build()
|
|
212
|
+
response = self.futures_market_api.get_ticker(req)
|
|
213
|
+
if response is None:
|
|
214
|
+
raise ValueError(f"KuCoin futures ticker returned no response for {symbol}")
|
|
215
|
+
if response.price is None:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"KuCoin futures ticker returned no price for {symbol}: {response!r}"
|
|
218
|
+
)
|
|
219
|
+
price = float(response.price)
|
|
220
|
+
if not isfinite(price) or price <= 0:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"KuCoin futures ticker returned an invalid price for {symbol}"
|
|
223
|
+
)
|
|
224
|
+
return price
|
|
225
|
+
|
|
207
226
|
def get_mark_price(self, symbol: str) -> float:
|
|
208
227
|
req = GetMarkPriceReqBuilder().set_symbol(symbol).build()
|
|
209
228
|
response = self.futures_market_api.get_mark_price(req)
|
|
@@ -573,11 +592,11 @@ class KucoinFutures(KucoinRest):
|
|
|
573
592
|
margin_mode: Optional margin mode, "ISOLATED" or "CROSS".
|
|
574
593
|
reduce_only: Optional reduce-only flag.
|
|
575
594
|
close_order: Optional close-position flag.
|
|
576
|
-
stop: Optional stop direction (DOWN/UP).
|
|
577
|
-
stop_price_type must also be set.
|
|
595
|
+
stop: Optional stop direction (DOWN/UP). Requires stop_price.
|
|
578
596
|
stop_price: Optional stop trigger price. Required when stop is set.
|
|
579
|
-
stop_price_type: Optional stop price type (TP/MP/IP).
|
|
580
|
-
|
|
597
|
+
stop_price_type: Optional stop price type (TP/MP/IP). Defaults to
|
|
598
|
+
last traded price (TP) for all conditional orders. An explicit
|
|
599
|
+
value overrides the default. Has no effect without stop.
|
|
581
600
|
post_only: Whether a limit order must rest as maker liquidity.
|
|
582
601
|
client_oid: Caller-owned idempotency key; generated when omitted.
|
|
583
602
|
"""
|
|
@@ -627,10 +646,10 @@ class KucoinFutures(KucoinRest):
|
|
|
627
646
|
|
|
628
647
|
# Optional stop-loss / take-profit trigger parameters
|
|
629
648
|
if stop is not None:
|
|
630
|
-
if stop_price is None
|
|
631
|
-
raise ValueError(
|
|
632
|
-
|
|
633
|
-
|
|
649
|
+
if stop_price is None:
|
|
650
|
+
raise ValueError("stop_price must be provided when stop is set")
|
|
651
|
+
if stop_price_type is None:
|
|
652
|
+
stop_price_type = AddOrderReq.StopPriceTypeEnum.TRADE_PRICE
|
|
634
653
|
builder = builder.set_stop(stop)
|
|
635
654
|
builder = builder.set_stop_price(str(stop_price))
|
|
636
655
|
builder = builder.set_stop_price_type(stop_price_type)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
from uuid import UUID
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
4
|
+
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
|
|
5
5
|
|
|
6
6
|
from pybinbot.shared.enums import (
|
|
7
7
|
BinanceKlineIntervals,
|
|
8
8
|
CloseConditions,
|
|
9
|
+
EntryMode,
|
|
9
10
|
MarketType,
|
|
10
11
|
Position,
|
|
11
12
|
QuoteAssets,
|
|
@@ -46,6 +47,14 @@ class BotBase(BaseModel):
|
|
|
46
47
|
)
|
|
47
48
|
created_at: float = Field(default_factory=timestamp)
|
|
48
49
|
updated_at: float = Field(default_factory=timestamp)
|
|
50
|
+
entry_mode: EntryMode = Field(default=EntryMode.immediate, validate_default=True)
|
|
51
|
+
entry_price: float | None = Field(default=None, ge=0, allow_inf_nan=False)
|
|
52
|
+
entry_expires_at: float = Field(
|
|
53
|
+
default_factory=lambda data: data["created_at"] + 6 * 60 * 60 * 1000,
|
|
54
|
+
gt=0,
|
|
55
|
+
allow_inf_nan=False,
|
|
56
|
+
description="Absolute entry deadline in Unix milliseconds; defaults to creation + 6 hours",
|
|
57
|
+
)
|
|
49
58
|
dynamic_trailing: bool = Field(default=False)
|
|
50
59
|
dynamic_position_sizing: bool = Field(default=False)
|
|
51
60
|
logs: list = Field(default=[])
|
|
@@ -104,6 +113,27 @@ class BotBase(BaseModel):
|
|
|
104
113
|
},
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
@field_validator("entry_mode", mode="before")
|
|
117
|
+
@classmethod
|
|
118
|
+
def default_entry_mode(cls, value):
|
|
119
|
+
return EntryMode.immediate if value is None else value
|
|
120
|
+
|
|
121
|
+
@field_validator("entry_expires_at", mode="before")
|
|
122
|
+
@classmethod
|
|
123
|
+
def default_entry_expiry(cls, value, info: ValidationInfo):
|
|
124
|
+
return info.data["created_at"] + 6 * 60 * 60 * 1000 if value is None else value
|
|
125
|
+
|
|
126
|
+
@model_validator(mode="after")
|
|
127
|
+
def validate_conditional_entry(self):
|
|
128
|
+
if self.entry_mode == EntryMode.conditional_limit:
|
|
129
|
+
if self.entry_price is None or self.entry_price <= 0:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"Conditional limit entry requires a positive entry_price"
|
|
132
|
+
)
|
|
133
|
+
if self.market_type != MarketType.FUTURES:
|
|
134
|
+
raise ValueError("Conditional limit entry currently requires futures")
|
|
135
|
+
return self
|
|
136
|
+
|
|
107
137
|
@model_validator(mode="after")
|
|
108
138
|
def validate_dynamic_position_sizing(self):
|
|
109
139
|
if self.dynamic_position_sizing:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
import threading
|
|
4
3
|
import time
|
|
4
|
+
from threading import Thread
|
|
5
5
|
|
|
6
6
|
from websocket import (
|
|
7
7
|
ABNF,
|
|
@@ -11,7 +11,7 @@ from websocket import (
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class BinanceSocketManager(
|
|
14
|
+
class BinanceSocketManager(Thread):
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
17
|
stream_url,
|
|
@@ -23,7 +23,7 @@ class BinanceSocketManager(threading.Thread):
|
|
|
23
23
|
on_pong=None,
|
|
24
24
|
logger=None,
|
|
25
25
|
):
|
|
26
|
-
|
|
26
|
+
Thread.__init__(self)
|
|
27
27
|
if not logger:
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
self.logger = logger
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybinbot
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Utility functions for the binbot project.
|
|
5
5
|
Author-email: Carlos Wu <carkodw@gmail.com>
|
|
6
6
|
Requires-Python: >=3.15
|
|
@@ -11,7 +11,7 @@ Requires-Dist: numpy==2.5.4
|
|
|
11
11
|
Requires-Dist: pandas==3.1.1
|
|
12
12
|
Requires-Dist: pandas-stubs==3.1.0.261029
|
|
13
13
|
Requires-Dist: requests>=2.32.5
|
|
14
|
-
Requires-Dist: kucoin-universal-sdk>=1.3.
|
|
14
|
+
Requires-Dist: kucoin-universal-sdk>=1.3.2
|
|
15
15
|
Requires-Dist: aiohttp<3.15,>=3.14.1
|
|
16
16
|
Requires-Dist: websocket-client>=1.9.0
|
|
17
17
|
Requires-Dist: python-dotenv>=1.2.1
|
|
@@ -54,6 +54,22 @@ uv sync --extra dev
|
|
|
54
54
|
|
|
55
55
|
`--extra dev` also installs development tools like ruff and mypy
|
|
56
56
|
|
|
57
|
+
## Futures order triggers
|
|
58
|
+
|
|
59
|
+
`KucoinFutures.place_futures_order()` defaults every conditional order to KuCoin's
|
|
60
|
+
last traded price (`stopPriceType=TP`) when `stop_price_type` is omitted or `None`.
|
|
61
|
+
This applies to both buy and sell orders, stop-market and stop-limit orders, and
|
|
62
|
+
opening and reducing orders. Explicit mark/index trigger types remain supported.
|
|
63
|
+
Orders without `stop` do not send a trigger-price type. Trigger levels, limit
|
|
64
|
+
prices, quantities, leverage, and fill handling are unchanged.
|
|
65
|
+
|
|
66
|
+
Last price can fluctuate more than mark price, but selecting it does not guarantee
|
|
67
|
+
the highest selling price or lowest buying price; fills depend on the order book.
|
|
68
|
+
|
|
69
|
+
`KucoinFutures.get_ticker_price(symbol)` returns the last traded contract price
|
|
70
|
+
from the futures ticker. The spot client exposes the same method for spot prices;
|
|
71
|
+
`KucoinFutures.get_mark_price(symbol)` remains available for mark prices.
|
|
72
|
+
|
|
57
73
|
## Telegram delivery
|
|
58
74
|
|
|
59
75
|
Import `TelegramDelivery` from `pybinbot.shared.telegram_delivery`. Initialization
|
|
@@ -22,7 +22,7 @@ build-backend = "setuptools.build_meta"
|
|
|
22
22
|
|
|
23
23
|
[project]
|
|
24
24
|
name = "pybinbot"
|
|
25
|
-
version = "2.1.
|
|
25
|
+
version = "2.1.2"
|
|
26
26
|
description = "Utility functions for the binbot project."
|
|
27
27
|
authors = [
|
|
28
28
|
{ name="Carlos Wu", email="carkodw@gmail.com" }
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
"pandas==3.1.1",
|
|
36
36
|
"pandas-stubs==3.1.0.261029",
|
|
37
37
|
"requests>=2.32.5",
|
|
38
|
-
"kucoin-universal-sdk>=1.3.
|
|
38
|
+
"kucoin-universal-sdk>=1.3.2",
|
|
39
39
|
"aiohttp>=3.14.1,<3.15",
|
|
40
40
|
"websocket-client>=1.9.0",
|
|
41
41
|
"python-dotenv>=1.2.1",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import ValidationError
|
|
3
|
+
|
|
4
|
+
from pybinbot import BotBase, EntryMode, MarketType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"overrides",
|
|
9
|
+
[{}, {"entry_mode": None, "entry_expires_at": None, "entry_price": None}],
|
|
10
|
+
)
|
|
11
|
+
def test_omitted_or_null_entry_fields_default_to_immediate_and_six_hours(overrides):
|
|
12
|
+
bot = BotBase(pair="XBTUSDTM", created_at=1000, **overrides)
|
|
13
|
+
assert bot.entry_mode == EntryMode.immediate
|
|
14
|
+
assert bot.entry_expires_at == 21_601_000
|
|
15
|
+
assert bot.entry_price is None
|
|
16
|
+
assert bot.model_dump(mode="json")["entry_price"] is None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_conditional_entry_serializes_strings_and_preserves_deadline():
|
|
20
|
+
bot = BotBase(
|
|
21
|
+
pair="XBTUSDTM",
|
|
22
|
+
market_type=MarketType.FUTURES,
|
|
23
|
+
entry_mode="conditional_limit",
|
|
24
|
+
entry_price=100,
|
|
25
|
+
entry_expires_at=9000,
|
|
26
|
+
)
|
|
27
|
+
assert bot.model_dump(mode="json")["entry_mode"] == "conditional_limit"
|
|
28
|
+
assert bot.entry_expires_at == 9000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"values",
|
|
33
|
+
[
|
|
34
|
+
{"entry_mode": "unknown"},
|
|
35
|
+
{"entry_mode": "conditional_limit"},
|
|
36
|
+
{"entry_mode": "conditional_limit", "entry_price": None},
|
|
37
|
+
{"entry_mode": "conditional_limit", "entry_price": 0},
|
|
38
|
+
{"entry_mode": "conditional_limit", "entry_price": -1},
|
|
39
|
+
{"entry_mode": "conditional_limit", "entry_price": float("inf")},
|
|
40
|
+
{"entry_expires_at": float("nan")},
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_invalid_entry_configuration_is_rejected(values):
|
|
44
|
+
with pytest.raises(ValidationError):
|
|
45
|
+
BotBase(pair="XBTUSDTM", market_type=MarketType.FUTURES, **values)
|
|
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
|
|
|
10
10
|
|
|
11
11
|
import pytest
|
|
12
12
|
from kucoin_universal_sdk.generate.futures.order.model_add_order_req import AddOrderReq
|
|
13
|
+
|
|
13
14
|
from pybinbot import KucoinFutures, OrderStatus, OrderType
|
|
14
15
|
|
|
15
16
|
|
|
@@ -113,3 +114,115 @@ def test_place_futures_order_marks_zero_fill_done_order_as_canceled_not_filled()
|
|
|
113
114
|
|
|
114
115
|
assert result.status == OrderStatus.CANCELED
|
|
115
116
|
assert result.qty == 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.parametrize("order_type", [OrderType.limit, OrderType.market])
|
|
120
|
+
@pytest.mark.parametrize(
|
|
121
|
+
"side,stop,reduce_only",
|
|
122
|
+
[
|
|
123
|
+
pytest.param("sell", "down", True, id="long-stop-loss"),
|
|
124
|
+
pytest.param("buy", "up", True, id="short-stop-loss"),
|
|
125
|
+
pytest.param("sell", "up", True, id="long-take-profit"),
|
|
126
|
+
pytest.param("buy", "down", True, id="short-take-profit"),
|
|
127
|
+
pytest.param("buy", "up", False, id="conditional-long-entry"),
|
|
128
|
+
pytest.param("sell", "down", False, id="conditional-short-entry"),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
def test_all_conditional_order_paths_default_to_last_trade(
|
|
132
|
+
order_type, side, stop, reduce_only
|
|
133
|
+
):
|
|
134
|
+
f = _make_futures()
|
|
135
|
+
f.futures_order_api.add_order.return_value = types.SimpleNamespace(
|
|
136
|
+
order_id="conditional-order"
|
|
137
|
+
)
|
|
138
|
+
f.retrieve_order = lambda oid: types.SimpleNamespace(
|
|
139
|
+
status=types.SimpleNamespace(value="open"),
|
|
140
|
+
filled_size="0",
|
|
141
|
+
avg_deal_price="0",
|
|
142
|
+
created_at=1000000,
|
|
143
|
+
type=types.SimpleNamespace(value=order_type.value.lower()),
|
|
144
|
+
time_in_force="GTC",
|
|
145
|
+
side=types.SimpleNamespace(value=side),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
with patch("pybinbot.apis.kucoin.futures.sleep"):
|
|
149
|
+
result = f.place_futures_order(
|
|
150
|
+
symbol="XBTUSDTM",
|
|
151
|
+
side=AddOrderReq.SideEnum(side),
|
|
152
|
+
size=8,
|
|
153
|
+
leverage=3,
|
|
154
|
+
order_type=order_type,
|
|
155
|
+
price=97.5 if order_type == OrderType.limit else None,
|
|
156
|
+
stop=AddOrderReq.StopEnum(stop),
|
|
157
|
+
stop_price=98,
|
|
158
|
+
reduce_only=reduce_only,
|
|
159
|
+
client_oid="conditional-oid",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
f.futures_order_api.add_order.assert_called_once()
|
|
163
|
+
request = f.futures_order_api.add_order.call_args.args[0]
|
|
164
|
+
assert request.stop_price_type == AddOrderReq.StopPriceTypeEnum.TRADE_PRICE
|
|
165
|
+
assert request.stop_price == "98"
|
|
166
|
+
assert request.stop.value == stop
|
|
167
|
+
assert request.side.value == side
|
|
168
|
+
assert request.type.value == order_type.value.lower()
|
|
169
|
+
assert request.price == ("97.5" if order_type == OrderType.limit else None)
|
|
170
|
+
assert request.size == 8
|
|
171
|
+
assert request.leverage == 3
|
|
172
|
+
assert request.reduce_only is reduce_only
|
|
173
|
+
assert request.client_oid == "conditional-oid"
|
|
174
|
+
assert result.stop_price == 98
|
|
175
|
+
assert result.status == OrderStatus.NEW
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize(
|
|
179
|
+
"trigger_type",
|
|
180
|
+
[
|
|
181
|
+
None,
|
|
182
|
+
AddOrderReq.StopPriceTypeEnum.MARK_PRICE,
|
|
183
|
+
AddOrderReq.StopPriceTypeEnum.INDEX_PRICE,
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
def test_conditional_trigger_uses_last_trade_for_none_and_respects_explicit_types(
|
|
187
|
+
trigger_type,
|
|
188
|
+
):
|
|
189
|
+
f = _make_futures()
|
|
190
|
+
f.futures_order_api.add_order.return_value = None
|
|
191
|
+
|
|
192
|
+
with pytest.raises(RuntimeError, match="not acknowledged"):
|
|
193
|
+
f.place_futures_order(
|
|
194
|
+
symbol="XBTUSDTM",
|
|
195
|
+
side=AddOrderReq.SideEnum.SELL,
|
|
196
|
+
size=8,
|
|
197
|
+
order_type=OrderType.market,
|
|
198
|
+
stop=AddOrderReq.StopEnum.DOWN,
|
|
199
|
+
stop_price=98,
|
|
200
|
+
stop_price_type=trigger_type,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
request = f.futures_order_api.add_order.call_args.args[0]
|
|
204
|
+
assert request.stop_price_type == (
|
|
205
|
+
trigger_type
|
|
206
|
+
if trigger_type is not None
|
|
207
|
+
else AddOrderReq.StopPriceTypeEnum.TRADE_PRICE
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("order_type", [OrderType.limit, OrderType.market])
|
|
212
|
+
def test_ordinary_orders_do_not_send_conditional_trigger_fields(order_type):
|
|
213
|
+
f = _make_futures()
|
|
214
|
+
f.futures_order_api.add_order.return_value = None
|
|
215
|
+
|
|
216
|
+
with pytest.raises(RuntimeError, match="not acknowledged"):
|
|
217
|
+
f.place_futures_order(
|
|
218
|
+
symbol="XBTUSDTM",
|
|
219
|
+
side=AddOrderReq.SideEnum.BUY,
|
|
220
|
+
size=8,
|
|
221
|
+
order_type=order_type,
|
|
222
|
+
price=100 if order_type == OrderType.limit else None,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
request = f.futures_order_api.add_order.call_args.args[0]
|
|
226
|
+
assert request.stop is None
|
|
227
|
+
assert request.stop_price is None
|
|
228
|
+
assert request.stop_price_type is None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from types import SimpleNamespace
|
|
2
|
+
from unittest.mock import Mock
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
@@ -27,6 +28,56 @@ def test_get_ticker_price_returns_float_when_price_is_available():
|
|
|
27
28
|
assert api.get_ticker_price("BTC-USDT") == 12.34
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def test_futures_ticker_returns_last_trade_instead_of_spot_mark_or_book():
|
|
32
|
+
api = object.__new__(KucoinFutures)
|
|
33
|
+
ticker = Mock(
|
|
34
|
+
return_value=SimpleNamespace(
|
|
35
|
+
price="101.5", best_bid_price="101", best_ask_price="102"
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
api.futures_market_api = SimpleNamespace(
|
|
39
|
+
get_ticker=ticker, get_mark_price=Mock(return_value=100)
|
|
40
|
+
)
|
|
41
|
+
spot_ticker = Mock(return_value=SimpleNamespace(price="99"))
|
|
42
|
+
api.spot_api = SimpleNamespace(get_ticker=spot_ticker)
|
|
43
|
+
|
|
44
|
+
assert api.get_ticker_price("XBTUSDTM") == 101.5
|
|
45
|
+
|
|
46
|
+
ticker.assert_called_once()
|
|
47
|
+
assert ticker.call_args.args[0].symbol == "XBTUSDTM"
|
|
48
|
+
api.futures_market_api.get_mark_price.assert_not_called()
|
|
49
|
+
spot_ticker.assert_not_called()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_futures_ticker_raises_clear_error_when_response_is_missing():
|
|
53
|
+
api = object.__new__(KucoinFutures)
|
|
54
|
+
api.futures_market_api = SimpleNamespace(get_ticker=Mock(return_value=None))
|
|
55
|
+
|
|
56
|
+
with pytest.raises(ValueError, match="KuCoin futures ticker returned no response"):
|
|
57
|
+
api.get_ticker_price("XBTUSDTM")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_futures_ticker_raises_clear_error_when_price_is_missing():
|
|
61
|
+
api = object.__new__(KucoinFutures)
|
|
62
|
+
api.futures_market_api = SimpleNamespace(
|
|
63
|
+
get_ticker=Mock(return_value=SimpleNamespace(price=None))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(ValueError, match="KuCoin futures ticker returned no price"):
|
|
67
|
+
api.get_ticker_price("XBTUSDTM")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize("price", ["0", "-1", "nan", "inf", "invalid"])
|
|
71
|
+
def test_futures_ticker_rejects_invalid_last_trade_prices(price):
|
|
72
|
+
api = object.__new__(KucoinFutures)
|
|
73
|
+
api.futures_market_api = SimpleNamespace(
|
|
74
|
+
get_ticker=Mock(return_value=SimpleNamespace(price=price))
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
with pytest.raises(ValueError):
|
|
78
|
+
api.get_ticker_price("XBTUSDTM")
|
|
79
|
+
|
|
80
|
+
|
|
30
81
|
def test_get_mark_price_raises_clear_error_when_value_is_missing():
|
|
31
82
|
api = object.__new__(KucoinFutures)
|
|
32
83
|
api.futures_market_api = SimpleNamespace(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|