pybinbot 0.0.1__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/order.py ADDED
@@ -0,0 +1,130 @@
1
+ from typing import List, Optional
2
+ from uuid import uuid4, UUID
3
+ from pydantic import BaseModel, Field, field_validator
4
+ from databases.utils import Amount, timestamp
5
+ from tools.enum_definitions import (
6
+ QuoteAssets,
7
+ BinanceKlineIntervals,
8
+ CloseConditions,
9
+ Status,
10
+ Strategy,
11
+ DealType,
12
+ OrderStatus,
13
+ )
14
+ from tools.handle_error import IResponseBase
15
+ from tools.maths import ts_to_humandate
16
+ from databases.tables.bot_table import BotTable, PaperTradingTable
17
+ from databases.tables.deal_table import DealTable
18
+ from databases.tables.order_table import ExchangeOrderTable
19
+
20
+
21
+ class OrderModel(BaseModel):
22
+ order_type: str = Field(
23
+ description="Because every exchange has different naming, we should keep it as a str rather than OrderType enum"
24
+ )
25
+ time_in_force: str
26
+ timestamp: int = Field(default=0)
27
+ order_id: int | str = Field(
28
+ description="Because every exchange has id type, we should keep it as looose as possible. Int is for backwards compatibility"
29
+ )
30
+ order_side: str = Field(
31
+ description="Because every exchange has different naming, we should keep it as a str rather than OrderType enum"
32
+ )
33
+ pair: str
34
+ qty: float
35
+ status: OrderStatus
36
+ price: float
37
+ deal_type: DealType
38
+ model_config = {
39
+ "from_attributes": True,
40
+ "use_enum_values": True,
41
+ "json_schema_extra": {
42
+ "description": "Most fields are optional. Deal field is generated internally, orders are filled up by Exchange",
43
+ "examples": [
44
+ {
45
+ "order_type": "LIMIT",
46
+ "time_in_force": "GTC",
47
+ "timestamp": 0,
48
+ "order_id": 0,
49
+ "order_side": "BUY",
50
+ "pair": "",
51
+ "qty": 0,
52
+ "status": "",
53
+ "price": 0,
54
+ }
55
+ ],
56
+ },
57
+ }
58
+
59
+ @classmethod
60
+ def dump_from_table(cls, bot):
61
+ if isinstance(bot, BotTable) or isinstance(bot, PaperTradingTable):
62
+ model = BotModel.model_construct(**bot.model_dump())
63
+ deal_model = DealModel.model_construct(**bot.deal.model_dump())
64
+ order_models = [
65
+ OrderModel.model_construct(**order.model_dump()) for order in bot.orders
66
+ ]
67
+ model.deal = deal_model
68
+ model.orders = order_models
69
+ return model
70
+ else:
71
+ return bot
72
+
73
+
74
+ class DealModel(BaseModel):
75
+ base_order_size: Amount = Field(default=0, gt=-1)
76
+ current_price: Amount = Field(default=0)
77
+ take_profit_price: Amount = Field(default=0)
78
+ trailling_stop_loss_price: Amount = Field(
79
+ default=0,
80
+ description="take_profit but for trailling, to avoid confusion, trailling_profit_price always be > trailling_stop_loss_price",
81
+ )
82
+ trailling_profit_price: Amount = Field(default=0)
83
+ stop_loss_price: Amount = Field(default=0)
84
+ total_interests: float = Field(default=0, gt=-1)
85
+ total_commissions: float = Field(default=0, gt=-1)
86
+ margin_loan_id: int = Field(
87
+ default=0,
88
+ ge=0,
89
+ description="Txid from Binance. This is used to check if there is a loan, 0 means no loan",
90
+ )
91
+ margin_repay_id: int = Field(
92
+ default=0, ge=0, description="= 0, it has not been repaid"
93
+ )
94
+ opening_price: Amount = Field(
95
+ default=0,
96
+ description="replaces previous buy_price or short_sell_price/margin_short_sell_price",
97
+ )
98
+ opening_qty: Amount = Field(
99
+ default=0,
100
+ description="replaces previous buy_total_qty or short_sell_qty/margin_short_sell_qty",
101
+ )
102
+ opening_timestamp: int = Field(default=0)
103
+ closing_price: Amount = Field(
104
+ default=0,
105
+ description="replaces previous sell_price or short_sell_price/margin_short_sell_price",
106
+ )
107
+ closing_qty: Amount = Field(
108
+ default=0,
109
+ description="replaces previous sell_qty or short_sell_qty/margin_short_sell_qty",
110
+ )
111
+ closing_timestamp: int = Field(
112
+ default=0,
113
+ description="replaces previous buy_timestamp or margin/short_sell timestamps",
114
+ )
115
+
116
+ @field_validator("margin_loan_id", mode="before")
117
+ @classmethod
118
+ def validate_margin_loan_id(cls, value):
119
+ if isinstance(value, float):
120
+ return int(value)
121
+ else:
122
+ return value
123
+
124
+ @field_validator("margin_loan_id", mode="after")
125
+ @classmethod
126
+ def cast_float(cls, value):
127
+ if isinstance(value, float):
128
+ return int(value)
129
+ else:
130
+ return value
models/signals.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Shared Pydantic models for Binquant and Binbot.
3
+ This file is auto-generated and should be reviewed for deduplication and refactoring.
4
+ """
5
+
6
+ # Example imports (update as needed)
7
+ from pydantic import BaseModel, Field, EmailStr, field_validator, ConfigDict
8
+ from typing import Optional, List, Sequence, Union
9
+ from uuid import UUID, uuid4
10
+ from datetime import datetime
11
+
12
+ # ...existing code...
13
+
14
+
15
+ # Example shared model (copy actual model code from source files)
16
+ class HABollinguerSpread(BaseModel):
17
+ """
18
+ Pydantic model for the Bollinguer spread.
19
+ (optional)
20
+ """
21
+
22
+ bb_high: float
23
+ bb_mid: float
24
+ bb_low: float
25
+
26
+
27
+ class SignalsConsumer(BaseModel):
28
+ """
29
+ Pydantic model for the signals consumer.
30
+ """
31
+
32
+ type: str = Field(default="signal")
33
+ date: str = Field(
34
+ default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
35
+ )
36
+ spread: Optional[float] = Field(default=0)
37
+ current_price: Optional[float] = Field(default=0)
38
+ msg: str
39
+ symbol: str
40
+ algo: str
41
+ bot_strategy: str = Field(default="long")
42
+ bb_spreads: Optional[HABollinguerSpread]
43
+ autotrade: bool = Field(default=True, description="If it is in testing mode, False")
44
+
45
+ model_config = ConfigDict(
46
+ extra="allow",
47
+ use_enum_values=True,
48
+ )
49
+
50
+ @field_validator("spread", "current_price")
51
+ @classmethod
52
+ def name_must_contain_space(cls, v):
53
+ if v is None:
54
+ return 0
55
+ elif isinstance(v, str):
56
+ return float(v)
57
+ elif isinstance(v, float):
58
+ return v
59
+ else:
60
+ raise ValueError("must be a float or 0")
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybinbot
3
+ Version: 0.0.1
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: aiokafka>=0.12.0
10
+ Requires-Dist: apscheduler>=3.6.3
11
+ Requires-Dist: confluent-kafka>=2.11.1
12
+ Requires-Dist: kafka-python>=2.0.2
13
+ Requires-Dist: kucoin-universal-sdk>=1.3.0
14
+ Requires-Dist: numpy==2.2.0
15
+ Requires-Dist: pandas>=2.2.3
16
+ Requires-Dist: passlib
17
+ Requires-Dist: pydantic[email]>=2.0.0
18
+ Requires-Dist: pydantic-settings>=2.10.1
19
+ Requires-Dist: pymongo>=4.6.3
20
+ Requires-Dist: python-dotenv
21
+ Requires-Dist: python-jose
22
+ Requires-Dist: requests>=2.28.1
23
+ Requires-Dist: requests-cache>=1.2.0
24
+ Requires-Dist: requests-html>=0.10.0
25
+ Requires-Dist: scipy==1.14.1
26
+ Requires-Dist: websocket-client>=1.5.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # binbot-utils
33
+
34
+ Utility functions for the binbot project.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ uv sync
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Import and use the utilities in your Python code.
45
+
46
+ ## Publishing
47
+
48
+ To build and upload to PyPI:
49
+
50
+ ```bash
51
+ python -m build
52
+ python -m twine upload dist/*
53
+ ```
@@ -0,0 +1,13 @@
1
+ models/bot_base.py,sha256=z9hSK7uVLW4oDmPdtntYozo8RnQnpZtFi8fL1r9Qd5Q,3593
2
+ models/order.py,sha256=pQbL2rpgXvmkqiwpKwVFkuJnn3i1QR-omuc4E5eDWsk,4573
3
+ models/signals.py,sha256=DZc2r9x9XouRu9u14wI-zVEBX23-7wn_-MFButAQUUs,1621
4
+ pybinbot-0.0.1.dist-info/licenses/LICENSE,sha256=ECEAqAQ81zTT8PeN7gYqbkZtewkyeleEqQ26MxuHQxs,938
5
+ shared/enums.py,sha256=8rRvUIMq6ApMBBgd_Ef0_8aM6HeYnB37ro5Mh9W6_WY,6624
6
+ shared/logging_config.py,sha256=XZblKXH9KsLUDbIJqFRZPzI0h17-CRBZH4KktVak-TI,1144
7
+ shared/maths.py,sha256=Xcj4_rx9kHjowFhk8ysMNB_FaCcrGcHpK_-0P62xj70,2660
8
+ shared/timestamps.py,sha256=FtZw6hKNJ1N1NUBusW4g5L1PwUZtX98LGFSAxxZFsVw,1587
9
+ shared/types.py,sha256=KfuJzjsbMUHFcBaQ6ZXUbuSyFHbHqehgeY73Zt8lqO8,173
10
+ pybinbot-0.0.1.dist-info/METADATA,sha256=loC9ZtGqe3JanBSuBIn3WNChG4TLcDvcCYr1RrhTLjQ,1220
11
+ pybinbot-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ pybinbot-0.0.1.dist-info/top_level.txt,sha256=pm2plP5btgXnaBayjioY_GnmGMW0k3q0j7xXnUsunSY,14
13
+ pybinbot-0.0.1.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,2 @@
1
+ models
2
+ shared
shared/enums.py ADDED
@@ -0,0 +1,266 @@
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"
@@ -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,111 @@
1
+ import math
2
+ import re
3
+ from decimal import Decimal
4
+ from datetime import datetime
5
+
6
+
7
+ def ensure_float(value: str | int | float) -> float:
8
+ if isinstance(value, str) or isinstance(value, int):
9
+ return float(value)
10
+
11
+ return value
12
+
13
+
14
+ def supress_trailling(value: str | float | int) -> float:
15
+ """
16
+ Supress trilling 0s
17
+ this function will not round the number
18
+ e.g. 3.140, 3.140000004
19
+
20
+ also supress scientific notation
21
+ e.g. 2.05-5
22
+ """
23
+ value = float(value)
24
+ # supress scientific notation
25
+ number = float(f"{value:f}")
26
+ number = float("{0:g}".format(number))
27
+ return number
28
+
29
+
30
+ def round_numbers(value: float | int, decimals=6) -> float | int:
31
+ decimal_points = 10 ** int(decimals)
32
+ number = float(value)
33
+ result = math.floor(number * decimal_points) / decimal_points
34
+ if decimals == 0:
35
+ result = int(result)
36
+ return result
37
+
38
+
39
+ def round_numbers_ceiling(value, decimals=6):
40
+ decimal_points = 10 ** int(decimals)
41
+ number = float(value)
42
+ result = math.ceil(number * decimal_points) / decimal_points
43
+ if decimals == 0:
44
+ result = int(result)
45
+ return float(result)
46
+
47
+
48
+ def round_numbers_floor(value, decimals=6):
49
+ decimal_points = 10 ** int(decimals)
50
+ number = float(value)
51
+ result = math.floor(number * decimal_points) / decimal_points
52
+ if decimals == 0:
53
+ result = int(result)
54
+ return float(result)
55
+
56
+
57
+ def supress_notation(num: float, precision: int = 0) -> str:
58
+ """
59
+ Supress scientific notation
60
+ e.g. 8e-5 = "0.00008"
61
+ """
62
+ dec_num = float(num)
63
+ num = round_numbers(dec_num, precision)
64
+ if precision >= 0:
65
+ decimal_points = precision
66
+ else:
67
+ decimal_points = int(Decimal(str(num)).as_tuple().exponent * -1)
68
+ return f"{num:.{decimal_points}f}"
69
+
70
+
71
+ def interval_to_millisecs(interval: str) -> int:
72
+ time, notation = re.findall(r"[A-Za-z]+|\d+", interval)
73
+ if notation == "m":
74
+ # minutes
75
+ return int(time) * 60 * 1000
76
+
77
+ if notation == "h":
78
+ # hours
79
+ return int(time) * 60 * 60 * 1000
80
+
81
+ if notation == "d":
82
+ # day
83
+ return int(time) * 24 * 60 * 60 * 1000
84
+
85
+ if notation == "w":
86
+ # weeks
87
+ return int(time) * 5 * 24 * 60 * 60 * 1000
88
+
89
+ if notation == "M":
90
+ # month
91
+ return int(time) * 30 * 24 * 60 * 60 * 1000
92
+
93
+ return 0
94
+
95
+
96
+ def format_ts(time: datetime) -> str:
97
+ """
98
+ Central place to format datetime
99
+ to human-readable date
100
+ """
101
+ return time.strftime("%Y-%m-%d %H:%M:%S.%f")
102
+
103
+
104
+ def zero_remainder(x):
105
+ number = x
106
+
107
+ while True:
108
+ if number % x == 0:
109
+ return number
110
+ else:
111
+ number += x
shared/timestamps.py ADDED
@@ -0,0 +1,66 @@
1
+ from time import time
2
+ import math
3
+ from shared.maths import round_numbers
4
+ from datetime import datetime
5
+
6
+
7
+ def timestamp() -> int:
8
+ ts = time() * 1000
9
+ rounded_ts = round_timestamp(ts)
10
+ return rounded_ts
11
+
12
+
13
+ def round_timestamp(ts: int | float) -> int:
14
+ """
15
+ Round millisecond timestamps to always 13 digits
16
+ this is the universal format that JS and Python accept
17
+ """
18
+ digits = int(math.log10(ts)) + 1
19
+ if digits > 13:
20
+ decimals = digits - 13
21
+ multiplier = 10**decimals
22
+ return int(round_numbers(ts * multiplier, decimals))
23
+ else:
24
+ return int(ts)
25
+
26
+
27
+ def ts_to_day(ts: float | int) -> str:
28
+ """
29
+ Convert timestamp to date (day) format YYYY-MM-DD
30
+ """
31
+ digits = int(math.log10(ts)) + 1
32
+ if digits >= 10:
33
+ ts = ts // pow(10, digits - 10)
34
+ else:
35
+ ts = ts * pow(10, 10 - digits)
36
+
37
+ dt_obj = datetime.fromtimestamp(ts)
38
+ b_str_date = datetime.strftime(dt_obj, "%Y-%m-%d")
39
+ return b_str_date
40
+
41
+
42
+ def ms_to_sec(ms: int) -> int:
43
+ """
44
+ JavaScript needs 13 digits (milliseconds)
45
+ for new Date() to parse timestamps
46
+ correctly
47
+ """
48
+ return ms // 1000
49
+
50
+
51
+ def sec_to_ms(sec: int) -> int:
52
+ """
53
+ Python datetime needs 10 digits (seconds)
54
+ to parse dates correctly from timestamps
55
+ """
56
+ return sec * 1000
57
+
58
+
59
+ def ts_to_humandate(ts: int) -> str:
60
+ """
61
+ Convert timestamp to human-readable date
62
+ """
63
+ if len(str(abs(1747852851106))) > 10:
64
+ # if timestamp is in milliseconds
65
+ ts = ts // 1000
66
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
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
+ ]