pybinbot 0.1.6__py3-none-any.whl

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.
models/bot_base.py ADDED
@@ -0,0 +1,99 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from shared.types import Amount
3
+ from shared.timestamps import timestamp
4
+ from shared.enums import (
5
+ QuoteAssets,
6
+ BinanceKlineIntervals,
7
+ CloseConditions,
8
+ Status,
9
+ Strategy,
10
+ )
11
+ from shared.timestamps import ts_to_humandate
12
+
13
+
14
+ class BotBase(BaseModel):
15
+ pair: str
16
+ fiat: str = Field(default="USDC")
17
+ quote_asset: QuoteAssets = Field(default=QuoteAssets.USDC)
18
+ fiat_order_size: Amount = Field(
19
+ default=0, ge=0, description="Min Binance 0.0001 BNB approx 15USD"
20
+ )
21
+ candlestick_interval: BinanceKlineIntervals = Field(
22
+ default=BinanceKlineIntervals.fifteen_minutes
23
+ )
24
+ close_condition: CloseConditions = Field(default=CloseConditions.dynamic_trailling)
25
+ cooldown: int = Field(
26
+ default=0,
27
+ ge=0,
28
+ description="cooldown period in minutes before opening next bot with same pair",
29
+ )
30
+ created_at: float = Field(default_factory=timestamp)
31
+ updated_at: float = Field(default_factory=timestamp)
32
+ dynamic_trailling: bool = Field(default=False)
33
+ logs: list = Field(default=[])
34
+ mode: str = Field(default="manual")
35
+ name: str = Field(
36
+ default="terminal",
37
+ description="Algorithm name or 'terminal' if executed from React app",
38
+ )
39
+ status: Status = Field(default=Status.inactive)
40
+ stop_loss: Amount = Field(
41
+ default=0, ge=-1, le=101, description="If stop_loss > 0, allow for reversal"
42
+ )
43
+ margin_short_reversal: bool = Field(
44
+ default=False,
45
+ description="Autoswitch from long to short or short to long strategy",
46
+ )
47
+ take_profit: Amount = Field(default=0, ge=-1, le=101)
48
+ trailling: bool = Field(default=False)
49
+ trailling_deviation: Amount = Field(
50
+ default=0,
51
+ ge=-1,
52
+ le=101,
53
+ description="Trailling activation (first take profit hit)",
54
+ )
55
+ trailling_profit: Amount = Field(default=0, ge=-1, le=101)
56
+ strategy: Strategy = Field(default=Strategy.long)
57
+ model_config = {
58
+ "from_attributes": True,
59
+ "use_enum_values": True,
60
+ "json_schema_extra": {
61
+ "description": "Most fields are optional. Deal and orders fields are generated internally and filled by Exchange",
62
+ "examples": [
63
+ {
64
+ "pair": "BNBUSDT",
65
+ "fiat": "USDC",
66
+ "quote_asset": "USDC",
67
+ "fiat_order_size": 15,
68
+ "candlestick_interval": "15m",
69
+ "close_condition": "dynamic_trailling",
70
+ "cooldown": 0,
71
+ "created_at": 1702999999.0,
72
+ "updated_at": 1702999999.0,
73
+ "dynamic_trailling": False,
74
+ "logs": [],
75
+ "mode": "manual",
76
+ "name": "Default bot",
77
+ "status": "inactive",
78
+ "stop_loss": 0,
79
+ "take_profit": 2.3,
80
+ "trailling": True,
81
+ "trailling_deviation": 0.63,
82
+ "trailling_profit": 2.3,
83
+ "margin_short_reversal": False,
84
+ "strategy": "long",
85
+ }
86
+ ],
87
+ },
88
+ }
89
+
90
+ @field_validator("pair")
91
+ @classmethod
92
+ def check_pair_not_empty(cls, v):
93
+ assert v != "", "Pair field must be filled."
94
+ return v
95
+
96
+ def add_log(self, message: str) -> str:
97
+ timestamped_message = f"[{ts_to_humandate(timestamp())}] {message}"
98
+ self.logs.append(timestamped_message)
99
+ return self.logs[-1]
models/deal.py ADDED
@@ -0,0 +1,70 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from shared.types import Amount
3
+
4
+
5
+ class DealBase(BaseModel):
6
+ """
7
+ Data model that is used for operations,
8
+ so it should all be numbers (int or float)
9
+ """
10
+
11
+ base_order_size: Amount = Field(default=0, gt=-1)
12
+ current_price: Amount = Field(default=0)
13
+ take_profit_price: Amount = Field(default=0)
14
+ trailling_stop_loss_price: Amount = Field(
15
+ default=0,
16
+ description="take_profit but for trailling, to avoid confusion, trailling_profit_price always be > trailling_stop_loss_price",
17
+ )
18
+ trailling_profit_price: Amount = Field(default=0)
19
+ stop_loss_price: Amount = Field(default=0)
20
+
21
+ # fields for margin trading
22
+ total_interests: float = Field(default=0, gt=-1)
23
+ total_commissions: float = Field(default=0, gt=-1)
24
+ margin_loan_id: int = Field(
25
+ default=0,
26
+ ge=0,
27
+ description="Txid from Binance. This is used to check if there is a loan, 0 means no loan",
28
+ )
29
+ margin_repay_id: int = Field(
30
+ default=0, ge=0, description="= 0, it has not been repaid"
31
+ )
32
+
33
+ # Refactored deal prices that combine both margin and spot
34
+ opening_price: Amount = Field(
35
+ default=0,
36
+ description="replaces previous buy_price or short_sell_price/margin_short_sell_price",
37
+ )
38
+ opening_qty: Amount = Field(
39
+ default=0,
40
+ description="replaces previous buy_total_qty or short_sell_qty/margin_short_sell_qty",
41
+ )
42
+ opening_timestamp: int = Field(default=0)
43
+ closing_price: Amount = Field(
44
+ default=0,
45
+ description="replaces previous sell_price or short_sell_price/margin_short_sell_price",
46
+ )
47
+ closing_qty: Amount = Field(
48
+ default=0,
49
+ description="replaces previous sell_qty or short_sell_qty/margin_short_sell_qty",
50
+ )
51
+ closing_timestamp: int = Field(
52
+ default=0,
53
+ description="replaces previous buy_timestamp or margin/short_sell timestamps",
54
+ )
55
+
56
+ @field_validator("margin_loan_id", mode="before")
57
+ @classmethod
58
+ def validate_margin_loan_id(cls, value):
59
+ if isinstance(value, float):
60
+ return int(value)
61
+ else:
62
+ return value
63
+
64
+ @field_validator("margin_loan_id", mode="after")
65
+ @classmethod
66
+ def cast_float(cls, value):
67
+ if isinstance(value, float):
68
+ return int(value)
69
+ else:
70
+ return value
models/order.py ADDED
@@ -0,0 +1,104 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from shared.types import Amount
3
+ from shared.enums import (
4
+ DealType,
5
+ OrderStatus,
6
+ )
7
+
8
+
9
+ class OrderBase(BaseModel):
10
+ order_type: str = Field(
11
+ description="Because every exchange has different naming, we should keep it as a str rather than OrderType enum"
12
+ )
13
+ time_in_force: str
14
+ timestamp: int = Field(default=0)
15
+ order_id: int | str = Field(
16
+ description="Because every exchange has id type, we should keep it as looose as possible. Int is for backwards compatibility"
17
+ )
18
+ order_side: str = Field(
19
+ description="Because every exchange has different naming, we should keep it as a str rather than OrderType enum"
20
+ )
21
+ pair: str
22
+ qty: float
23
+ status: OrderStatus
24
+ price: float
25
+ deal_type: DealType
26
+ model_config = {
27
+ "from_attributes": True,
28
+ "use_enum_values": True,
29
+ "json_schema_extra": {
30
+ "description": "Most fields are optional. Deal field is generated internally, orders are filled up by Exchange",
31
+ "examples": [
32
+ {
33
+ "order_type": "LIMIT",
34
+ "time_in_force": "GTC",
35
+ "timestamp": 0,
36
+ "order_id": 0,
37
+ "order_side": "BUY",
38
+ "pair": "",
39
+ "qty": 0,
40
+ "status": "",
41
+ "price": 0,
42
+ }
43
+ ],
44
+ },
45
+ }
46
+
47
+
48
+ class DealModel(BaseModel):
49
+ base_order_size: Amount = Field(default=0, gt=-1)
50
+ current_price: Amount = Field(default=0)
51
+ take_profit_price: Amount = Field(default=0)
52
+ trailling_stop_loss_price: Amount = Field(
53
+ default=0,
54
+ description="take_profit but for trailling, to avoid confusion, trailling_profit_price always be > trailling_stop_loss_price",
55
+ )
56
+ trailling_profit_price: Amount = Field(default=0)
57
+ stop_loss_price: Amount = Field(default=0)
58
+ total_interests: float = Field(default=0, gt=-1)
59
+ total_commissions: float = Field(default=0, gt=-1)
60
+ margin_loan_id: int = Field(
61
+ default=0,
62
+ ge=0,
63
+ description="Txid from Binance. This is used to check if there is a loan, 0 means no loan",
64
+ )
65
+ margin_repay_id: int = Field(
66
+ default=0, ge=0, description="= 0, it has not been repaid"
67
+ )
68
+ opening_price: Amount = Field(
69
+ default=0,
70
+ description="replaces previous buy_price or short_sell_price/margin_short_sell_price",
71
+ )
72
+ opening_qty: Amount = Field(
73
+ default=0,
74
+ description="replaces previous buy_total_qty or short_sell_qty/margin_short_sell_qty",
75
+ )
76
+ opening_timestamp: int = Field(default=0)
77
+ closing_price: Amount = Field(
78
+ default=0,
79
+ description="replaces previous sell_price or short_sell_price/margin_short_sell_price",
80
+ )
81
+ closing_qty: Amount = Field(
82
+ default=0,
83
+ description="replaces previous sell_qty or short_sell_qty/margin_short_sell_qty",
84
+ )
85
+ closing_timestamp: int = Field(
86
+ default=0,
87
+ description="replaces previous buy_timestamp or margin/short_sell timestamps",
88
+ )
89
+
90
+ @field_validator("margin_loan_id", mode="before")
91
+ @classmethod
92
+ def validate_margin_loan_id(cls, value):
93
+ if isinstance(value, float):
94
+ return int(value)
95
+ else:
96
+ return value
97
+
98
+ @field_validator("margin_loan_id", mode="after")
99
+ @classmethod
100
+ def cast_float(cls, value):
101
+ if isinstance(value, float):
102
+ return int(value)
103
+ else:
104
+ return value
models/signals.py ADDED
@@ -0,0 +1,51 @@
1
+ from pydantic import BaseModel, Field, field_validator, ConfigDict
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+
6
+ # Example shared model (copy actual model code from source files)
7
+ class HABollinguerSpread(BaseModel):
8
+ """
9
+ Pydantic model for the Bollinguer spread.
10
+ (optional)
11
+ """
12
+
13
+ bb_high: float
14
+ bb_mid: float
15
+ bb_low: float
16
+
17
+
18
+ class SignalsConsumer(BaseModel):
19
+ """
20
+ Pydantic model for the signals consumer.
21
+ """
22
+
23
+ type: str = Field(default="signal")
24
+ date: str = Field(
25
+ default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
26
+ )
27
+ spread: Optional[float] = Field(default=0)
28
+ current_price: Optional[float] = Field(default=0)
29
+ msg: str
30
+ symbol: str
31
+ algo: str
32
+ bot_strategy: str = Field(default="long")
33
+ bb_spreads: Optional[HABollinguerSpread]
34
+ autotrade: bool = Field(default=True, description="If it is in testing mode, False")
35
+
36
+ model_config = ConfigDict(
37
+ extra="allow",
38
+ use_enum_values=True,
39
+ )
40
+
41
+ @field_validator("spread", "current_price")
42
+ @classmethod
43
+ def name_must_contain_space(cls, v):
44
+ if v is None:
45
+ return 0
46
+ elif isinstance(v, str):
47
+ return float(v)
48
+ elif isinstance(v, float):
49
+ return v
50
+ else:
51
+ raise ValueError("must be a float or 0")
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybinbot
3
+ Version: 0.1.6
4
+ Summary: Utility functions for the binbot project.
5
+ Author-email: Carlos Wu <carkodw@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pydantic[email]>=2.0.0
10
+ Requires-Dist: numpy==2.2.0
11
+ Requires-Dist: pandas>=2.2.3
12
+ Requires-Dist: pymongo==4.6.3
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
15
+ Requires-Dist: ruff; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # PyBinbot
19
+
20
+ Utility functions for the binbot project. Most of the code here is not runnable, there's no server or individual scripts, you simply move code to here when it's used in both binbot and binquant.
21
+
22
+ ``pybinbot`` is the public API module for the distribution.
23
+
24
+ This module re-exports the internal ``shared`` and ``models`` packages and the most commonly used helpers and enums so consumers can simply::
25
+
26
+ from pybinbot import round_numbers, ExchangeId
27
+
28
+ The implementation deliberately avoids importing heavy third-party libraries at module import time.
29
+
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ uv sync --extra dev
35
+ ```
36
+
37
+ `--extra dev` also installs development tools like ruff and mypy
38
+
39
+
40
+ ## Publishing
41
+
42
+ ```bash
43
+ make bump-patch
44
+ ```
45
+ or
46
+
47
+ ```bash
48
+ make bump-minor
49
+ ```
50
+
51
+ or
52
+
53
+ ```bash
54
+ make bump-major
55
+ ```
56
+
57
+ For further commands take a look at the `Makefile` such as testing `make test`
@@ -0,0 +1,15 @@
1
+ pybinbot.py,sha256=bLB9AYwuk3cbQ7HVlVGwTtzqNSV3FsZv4Rtbetg0Kas,1920
2
+ models/bot_base.py,sha256=z9hSK7uVLW4oDmPdtntYozo8RnQnpZtFi8fL1r9Qd5Q,3593
3
+ models/deal.py,sha256=jOdMSobN_K4-be3hG38l0WeZq5ln5JndA-BBgMUElsI,2402
4
+ models/order.py,sha256=FHs7qi2JNYn-kXfWD2m5oFSSeMQDKmw0uPDwOvy5KkQ,3592
5
+ models/signals.py,sha256=DAcV2ft6n5iJW4kqspdfEakFZ3igx97erwyTiyDMlgM,1356
6
+ pybinbot-0.1.6.dist-info/licenses/LICENSE,sha256=ECEAqAQ81zTT8PeN7gYqbkZtewkyeleEqQ26MxuHQxs,938
7
+ shared/enums.py,sha256=b472TAbRrnznDRDDVrLW_2I_9dURafqeC3pMu4DHQ1w,6730
8
+ shared/logging_config.py,sha256=XZblKXH9KsLUDbIJqFRZPzI0h17-CRBZH4KktVak-TI,1144
9
+ shared/maths.py,sha256=JjlrgV0INlJG4Zj28ahRkfzcI2Ec4noilrDwX2p82TM,3207
10
+ shared/timestamps.py,sha256=401JkggjW--trNenxkUBEObnWyy9Cd-L3xVpqdbW8Tc,2587
11
+ shared/types.py,sha256=KfuJzjsbMUHFcBaQ6ZXUbuSyFHbHqehgeY73Zt8lqO8,173
12
+ pybinbot-0.1.6.dist-info/METADATA,sha256=LEBxxxJ0TBmDQ77xJpskIZYprx8_5y52oRgDFxsRf60,1358
13
+ pybinbot-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ pybinbot-0.1.6.dist-info/top_level.txt,sha256=rfaU2KRcKvquGQYwN5weorBMzgHpWW4eZlITOQwjRvw,23
15
+ pybinbot-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ models
2
+ pybinbot
3
+ shared
pybinbot.py ADDED
@@ -0,0 +1,93 @@
1
+ from shared.maths import (
2
+ supress_trailling,
3
+ round_numbers,
4
+ round_numbers_ceiling,
5
+ round_numbers_floor,
6
+ supress_notation,
7
+ interval_to_millisecs,
8
+ format_ts,
9
+ zero_remainder,
10
+ )
11
+ from shared.timestamps import (
12
+ timestamp,
13
+ round_timestamp,
14
+ ts_to_day,
15
+ ms_to_sec,
16
+ sec_to_ms,
17
+ ts_to_humandate,
18
+ timestamp_to_datetime,
19
+ )
20
+ from shared.enums import (
21
+ CloseConditions,
22
+ KafkaTopics,
23
+ DealType,
24
+ BinanceOrderModel,
25
+ Status,
26
+ Strategy,
27
+ OrderType,
28
+ TimeInForce,
29
+ OrderSide,
30
+ OrderStatus,
31
+ TrendEnum,
32
+ BinanceKlineIntervals,
33
+ KucoinKlineIntervals,
34
+ AutotradeSettingsDocument,
35
+ UserRoles,
36
+ QuoteAssets,
37
+ ExchangeId,
38
+ MarketDominance,
39
+ )
40
+ from shared.types import Amount
41
+ from shared.logging_config import configure_logging
42
+ from models.bot_base import BotBase
43
+ from models.order import OrderBase
44
+ from models.deal import DealBase
45
+ from models.signals import HABollinguerSpread, SignalsConsumer
46
+
47
+ __all__ = [
48
+ # models
49
+ "BotBase",
50
+ "OrderBase",
51
+ "DealBase",
52
+ # misc
53
+ "Amount",
54
+ "configure_logging",
55
+ # maths helpers
56
+ "supress_trailling",
57
+ "round_numbers",
58
+ "round_numbers_ceiling",
59
+ "round_numbers_floor",
60
+ "supress_notation",
61
+ "interval_to_millisecs",
62
+ "format_ts",
63
+ "zero_remainder",
64
+ # timestamp helpers
65
+ "timestamp",
66
+ "round_timestamp",
67
+ "ts_to_day",
68
+ "ms_to_sec",
69
+ "sec_to_ms",
70
+ "ts_to_humandate",
71
+ "timestamp_to_datetime",
72
+ # enums
73
+ "CloseConditions",
74
+ "KafkaTopics",
75
+ "DealType",
76
+ "BinanceOrderModel",
77
+ "Status",
78
+ "Strategy",
79
+ "OrderType",
80
+ "TimeInForce",
81
+ "OrderSide",
82
+ "OrderStatus",
83
+ "TrendEnum",
84
+ "BinanceKlineIntervals",
85
+ "KucoinKlineIntervals",
86
+ "AutotradeSettingsDocument",
87
+ "UserRoles",
88
+ "QuoteAssets",
89
+ "ExchangeId",
90
+ "HABollinguerSpread",
91
+ "SignalsConsumer",
92
+ "MarketDominance",
93
+ ]
shared/enums.py ADDED
@@ -0,0 +1,272 @@
1
+ from enum import Enum
2
+ from pydantic import BaseModel, field_validator
3
+
4
+
5
+ class CloseConditions(str, Enum):
6
+ dynamic_trailling = "dynamic_trailling"
7
+ # No trailling, standard stop loss
8
+ timestamp = "timestamp"
9
+ # binbot-research param (self.market_trend_reversal)
10
+ market_reversal = "market_reversal"
11
+
12
+
13
+ class KafkaTopics(str, Enum):
14
+ klines_store_topic = "klines-store-topic"
15
+ technical_indicators = "technical-indicators"
16
+ signals = "signals"
17
+ restart_streaming = "restart-streaming"
18
+ restart_autotrade = "restart-autotrade"
19
+
20
+
21
+ class DealType(str, Enum):
22
+ base_order = "base_order"
23
+ take_profit = "take_profit"
24
+ stop_loss = "stop_loss"
25
+ short_sell = "short_sell"
26
+ short_buy = "short_buy"
27
+ margin_short = "margin_short"
28
+ panic_close = "panic_close"
29
+
30
+
31
+ class BinanceOrderModel(BaseModel):
32
+ """
33
+ Data model given by Binance,
34
+ therefore it should be strings
35
+ """
36
+
37
+ order_type: str
38
+ time_in_force: str
39
+ timestamp: int
40
+ order_id: int
41
+ order_side: str
42
+ pair: str
43
+ qty: float
44
+ status: str
45
+ price: float
46
+ deal_type: DealType
47
+
48
+ @field_validator("timestamp", "order_id", "price", "qty", "order_id")
49
+ @classmethod
50
+ def validate_str_numbers(cls, v):
51
+ if isinstance(v, float):
52
+ return v
53
+ elif isinstance(v, int):
54
+ return v
55
+ elif isinstance(v, str):
56
+ return float(v)
57
+ else:
58
+ raise ValueError(f"{v} must be a number")
59
+
60
+
61
+ class Status(str, Enum):
62
+ all = "all"
63
+ inactive = "inactive"
64
+ active = "active"
65
+ completed = "completed"
66
+ error = "error"
67
+
68
+
69
+ class Strategy(str, Enum):
70
+ long = "long"
71
+ margin_short = "margin_short"
72
+
73
+
74
+ class OrderType(str, Enum):
75
+ limit = "LIMIT"
76
+ market = "MARKET"
77
+ stop_loss = "STOP_LOSS"
78
+ stop_loss_limit = "STOP_LOSS_LIMIT"
79
+ take_profit = "TAKE_PROFIT"
80
+ take_profit_limit = "TAKE_PROFIT_LIMIT"
81
+ limit_maker = "LIMIT_MAKER"
82
+
83
+
84
+ class TimeInForce(str, Enum):
85
+ gtc = "GTC"
86
+ ioc = "IOC"
87
+ fok = "FOK"
88
+
89
+
90
+ class OrderSide(str, Enum):
91
+ buy = "BUY"
92
+ sell = "SELL"
93
+
94
+
95
+ class OrderStatus(str, Enum):
96
+ """
97
+ Must be all uppercase for SQL alchemy
98
+ and Alembic to do migration properly
99
+ """
100
+
101
+ NEW = "NEW"
102
+ PARTIALLY_FILLED = "PARTIALLY_FILLED"
103
+ FILLED = "FILLED"
104
+ CANCELED = "CANCELED"
105
+ REJECTED = "REJECTED"
106
+ EXPIRED = "EXPIRED"
107
+
108
+
109
+ class TrendEnum(str, Enum):
110
+ up_trend = "uptrend"
111
+ down_trend = "downtrend"
112
+ neutral = None
113
+
114
+
115
+ class DealType(str, Enum):
116
+ base_order = "base_order"
117
+ take_profit = "take_profit"
118
+ stop_loss = "stop_loss"
119
+ short_sell = "short_sell"
120
+ short_buy = "short_buy"
121
+ margin_short = "margin_short"
122
+ panic_close = "panic_close"
123
+ trailling_profit = "trailling_profit"
124
+ conversion = "conversion" # converts one crypto to another
125
+
126
+
127
+ class BinanceKlineIntervals(str, Enum):
128
+ one_minute = "1m"
129
+ three_minutes = "3m"
130
+ five_minutes = "5m"
131
+ fifteen_minutes = "15m"
132
+ thirty_minutes = "30m"
133
+ one_hour = "1h"
134
+ two_hours = "2h"
135
+ four_hours = "4h"
136
+ six_hours = "6h"
137
+ eight_hours = "8h"
138
+ twelve_hours = "12h"
139
+ one_day = "1d"
140
+ three_days = "3d"
141
+ one_week = "1w"
142
+ one_month = "1M"
143
+
144
+ def bin_size(self):
145
+ return int(self.value[:-1])
146
+
147
+ def unit(self):
148
+ if self.value[-1:] == "m":
149
+ return "minute"
150
+ elif self.value[-1:] == "h":
151
+ return "hour"
152
+ elif self.value[-1:] == "d":
153
+ return "day"
154
+ elif self.value[-1:] == "w":
155
+ return "week"
156
+ elif self.value[-1:] == "M":
157
+ return "month"
158
+
159
+ def to_kucoin_interval(self) -> str:
160
+ """
161
+ Convert Binance interval format to Kucoin interval format.
162
+
163
+ Binance: 1m, 5m, 15m, 1h, 4h, 1d, 1w
164
+ Kucoin: 1min, 5min, 15min, 1hour, 4hour, 1day, 1week
165
+ """
166
+ interval_map = {
167
+ "1m": "1min",
168
+ "3m": "3min",
169
+ "5m": "5min",
170
+ "15m": "15min",
171
+ "30m": "30min",
172
+ "1h": "1hour",
173
+ "2h": "2hour",
174
+ "4h": "4hour",
175
+ "6h": "6hour",
176
+ "8h": "8hour",
177
+ "12h": "12hour",
178
+ "1d": "1day",
179
+ "3d": "3day",
180
+ "1w": "1week",
181
+ "1M": "1month",
182
+ }
183
+ return interval_map.get(self.value, self.value)
184
+
185
+
186
+ class KucoinKlineIntervals(str, Enum):
187
+ ONE_MINUTE = "1min"
188
+ THREE_MINUTES = "3min"
189
+ FIVE_MINUTES = "5min"
190
+ FIFTEEN_MINUTES = "15min"
191
+ THIRTY_MINUTES = "30min"
192
+ ONE_HOUR = "1hour"
193
+ TWO_HOURS = "2hour"
194
+ FOUR_HOURS = "4hour"
195
+ SIX_HOURS = "6hour"
196
+ EIGHT_HOURS = "8hour"
197
+ TWELVE_HOURS = "12hour"
198
+ ONE_DAY = "1day"
199
+ ONE_WEEK = "1week"
200
+
201
+ # Helper to calculate interval duration in milliseconds
202
+ def get_interval_ms(interval_str: str) -> int:
203
+ """Convert Kucoin interval string to milliseconds"""
204
+ interval_map = {
205
+ "1min": 60 * 1000,
206
+ "3min": 3 * 60 * 1000,
207
+ "5min": 5 * 60 * 1000,
208
+ "15min": 15 * 60 * 1000,
209
+ "30min": 30 * 60 * 1000,
210
+ "1hour": 60 * 60 * 1000,
211
+ "2hour": 2 * 60 * 60 * 1000,
212
+ "4hour": 4 * 60 * 60 * 1000,
213
+ "6hour": 6 * 60 * 60 * 1000,
214
+ "8hour": 8 * 60 * 60 * 1000,
215
+ "12hour": 12 * 60 * 60 * 1000,
216
+ "1day": 24 * 60 * 60 * 1000,
217
+ "1week": 7 * 24 * 60 * 60 * 1000,
218
+ }
219
+ return interval_map.get(interval_str, 60 * 1000) # Default to 1 minute
220
+
221
+
222
+ class AutotradeSettingsDocument(str, Enum):
223
+ # Autotrade settings for test bots
224
+ test_autotrade_settings = "test_autotrade_settings"
225
+ # Autotrade settings for real bots
226
+ settings = "autotrade_settings"
227
+
228
+
229
+ class UserRoles(str, Enum):
230
+ # Full access to all resources
231
+ user = "user"
232
+ # Access to terminal and customer accounts
233
+ admin = "admin"
234
+ # Only access to funds and client website
235
+ customer = "customer"
236
+
237
+
238
+ class QuoteAssets(str, Enum):
239
+ """
240
+ Quote assets supported by Binbot orders
241
+ Includes both crypto assets and fiat currencies
242
+ """
243
+
244
+ # Crypto assets
245
+ USDT = "USDT"
246
+ USDC = "USDC"
247
+ BTC = "BTC"
248
+ ETH = "ETH"
249
+ # Backwards compatibility
250
+ TRY = "TRY"
251
+
252
+ def is_fiat(self) -> bool:
253
+ """Check if the asset is a fiat currency"""
254
+ return self.value in ["TRY", "EUR", "USD"]
255
+
256
+ @classmethod
257
+ def get_fiat_currencies(cls) -> list["QuoteAssets"]:
258
+ """
259
+ Get all fiat currencies
260
+ """
261
+ return [asset for asset in cls if asset.is_fiat()]
262
+
263
+
264
+ class ExchangeId(str, Enum):
265
+ KUCOIN = "kucoin"
266
+ BINANCE = "binance"
267
+
268
+
269
+ class MarketDominance(str, Enum):
270
+ NEUTRAL = "neutral"
271
+ GAINERS = "gainers"
272
+ LOSERS = "losers"
@@ -0,0 +1,42 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ from typing import Iterable
5
+
6
+ DEFAULT_FORMAT = "%(asctime)s.%(msecs)03d UTC %(levelname)s %(name)s: %(message)s"
7
+ DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S"
8
+
9
+
10
+ def configure_logging(
11
+ *,
12
+ level: str | None = None,
13
+ fmt: str = DEFAULT_FORMAT,
14
+ datefmt: str = DEFAULT_DATEFMT,
15
+ utc: bool = True,
16
+ force: bool = True,
17
+ quiet_loggers: Iterable[str] | None = ("uvicorn", "confluent_kafka"),
18
+ ) -> None:
19
+ """
20
+ Configure root logging consistently across API services.
21
+ """
22
+ resolved_level = str(level or os.environ.get("LOG_LEVEL", "INFO")).upper()
23
+ logging.basicConfig(
24
+ level=resolved_level,
25
+ format=fmt,
26
+ datefmt=datefmt,
27
+ force=force,
28
+ )
29
+ if utc:
30
+ logging.Formatter.converter = time.gmtime
31
+
32
+ if quiet_loggers:
33
+ quiet_level = os.environ.get("QUIET_LIB_LOG_LEVEL", "WARNING").upper()
34
+ for name in quiet_loggers:
35
+ logging.getLogger(name).setLevel(quiet_level)
36
+
37
+ logging.getLogger(__name__).debug(
38
+ "Logging configured (level=%s, utc=%s, force=%s)",
39
+ resolved_level,
40
+ utc,
41
+ force,
42
+ )
shared/maths.py ADDED
@@ -0,0 +1,123 @@
1
+ import math
2
+ import re
3
+ from decimal import Decimal
4
+ from datetime import datetime
5
+ from typing import cast
6
+
7
+
8
+ def ensure_float(value: str | int | float) -> float:
9
+ if isinstance(value, str) or isinstance(value, int):
10
+ return float(value)
11
+
12
+ return value
13
+
14
+
15
+ def supress_trailling(value: str | float | int) -> float:
16
+ """
17
+ Supress trailing 0s without changing the numeric value.
18
+
19
+ Also attempts to normalise away scientific notation for small
20
+ numbers while preserving the original value.
21
+ """
22
+ # Use Decimal(str(value)) to avoid binary float rounding issues
23
+ dec = Decimal(str(value))
24
+ # Normalise to remove redundant trailing zeros while preserving value
25
+ dec = dec.normalize()
26
+ return float(dec)
27
+
28
+
29
+ def round_numbers(value: float | int, decimals=6) -> float | int:
30
+ decimal_points = 10 ** int(decimals)
31
+ number = float(value)
32
+ result = math.floor(number * decimal_points) / decimal_points
33
+ if decimals == 0:
34
+ result = int(result)
35
+ return result
36
+
37
+
38
+ def round_numbers_ceiling(value, decimals=6):
39
+ decimal_points = 10 ** int(decimals)
40
+ number = float(value)
41
+ result = math.ceil(number * decimal_points) / decimal_points
42
+ if decimals == 0:
43
+ result = int(result)
44
+ return float(result)
45
+
46
+
47
+ def round_numbers_floor(value, decimals=6):
48
+ decimal_points = 10 ** int(decimals)
49
+ number = float(value)
50
+ result = math.floor(number * decimal_points) / decimal_points
51
+ if decimals == 0:
52
+ result = int(result)
53
+ return float(result)
54
+
55
+
56
+ def supress_notation(num: float, precision: int = 0) -> str:
57
+ """
58
+ Supress scientific notation and return a fixed-point string.
59
+
60
+ Examples
61
+ -------
62
+ 8e-5 -> "0.00008" (precision=5)
63
+ 123.456, precision=2 -> "123.46"
64
+ """
65
+ dec = Decimal(str(num))
66
+
67
+ if precision >= 0:
68
+ # Quantize to the requested number of decimal places using
69
+ # Decimal's standard rounding (half even by default).
70
+ quant = Decimal(1).scaleb(-precision) # 10**-precision
71
+ dec = dec.quantize(quant)
72
+ decimal_points = precision
73
+ else:
74
+ # Let Decimal decide the scale, then format with all significant
75
+ # decimal places.
76
+ dec = dec.normalize()
77
+ exp = cast(int, dec.as_tuple().exponent)
78
+ decimal_points = -exp
79
+
80
+ return f"{dec:.{decimal_points}f}"
81
+
82
+
83
+ def interval_to_millisecs(interval: str) -> int:
84
+ time, notation = re.findall(r"[A-Za-z]+|\d+", interval)
85
+ if notation == "m":
86
+ # minutes
87
+ return int(time) * 60 * 1000
88
+
89
+ if notation == "h":
90
+ # hours
91
+ return int(time) * 60 * 60 * 1000
92
+
93
+ if notation == "d":
94
+ # day
95
+ return int(time) * 24 * 60 * 60 * 1000
96
+
97
+ if notation == "w":
98
+ # weeks
99
+ return int(time) * 5 * 24 * 60 * 60 * 1000
100
+
101
+ if notation == "M":
102
+ # month
103
+ return int(time) * 30 * 24 * 60 * 60 * 1000
104
+
105
+ return 0
106
+
107
+
108
+ def format_ts(time: datetime) -> str:
109
+ """
110
+ Central place to format datetime
111
+ to human-readable date
112
+ """
113
+ return time.strftime("%Y-%m-%d %H:%M:%S.%f")
114
+
115
+
116
+ def zero_remainder(x):
117
+ number = x
118
+
119
+ while True:
120
+ if number % x == 0:
121
+ return number
122
+ else:
123
+ number += x
shared/timestamps.py ADDED
@@ -0,0 +1,97 @@
1
+ import os
2
+ from time import time
3
+ import math
4
+ from zoneinfo import ZoneInfo
5
+ from shared.maths import round_numbers_ceiling
6
+ from datetime import datetime
7
+
8
+ format = "%Y-%m-%d %H:%M:%S"
9
+
10
+ def timestamp() -> int:
11
+ ts = time() * 1000
12
+ rounded_ts = round_timestamp(ts)
13
+ return rounded_ts
14
+
15
+
16
+ def round_timestamp(ts: int | float) -> int:
17
+ """
18
+ Round (or trim) millisecond timestamps to at most 13 digits.
19
+
20
+ For realistic millisecond timestamps (13 digits) this is a no-op.
21
+ For larger integers, extra lower-order digits are discarded so that
22
+ the result has exactly 13 digits.
23
+ """
24
+ ts_int = int(ts)
25
+ digits = int(math.log10(ts_int)) + 1 if ts_int > 0 else 1
26
+
27
+ if digits > 13:
28
+ # Drop extra lower-order digits to get back to 13 digits.
29
+ decimals = digits - 13
30
+ factor = 10**decimals
31
+ return ts_int // factor
32
+ else:
33
+ return ts_int
34
+
35
+
36
+ def ts_to_day(ts: float | int) -> str:
37
+ """
38
+ Convert timestamp to date (day) format YYYY-MM-DD
39
+ """
40
+ digits = int(math.log10(ts)) + 1
41
+ if digits >= 10:
42
+ ts = ts // pow(10, digits - 10)
43
+ else:
44
+ ts = ts * pow(10, 10 - digits)
45
+
46
+ dt_obj = datetime.fromtimestamp(ts)
47
+ b_str_date = datetime.strftime(dt_obj, format)
48
+ return b_str_date
49
+
50
+
51
+ def ms_to_sec(ms: int) -> int:
52
+ """
53
+ JavaScript needs 13 digits (milliseconds)
54
+ for new Date() to parse timestamps
55
+ correctly
56
+ """
57
+ return ms // 1000
58
+
59
+
60
+ def sec_to_ms(sec: int) -> int:
61
+ """
62
+ Python datetime needs 10 digits (seconds)
63
+ to parse dates correctly from timestamps
64
+ """
65
+ return sec * 1000
66
+
67
+
68
+ def ts_to_humandate(ts: int) -> str:
69
+ """Convert timestamp to human-readable date.
70
+
71
+ Accepts either seconds (10 digits) or milliseconds (13 digits) and
72
+ normalises to seconds for ``datetime.fromtimestamp``.
73
+ """
74
+ if len(str(abs(ts))) > 10:
75
+ # if timestamp is in milliseconds
76
+ ts = ts // 1000
77
+ return datetime.fromtimestamp(ts).strftime(format)
78
+
79
+
80
+ def timestamp_to_datetime(timestamp: str | int) -> str:
81
+ """
82
+ Convert a timestamp in milliseconds to seconds
83
+ to match expectation of datetime
84
+ Then convert to a human readable format.
85
+
86
+ Parameters
87
+ ----------
88
+ timestamp : str | int
89
+ The timestamp in milliseconds. Always in London timezone
90
+ to avoid inconsistencies across environments (Github, prod, local)
91
+ """
92
+ timestamp = int(round_numbers_ceiling(int(timestamp) / 1000, 0))
93
+ dt = datetime.fromtimestamp(
94
+ timestamp, tz=ZoneInfo(os.getenv("TZ", "Europe/London"))
95
+ )
96
+ return dt.strftime(format)
97
+
shared/types.py ADDED
@@ -0,0 +1,8 @@
1
+ from typing import Annotated
2
+ from pydantic import BeforeValidator
3
+ from shared.maths import ensure_float
4
+
5
+ Amount = Annotated[
6
+ float,
7
+ BeforeValidator(ensure_float),
8
+ ]