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.
Files changed (79) hide show
  1. {pybinbot-2.1.0/pybinbot.egg-info → pybinbot-2.1.2}/PKG-INFO +18 -2
  2. {pybinbot-2.1.0 → pybinbot-2.1.2}/README.md +16 -0
  3. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/__init__.py +2 -0
  4. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/futures.py +27 -8
  5. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/bot_base.py +31 -1
  6. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/enums.py +5 -0
  7. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/socket_manager.py +3 -3
  8. {pybinbot-2.1.0 → pybinbot-2.1.2/pybinbot.egg-info}/PKG-INFO +18 -2
  9. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/SOURCES.txt +1 -0
  10. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/requires.txt +1 -1
  11. {pybinbot-2.1.0 → pybinbot-2.1.2}/pyproject.toml +2 -2
  12. pybinbot-2.1.2/tests/test_conditional_entry.py +45 -0
  13. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_futures_place_order.py +113 -0
  14. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_kucoin_api.py +51 -0
  15. {pybinbot-2.1.0 → pybinbot-2.1.2}/LICENSE +0 -0
  16. {pybinbot-2.1.0 → pybinbot-2.1.2}/MANIFEST.in +0 -0
  17. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binance/base.py +0 -0
  18. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binance/exceptions.py +0 -0
  19. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binbot/base.py +0 -0
  20. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/binbot/exceptions.py +0 -0
  21. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/coingecko.py +0 -0
  22. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/base.py +0 -0
  23. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/exceptions.py +0 -0
  24. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/market.py +0 -0
  25. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/orders.py +0 -0
  26. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/apis/kucoin/rest.py +0 -0
  27. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/__init__.py +0 -0
  28. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/autotrade_settings.py +0 -0
  29. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/bot.py +0 -0
  30. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/deal.py +0 -0
  31. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/derivatives.py +0 -0
  32. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/grid_ladder.py +0 -0
  33. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/order.py +0 -0
  34. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/routes.py +0 -0
  35. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/signals.py +0 -0
  36. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/models/symbol.py +0 -0
  37. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/py.typed +0 -0
  38. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/__init__.py +0 -0
  39. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/beta.py +0 -0
  40. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/breadth.py +0 -0
  41. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/cache.py +0 -0
  42. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/candles.py +0 -0
  43. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/handlers.py +0 -0
  44. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/heikin_ashi.py +0 -0
  45. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/indicators.py +0 -0
  46. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/logging_config.py +0 -0
  47. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/maths.py +0 -0
  48. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/price_crossings.py +0 -0
  49. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/telegram_delivery.py +0 -0
  50. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/timestamps.py +0 -0
  51. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/types.py +0 -0
  52. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/shared/utils.py +0 -0
  53. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/async_socket_client.py +0 -0
  54. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/binance/socket_client.py +0 -0
  55. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot/streaming/kucoin/kucoin_async_client.py +0 -0
  56. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/dependency_links.txt +0 -0
  57. {pybinbot-2.1.0 → pybinbot-2.1.2}/pybinbot.egg-info/top_level.txt +0 -0
  58. {pybinbot-2.1.0 → pybinbot-2.1.2}/setup.cfg +0 -0
  59. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_beta.py +0 -0
  60. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_binbot_api.py +0 -0
  61. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_bot_futures_leverage.py +0 -0
  62. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_breadth.py +0 -0
  63. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_candles.py +0 -0
  64. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_coingecko.py +0 -0
  65. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_conditional_futures_orders.py +0 -0
  66. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_deal_model.py +0 -0
  67. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_derivatives_api.py +0 -0
  68. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_grid_ladder.py +0 -0
  69. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_handlers.py +0 -0
  70. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_heikin_ashi.py +0 -0
  71. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_indicators.py +0 -0
  72. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_logging_config.py +0 -0
  73. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_matching_engine.py +0 -0
  74. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_maths.py +0 -0
  75. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_price_crossings.py +0 -0
  76. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_symbol_model.py +0 -0
  77. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_telegram_delivery.py +0 -0
  78. {pybinbot-2.1.0 → pybinbot-2.1.2}/tests/test_timestamps.py +0 -0
  79. {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.0
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.0
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). If provided, stop_price and
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). Required when
580
- stop is set.
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 or stop_price_type is None:
631
- raise ValueError(
632
- "stop_price and stop_price_type must be provided when stop is set"
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:
@@ -60,6 +60,11 @@ class Status(str, Enum):
60
60
  error = "error"
61
61
 
62
62
 
63
+ class EntryMode(str, Enum):
64
+ immediate = "immediate"
65
+ conditional_limit = "conditional_limit"
66
+
67
+
63
68
  class Position(str, Enum):
64
69
  long = "long"
65
70
  short = "short"
@@ -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(threading.Thread):
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
- threading.Thread.__init__(self)
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.0
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.0
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
@@ -57,6 +57,7 @@ tests/test_bot_futures_leverage.py
57
57
  tests/test_breadth.py
58
58
  tests/test_candles.py
59
59
  tests/test_coingecko.py
60
+ tests/test_conditional_entry.py
60
61
  tests/test_conditional_futures_orders.py
61
62
  tests/test_deal_model.py
62
63
  tests/test_derivatives_api.py
@@ -3,7 +3,7 @@ numpy==2.5.4
3
3
  pandas==3.1.1
4
4
  pandas-stubs==3.1.0.261029
5
5
  requests>=2.32.5
6
- kucoin-universal-sdk>=1.3.0
6
+ kucoin-universal-sdk>=1.3.2
7
7
  aiohttp<3.15,>=3.14.1
8
8
  websocket-client>=1.9.0
9
9
  python-dotenv>=1.2.1
@@ -22,7 +22,7 @@ build-backend = "setuptools.build_meta"
22
22
 
23
23
  [project]
24
24
  name = "pybinbot"
25
- version = "2.1.0"
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.0",
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