pybinbot 0.0.8__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-0.0.8/LICENSE ADDED
@@ -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
+ include README.md
2
+ include LICENSE
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybinbot
3
+ Version: 0.0.8
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,22 @@
1
+ # binbot-utils
2
+
3
+ Utility functions for the binbot project.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv sync
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Import and use the utilities in your Python code.
14
+
15
+ ## Publishing
16
+
17
+ To build and upload to PyPI:
18
+
19
+ ```bash
20
+ python -m build
21
+ python -m twine upload dist/*
22
+ ```
@@ -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]
@@ -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
@@ -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.8
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,20 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pybinbot.py
5
+ pyproject.toml
6
+ models/bot_base.py
7
+ models/order.py
8
+ models/signals.py
9
+ pybinbot.egg-info/PKG-INFO
10
+ pybinbot.egg-info/SOURCES.txt
11
+ pybinbot.egg-info/dependency_links.txt
12
+ pybinbot.egg-info/requires.txt
13
+ pybinbot.egg-info/top_level.txt
14
+ shared/enums.py
15
+ shared/logging_config.py
16
+ shared/maths.py
17
+ shared/timestamps.py
18
+ shared/types.py
19
+ tests/test_maths.py
20
+ tests/test_timestamps.py
@@ -0,0 +1,22 @@
1
+ aiokafka>=0.12.0
2
+ apscheduler>=3.6.3
3
+ confluent-kafka>=2.11.1
4
+ kafka-python>=2.0.2
5
+ kucoin-universal-sdk>=1.3.0
6
+ numpy==2.2.0
7
+ pandas>=2.2.3
8
+ passlib
9
+ pydantic[email]>=2.0.0
10
+ pydantic-settings>=2.10.1
11
+ pymongo>=4.6.3
12
+ python-dotenv
13
+ python-jose
14
+ requests>=2.28.1
15
+ requests-cache>=1.2.0
16
+ requests-html>=0.10.0
17
+ scipy==1.14.1
18
+ websocket-client>=1.5.0
19
+
20
+ [dev]
21
+ pytest>=9.0.2
22
+ ruff
@@ -0,0 +1,3 @@
1
+ models
2
+ pybinbot
3
+ shared
@@ -0,0 +1,94 @@
1
+ """Public API module for the ``pybinbot`` distribution.
2
+
3
+ This module re-exports the internal ``shared`` and ``models`` packages and
4
+ the most commonly used helpers and enums so consumers can simply::
5
+
6
+ from pybinbot import round_numbers, ExchangeId
7
+
8
+ The implementation deliberately avoids importing heavy third-party
9
+ libraries at module import time.
10
+ """
11
+
12
+ import shared # type: ignore[import]
13
+ import models # type: ignore[import]
14
+
15
+ from shared import maths, timestamps, enums # type: ignore[import]
16
+ from shared.maths import ( # type: ignore[import]
17
+ supress_trailling,
18
+ round_numbers,
19
+ round_numbers_ceiling,
20
+ round_numbers_floor,
21
+ supress_notation,
22
+ interval_to_millisecs,
23
+ format_ts,
24
+ zero_remainder,
25
+ )
26
+ from shared.timestamps import ( # type: ignore[import]
27
+ timestamp,
28
+ round_timestamp,
29
+ ts_to_day,
30
+ ms_to_sec,
31
+ sec_to_ms,
32
+ ts_to_humandate,
33
+ )
34
+ from shared.enums import ( # type: ignore[import]
35
+ CloseConditions,
36
+ KafkaTopics,
37
+ DealType,
38
+ BinanceOrderModel,
39
+ Status,
40
+ Strategy,
41
+ OrderType,
42
+ TimeInForce,
43
+ OrderSide,
44
+ OrderStatus,
45
+ TrendEnum,
46
+ BinanceKlineIntervals,
47
+ KucoinKlineIntervals,
48
+ AutotradeSettingsDocument,
49
+ UserRoles,
50
+ QuoteAssets,
51
+ ExchangeId,
52
+ )
53
+
54
+ __all__ = [
55
+ "shared",
56
+ "models",
57
+ "maths",
58
+ "timestamps",
59
+ "enums",
60
+ # maths helpers
61
+ "supress_trailling",
62
+ "round_numbers",
63
+ "round_numbers_ceiling",
64
+ "round_numbers_floor",
65
+ "supress_notation",
66
+ "interval_to_millisecs",
67
+ "format_ts",
68
+ "zero_remainder",
69
+ # timestamp helpers
70
+ "timestamp",
71
+ "round_timestamp",
72
+ "ts_to_day",
73
+ "ms_to_sec",
74
+ "sec_to_ms",
75
+ "ts_to_humandate",
76
+ # enums and models
77
+ "CloseConditions",
78
+ "KafkaTopics",
79
+ "DealType",
80
+ "BinanceOrderModel",
81
+ "Status",
82
+ "Strategy",
83
+ "OrderType",
84
+ "TimeInForce",
85
+ "OrderSide",
86
+ "OrderStatus",
87
+ "TrendEnum",
88
+ "BinanceKlineIntervals",
89
+ "KucoinKlineIntervals",
90
+ "AutotradeSettingsDocument",
91
+ "UserRoles",
92
+ "QuoteAssets",
93
+ "ExchangeId",
94
+ ]
@@ -0,0 +1,47 @@
1
+ [tool.setuptools]
2
+ packages = ["shared", "models"]
3
+ py-modules = ["pybinbot"]
4
+
5
+ [tool.setuptools.package-data]
6
+ pybinbot = ["py.typed"]
7
+
8
+ [build-system]
9
+ requires = ["setuptools>=61.0"]
10
+ build-backend = "setuptools.build_meta"
11
+
12
+
13
+ [project]
14
+ name = "pybinbot"
15
+ version = "0.0.8"
16
+ description = "Utility functions for the binbot project."
17
+ authors = [
18
+ { name="Carlos Wu", email="carkodw@gmail.com" }
19
+ ]
20
+ readme = "README.md"
21
+ requires-python = ">=3.11"
22
+ dependencies = [
23
+ "aiokafka>=0.12.0",
24
+ "apscheduler>=3.6.3",
25
+ "confluent-kafka>=2.11.1",
26
+ "kafka-python>=2.0.2",
27
+ "kucoin-universal-sdk>=1.3.0",
28
+ "numpy==2.2.0",
29
+ "pandas>=2.2.3",
30
+ "passlib",
31
+ "pydantic[email]>=2.0.0",
32
+ "pydantic-settings>=2.10.1",
33
+ "pymongo>=4.6.3",
34
+ "python-dotenv",
35
+ "python-jose",
36
+ "requests>=2.28.1",
37
+ "requests-cache>=1.2.0",
38
+ "requests-html>=0.10.0",
39
+ "scipy==1.14.1",
40
+ "websocket-client>=1.5.0",
41
+ ]
42
+
43
+ [project.optional-dependencies]
44
+ dev = [
45
+ "pytest>=9.0.2",
46
+ "ruff"
47
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )
@@ -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
@@ -0,0 +1,74 @@
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 (or trim) millisecond timestamps to at most 13 digits.
16
+
17
+ For realistic millisecond timestamps (13 digits) this is a no-op.
18
+ For larger integers, extra lower-order digits are discarded so that
19
+ the result has exactly 13 digits.
20
+ """
21
+ ts_int = int(ts)
22
+ digits = int(math.log10(ts_int)) + 1 if ts_int > 0 else 1
23
+
24
+ if digits > 13:
25
+ # Drop extra lower-order digits to get back to 13 digits.
26
+ decimals = digits - 13
27
+ factor = 10**decimals
28
+ return ts_int // factor
29
+ else:
30
+ return ts_int
31
+
32
+
33
+ def ts_to_day(ts: float | int) -> str:
34
+ """
35
+ Convert timestamp to date (day) format YYYY-MM-DD
36
+ """
37
+ digits = int(math.log10(ts)) + 1
38
+ if digits >= 10:
39
+ ts = ts // pow(10, digits - 10)
40
+ else:
41
+ ts = ts * pow(10, 10 - digits)
42
+
43
+ dt_obj = datetime.fromtimestamp(ts)
44
+ b_str_date = datetime.strftime(dt_obj, "%Y-%m-%d")
45
+ return b_str_date
46
+
47
+
48
+ def ms_to_sec(ms: int) -> int:
49
+ """
50
+ JavaScript needs 13 digits (milliseconds)
51
+ for new Date() to parse timestamps
52
+ correctly
53
+ """
54
+ return ms // 1000
55
+
56
+
57
+ def sec_to_ms(sec: int) -> int:
58
+ """
59
+ Python datetime needs 10 digits (seconds)
60
+ to parse dates correctly from timestamps
61
+ """
62
+ return sec * 1000
63
+
64
+
65
+ def ts_to_humandate(ts: int) -> str:
66
+ """Convert timestamp to human-readable date.
67
+
68
+ Accepts either seconds (10 digits) or milliseconds (13 digits) and
69
+ normalises to seconds for ``datetime.fromtimestamp``.
70
+ """
71
+ if len(str(abs(ts))) > 10:
72
+ # if timestamp is in milliseconds
73
+ ts = ts // 1000
74
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
@@ -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
+ ]
@@ -0,0 +1,55 @@
1
+ from shared import maths
2
+
3
+
4
+ def test_ensure_float():
5
+ assert maths.ensure_float("3.14") == 3.14
6
+ assert maths.ensure_float(2) == 2.0
7
+ assert maths.ensure_float(2.5) == 2.5
8
+
9
+
10
+ def test_supress_trailling():
11
+ assert maths.supress_trailling("3.14000") == 3.14
12
+ assert maths.supress_trailling(2.05e-5) == 0.0000205
13
+ assert maths.supress_trailling(3.140000004) == 3.140000004
14
+
15
+
16
+ def test_round_numbers():
17
+ assert maths.round_numbers(3.14159, 2) == 3.14
18
+ assert maths.round_numbers(3.999, 0) == 3
19
+ assert maths.round_numbers(2, 3) == 2.0
20
+
21
+
22
+ def test_round_numbers_ceiling():
23
+ assert maths.round_numbers_ceiling(3.14159, 2) == 3.15
24
+ assert maths.round_numbers_ceiling(3.0001, 0) == 4.0
25
+
26
+
27
+ def test_round_numbers_floor():
28
+ assert maths.round_numbers_floor(3.14159, 2) == 3.14
29
+ assert maths.round_numbers_floor(3.999, 0) == 3.0
30
+
31
+
32
+ def test_supress_notation():
33
+ assert maths.supress_notation(8e-5, 5) == "0.00008"
34
+ assert maths.supress_notation(123.456, 2) == "123.46"
35
+
36
+
37
+ def test_interval_to_millisecs():
38
+ assert maths.interval_to_millisecs("5m") == 300000
39
+ assert maths.interval_to_millisecs("2h") == 7200000
40
+ assert maths.interval_to_millisecs("1d") == 86400000
41
+ assert maths.interval_to_millisecs("1w") == 432000000
42
+ assert maths.interval_to_millisecs("1M") == 2592000000
43
+ assert maths.interval_to_millisecs("10x") == 0
44
+
45
+
46
+ def test_format_ts():
47
+ from datetime import datetime
48
+
49
+ dt = datetime(2024, 1, 2, 3, 4, 5, 6789)
50
+ assert maths.format_ts(dt).startswith("2024-01-02 03:04:05.")
51
+
52
+
53
+ def test_zero_remainder():
54
+ assert maths.zero_remainder(5) == 5
55
+ assert maths.zero_remainder(7) == 7
@@ -0,0 +1,41 @@
1
+ from shared import timestamps
2
+ from datetime import datetime
3
+
4
+
5
+ def test_timestamp():
6
+ ts = timestamps.timestamp()
7
+ assert isinstance(ts, int)
8
+ assert len(str(ts)) == 13
9
+
10
+
11
+ def test_round_timestamp():
12
+ assert timestamps.round_timestamp(1747852851106) == 1747852851106
13
+ # Extra lower-order digits are discarded to keep at most 13 digits
14
+ assert timestamps.round_timestamp(1747852851106123) == 1747852851106
15
+ assert timestamps.round_timestamp(1747852851) == 1747852851
16
+
17
+
18
+ def test_ts_to_day():
19
+ ts = 1700000000 # seconds
20
+ assert timestamps.ts_to_day(ts) == datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
21
+ ms = 1700000000000 # ms
22
+ assert timestamps.ts_to_day(ms) == datetime.fromtimestamp(ms // 1000).strftime(
23
+ "%Y-%m-%d"
24
+ )
25
+
26
+
27
+ def test_ms_to_sec():
28
+ assert timestamps.ms_to_sec(1000) == 1
29
+ assert timestamps.ms_to_sec(1234567) == 1234
30
+
31
+
32
+ def test_sec_to_ms():
33
+ assert timestamps.sec_to_ms(1) == 1000
34
+ assert timestamps.sec_to_ms(1234) == 1234000
35
+
36
+
37
+ def test_ts_to_humandate():
38
+ ms = 1747852851106
39
+ sec = 1747852851
40
+ assert timestamps.ts_to_humandate(ms).startswith("202")
41
+ assert timestamps.ts_to_humandate(sec).startswith("202")