bt-api-base 0.15.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.
- bt_api_base/__init__.py +7 -0
- bt_api_base/_compat.py +29 -0
- bt_api_base/_plugin_shims.py +28 -0
- bt_api_base/_version.py +5 -0
- bt_api_base/auth_config.py +171 -0
- bt_api_base/balance_utils.py +99 -0
- bt_api_base/cache.py +212 -0
- bt_api_base/config_loader.py +288 -0
- bt_api_base/connection_pool.py +247 -0
- bt_api_base/containers/__init__.py +45 -0
- bt_api_base/containers/accounts/__init__.py +5 -0
- bt_api_base/containers/accounts/account.py +182 -0
- bt_api_base/containers/auto_init_mixin.py +63 -0
- bt_api_base/containers/balances/__init__.py +5 -0
- bt_api_base/containers/balances/balance.py +260 -0
- bt_api_base/containers/bars/__init__.py +5 -0
- bt_api_base/containers/bars/bar.py +90 -0
- bt_api_base/containers/exchanges/__init__.py +5 -0
- bt_api_base/containers/exchanges/exchange_data.py +89 -0
- bt_api_base/containers/fundingrates/__init__.py +5 -0
- bt_api_base/containers/fundingrates/funding_rate.py +122 -0
- bt_api_base/containers/greeks/__init__.py +5 -0
- bt_api_base/containers/greeks/greeks.py +63 -0
- bt_api_base/containers/incomes/__init__.py +5 -0
- bt_api_base/containers/incomes/income.py +77 -0
- bt_api_base/containers/instrument.py +177 -0
- bt_api_base/containers/liquidations/__init__.py +5 -0
- bt_api_base/containers/liquidations/liquidation.py +61 -0
- bt_api_base/containers/markprices/__init__.py +5 -0
- bt_api_base/containers/markprices/mark_price.py +70 -0
- bt_api_base/containers/orderbooks/__init__.py +5 -0
- bt_api_base/containers/orderbooks/orderbook.py +94 -0
- bt_api_base/containers/orders/__init__.py +5 -0
- bt_api_base/containers/orders/order.py +290 -0
- bt_api_base/containers/positions/__init__.py +5 -0
- bt_api_base/containers/positions/position.py +196 -0
- bt_api_base/containers/requestdatas/__init__.py +5 -0
- bt_api_base/containers/requestdatas/request_data.py +173 -0
- bt_api_base/containers/symbols/__init__.py +5 -0
- bt_api_base/containers/symbols/symbol.py +125 -0
- bt_api_base/containers/tickers/__init__.py +5 -0
- bt_api_base/containers/tickers/ticker.py +169 -0
- bt_api_base/containers/timers/__init__.py +5 -0
- bt_api_base/containers/timers/timer.py +15 -0
- bt_api_base/containers/trades/__init__.py +5 -0
- bt_api_base/containers/trades/trade.py +151 -0
- bt_api_base/core/__init__.py +46 -0
- bt_api_base/core/async_context.py +475 -0
- bt_api_base/core/dependency_injection.py +261 -0
- bt_api_base/core/interfaces.py +325 -0
- bt_api_base/core/services.py +609 -0
- bt_api_base/error.py +432 -0
- bt_api_base/event_bus.py +136 -0
- bt_api_base/exceptions.py +359 -0
- bt_api_base/feeds/__init__.py +18 -0
- bt_api_base/feeds/abstract_feed.py +305 -0
- bt_api_base/feeds/base_stream.py +116 -0
- bt_api_base/feeds/capability.py +110 -0
- bt_api_base/feeds/connection_mixin.py +71 -0
- bt_api_base/feeds/feed.py +633 -0
- bt_api_base/feeds/http_client.py +304 -0
- bt_api_base/feeds/my_websocket_app.py +324 -0
- bt_api_base/functions/__init__.py +7 -0
- bt_api_base/functions/async_base.py +132 -0
- bt_api_base/functions/calculate_time.py +131 -0
- bt_api_base/functions/log_message.py +124 -0
- bt_api_base/functions/utils.py +301 -0
- bt_api_base/gateway/__init__.py +22 -0
- bt_api_base/gateway/adapters/__init__.py +42 -0
- bt_api_base/gateway/adapters/base.py +56 -0
- bt_api_base/gateway/adapters/plugin_adapter.py +81 -0
- bt_api_base/gateway/models.py +53 -0
- bt_api_base/gateway/protocol.py +39 -0
- bt_api_base/gateway/registrar.py +37 -0
- bt_api_base/instrument_manager.py +122 -0
- bt_api_base/logging_factory.py +109 -0
- bt_api_base/plugins/__init__.py +19 -0
- bt_api_base/plugins/errors.py +17 -0
- bt_api_base/plugins/loader.py +229 -0
- bt_api_base/plugins/protocol.py +16 -0
- bt_api_base/rate_limiter.py +304 -0
- bt_api_base/registry.py +274 -0
- bt_api_base/security.py +209 -0
- bt_api_base/utils/__init__.py +3 -0
- bt_api_base/utils/time.py +29 -0
- bt_api_base/websocket/__init__.py +41 -0
- bt_api_base/websocket/exchange_adapters.py +488 -0
- bt_api_base/websocket_manager.py +594 -0
- bt_api_base-0.15.1.dist-info/METADATA +536 -0
- bt_api_base-0.15.1.dist-info/RECORD +93 -0
- bt_api_base-0.15.1.dist-info/WHEEL +5 -0
- bt_api_base-0.15.1.dist-info/licenses/LICENSE +21 -0
- bt_api_base-0.15.1.dist-info/top_level.txt +1 -0
bt_api_base/__init__.py
ADDED
bt_api_base/_compat.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timezone
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from typing import Never as _Never
|
|
8
|
+
from typing import ParamSpec as _ParamSpec
|
|
9
|
+
from typing import Self as _Self
|
|
10
|
+
except ImportError:
|
|
11
|
+
from typing_extensions import Never as _Never
|
|
12
|
+
from typing_extensions import ParamSpec as _ParamSpec
|
|
13
|
+
from typing_extensions import Self as _Self
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from enum import StrEnum as _StrEnum
|
|
17
|
+
except ImportError:
|
|
18
|
+
|
|
19
|
+
class _StrEnum(str, Enum):
|
|
20
|
+
"""Backport for Python < 3.11."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Never = _Never
|
|
24
|
+
ParamSpec = _ParamSpec
|
|
25
|
+
Self = _Self
|
|
26
|
+
StrEnum = _StrEnum
|
|
27
|
+
UTC = timezone.utc
|
|
28
|
+
|
|
29
|
+
__all__ = ["Never", "ParamSpec", "Self", "StrEnum", "UTC"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def reexport_plugin_module(
|
|
9
|
+
legacy_module: str,
|
|
10
|
+
plugin_module: str,
|
|
11
|
+
namespace: dict[str, Any],
|
|
12
|
+
) -> tuple[list[str], Any]:
|
|
13
|
+
"""Load a plugin module and expose its public symbols via a legacy path."""
|
|
14
|
+
|
|
15
|
+
warnings.warn(
|
|
16
|
+
(
|
|
17
|
+
f"{legacy_module} is deprecated. "
|
|
18
|
+
f"Install the corresponding plugin package and import from {plugin_module} instead."
|
|
19
|
+
),
|
|
20
|
+
DeprecationWarning,
|
|
21
|
+
stacklevel=2,
|
|
22
|
+
)
|
|
23
|
+
module = import_module(plugin_module)
|
|
24
|
+
exports = list(
|
|
25
|
+
getattr(module, "__all__", [name for name in dir(module) if not name.startswith("_")])
|
|
26
|
+
)
|
|
27
|
+
namespace.update({name: getattr(module, name) for name in exports if hasattr(module, name)})
|
|
28
|
+
return exports, module
|
bt_api_base/_version.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""认证配置 — 统一管理不同交易所的认证方式。
|
|
2
|
+
|
|
3
|
+
加密货币交易所使用 API Key,CTP 使用 Broker/User/Password,IB 使用 TWS 连接参数.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AuthConfig",
|
|
13
|
+
"CryptoAuthConfig",
|
|
14
|
+
"CtpAuthConfig",
|
|
15
|
+
"IbAuthConfig",
|
|
16
|
+
"IbWebAuthConfig",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require_non_empty_str(value: str, field_name: str) -> str:
|
|
21
|
+
if not isinstance(value, str) or not value.strip():
|
|
22
|
+
raise ValueError(f"{field_name} must be a non-empty string")
|
|
23
|
+
return value.strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_port(port: int, field_name: str = "port") -> int:
|
|
27
|
+
if not isinstance(port, int) or not (1 <= port <= 65535):
|
|
28
|
+
raise ValueError(f"{field_name} must be in range 1-65535")
|
|
29
|
+
return port
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_url(value: str, field_name: str) -> str:
|
|
33
|
+
parsed = urlparse(value)
|
|
34
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
35
|
+
raise ValueError(f"{field_name} must be a valid http/https URL")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _validate_tcp_front(value: str, field_name: str) -> str:
|
|
40
|
+
parsed = urlparse(value)
|
|
41
|
+
if parsed.scheme != "tcp" or not parsed.hostname or parsed.port is None:
|
|
42
|
+
raise ValueError(f"{field_name} must be a valid tcp://host:port address")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AuthConfig:
|
|
47
|
+
"""认证配置基类."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, exchange: str, asset_type: str = "SWAP", **kwargs: Any) -> None:
|
|
50
|
+
self.exchange = _require_non_empty_str(exchange, "exchange")
|
|
51
|
+
self.asset_type = _require_non_empty_str(asset_type, "asset_type")
|
|
52
|
+
|
|
53
|
+
def get_exchange_name(self) -> str:
|
|
54
|
+
return f"{self.exchange}___{self.asset_type}"
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CryptoAuthConfig(AuthConfig):
|
|
61
|
+
"""加密货币交易所认证配置(Binance, OKX 等)."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
exchange: str,
|
|
66
|
+
asset_type: str = "SWAP",
|
|
67
|
+
public_key: str | None = None,
|
|
68
|
+
private_key: str | None = None,
|
|
69
|
+
passphrase: str | None = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__(exchange, asset_type, **kwargs)
|
|
73
|
+
if public_key is not None:
|
|
74
|
+
public_key = _require_non_empty_str(public_key, "public_key")
|
|
75
|
+
if private_key is not None:
|
|
76
|
+
private_key = _require_non_empty_str(private_key, "private_key")
|
|
77
|
+
if public_key is None and private_key is not None:
|
|
78
|
+
raise ValueError("public_key is required when private_key is provided")
|
|
79
|
+
if private_key is None and public_key is not None:
|
|
80
|
+
raise ValueError("private_key is required when public_key is provided")
|
|
81
|
+
self.public_key = public_key
|
|
82
|
+
self.private_key = private_key
|
|
83
|
+
self.passphrase = (
|
|
84
|
+
_require_non_empty_str(passphrase, "passphrase") if passphrase is not None else None
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CtpAuthConfig(AuthConfig):
|
|
89
|
+
"""CTP 认证配置."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
exchange: str = "CTP",
|
|
94
|
+
asset_type: str = "FUTURE",
|
|
95
|
+
broker_id: str = "",
|
|
96
|
+
user_id: str = "",
|
|
97
|
+
password: str = "",
|
|
98
|
+
auth_code: str = "",
|
|
99
|
+
app_id: str = "",
|
|
100
|
+
md_front: str = "",
|
|
101
|
+
td_front: str = "",
|
|
102
|
+
product_info: str = "",
|
|
103
|
+
**kwargs: Any,
|
|
104
|
+
) -> None:
|
|
105
|
+
super().__init__(exchange, asset_type, **kwargs)
|
|
106
|
+
self.broker_id = _require_non_empty_str(broker_id, "broker_id")
|
|
107
|
+
self.user_id = _require_non_empty_str(user_id, "user_id")
|
|
108
|
+
self.password = _require_non_empty_str(password, "password")
|
|
109
|
+
self.auth_code = auth_code
|
|
110
|
+
self.app_id = app_id
|
|
111
|
+
self.md_front = _validate_tcp_front(md_front, "md_front")
|
|
112
|
+
self.td_front = _validate_tcp_front(td_front, "td_front")
|
|
113
|
+
self.product_info = product_info
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class IbAuthConfig(AuthConfig):
|
|
117
|
+
"""Interactive Brokers 认证配置."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
exchange: str = "IB",
|
|
122
|
+
asset_type: str = "STK",
|
|
123
|
+
host: str = "127.0.0.1",
|
|
124
|
+
port: int = 7497,
|
|
125
|
+
client_id: int = 1,
|
|
126
|
+
**kwargs: Any,
|
|
127
|
+
) -> None:
|
|
128
|
+
super().__init__(exchange, asset_type, **kwargs)
|
|
129
|
+
self.host = _require_non_empty_str(host, "host")
|
|
130
|
+
self.port = _validate_port(port, "port")
|
|
131
|
+
if not isinstance(client_id, int) or client_id < 0:
|
|
132
|
+
raise ValueError("client_id must be a non-negative integer")
|
|
133
|
+
self.client_id = client_id
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class IbWebAuthConfig(AuthConfig):
|
|
137
|
+
"""Interactive Brokers Web API 认证配置."""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
exchange: str = "IB_WEB",
|
|
142
|
+
asset_type: str = "STK",
|
|
143
|
+
base_url: str = "https://localhost:5000",
|
|
144
|
+
account_id: str | None = None,
|
|
145
|
+
access_token: str | None = None,
|
|
146
|
+
client_id: str | None = None,
|
|
147
|
+
private_key_path: str | None = None,
|
|
148
|
+
verify_ssl: bool = False,
|
|
149
|
+
proxies: dict[str, str] | None = None,
|
|
150
|
+
timeout: int = 10,
|
|
151
|
+
cookies: dict[str, str] | None = None,
|
|
152
|
+
cookie_source: str | None = None,
|
|
153
|
+
cookie_browser: str = "chrome",
|
|
154
|
+
cookie_path: str = "/sso",
|
|
155
|
+
**kwargs: Any,
|
|
156
|
+
) -> None:
|
|
157
|
+
super().__init__(exchange, asset_type, **kwargs)
|
|
158
|
+
self.base_url = _validate_url(base_url, "base_url")
|
|
159
|
+
self.account_id = account_id
|
|
160
|
+
self.access_token = access_token
|
|
161
|
+
self.client_id = client_id
|
|
162
|
+
self.private_key_path = private_key_path
|
|
163
|
+
self.verify_ssl = verify_ssl
|
|
164
|
+
self.proxies = proxies
|
|
165
|
+
if not isinstance(timeout, int) or timeout <= 0:
|
|
166
|
+
raise ValueError("timeout must be a positive integer")
|
|
167
|
+
self.timeout = timeout
|
|
168
|
+
self.cookies = cookies
|
|
169
|
+
self.cookie_source = cookie_source
|
|
170
|
+
self.cookie_browser = cookie_browser
|
|
171
|
+
self.cookie_path = cookie_path
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
通用余额解析工具函数
|
|
3
|
+
将各交易所重复的 balance_handler 逻辑抽取到此处
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from bt_api_base.containers.accounts.account import AccountData
|
|
12
|
+
from bt_api_base.containers.balances.balance import BalanceData
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _to_float(value: float | int | None) -> float:
|
|
16
|
+
"""Coerce numeric balance fields while tolerating missing values."""
|
|
17
|
+
return float(value) if value is not None else 0.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _currency_key(value: str | None) -> str:
|
|
21
|
+
"""Normalize currency keys used in aggregated balance maps."""
|
|
22
|
+
return value or ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _apply_balance_values(
|
|
26
|
+
value_result: dict[str, dict[str, float]],
|
|
27
|
+
cash_result: dict[str, dict[str, float]],
|
|
28
|
+
currency: str,
|
|
29
|
+
margin: float | int | None,
|
|
30
|
+
available_margin: float | int | None,
|
|
31
|
+
unrealized_profit: float | int | None,
|
|
32
|
+
) -> None:
|
|
33
|
+
cash_result[currency] = {"cash": _to_float(available_margin)}
|
|
34
|
+
value_result[currency] = {"value": _to_float(margin) + _to_float(unrealized_profit)}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def simple_balance_handler(
|
|
38
|
+
account_list: list[AccountData],
|
|
39
|
+
) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]]]:
|
|
40
|
+
"""通用余额解析处理函数(适用于 Binance/CTP/IB 等单层账户结构)。
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
account_list: 账户数据列表,每个元素实现 get_account_type、get_available_margin 等接口。
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
(value_result, cash_result): value_result 为 {currency: {"value": float}},
|
|
47
|
+
cash_result 为 {currency: {"cash": float}}。
|
|
48
|
+
"""
|
|
49
|
+
value_result: dict[str, dict[str, float]] = {}
|
|
50
|
+
cash_result: dict[str, dict[str, float]] = {}
|
|
51
|
+
for account in account_list:
|
|
52
|
+
account.init_data()
|
|
53
|
+
currency = _currency_key(account.get_account_type())
|
|
54
|
+
_apply_balance_values(
|
|
55
|
+
value_result,
|
|
56
|
+
cash_result,
|
|
57
|
+
currency,
|
|
58
|
+
account.get_margin(),
|
|
59
|
+
account.get_available_margin(),
|
|
60
|
+
account.get_unrealized_profit(),
|
|
61
|
+
)
|
|
62
|
+
return value_result, cash_result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def nested_balance_handler(
|
|
66
|
+
account_list: list[AccountData],
|
|
67
|
+
) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]]]:
|
|
68
|
+
"""嵌套余额解析处理函数(适用于 OKX 等多层账户结构)。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
account_list: 账户数据列表,每个 account 内含 get_balances() 返回 balance 列表。
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
(value_result, cash_result): 同 simple_balance_handler。
|
|
75
|
+
"""
|
|
76
|
+
value_result: dict[str, dict[str, float]] = {}
|
|
77
|
+
cash_result: dict[str, dict[str, float]] = {}
|
|
78
|
+
for account in account_list:
|
|
79
|
+
account.init_data()
|
|
80
|
+
for balance in account.get_balances():
|
|
81
|
+
balance.init_data()
|
|
82
|
+
_update_nested_balance(value_result, cash_result, balance)
|
|
83
|
+
return value_result, cash_result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _update_nested_balance(
|
|
87
|
+
value_result: dict[str, dict[str, float]],
|
|
88
|
+
cash_result: dict[str, dict[str, float]],
|
|
89
|
+
balance: BalanceData,
|
|
90
|
+
) -> None:
|
|
91
|
+
currency = _currency_key(balance.get_symbol_name())
|
|
92
|
+
_apply_balance_values(
|
|
93
|
+
value_result,
|
|
94
|
+
cash_result,
|
|
95
|
+
currency,
|
|
96
|
+
balance.get_margin(),
|
|
97
|
+
balance.get_available_margin(),
|
|
98
|
+
balance.get_unrealized_profit(),
|
|
99
|
+
)
|
bt_api_base/cache.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Caching utilities for performance optimization.
|
|
3
|
+
|
|
4
|
+
Provides in-memory caching for frequently accessed data like exchange info,
|
|
5
|
+
trading pairs, and market data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
from collections.abc import Callable, Iterator
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import Any, TypeVar
|
|
16
|
+
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
|
+
_CACHE_MISSING = object()
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SimpleCache",
|
|
22
|
+
"ExchangeInfoCache",
|
|
23
|
+
"MarketDataCache",
|
|
24
|
+
"cached",
|
|
25
|
+
"get_exchange_info_cache",
|
|
26
|
+
"get_market_data_cache",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SimpleCache:
|
|
31
|
+
"""
|
|
32
|
+
Simple in-memory cache with TTL (Time To Live) support.
|
|
33
|
+
|
|
34
|
+
Features:
|
|
35
|
+
- Key-value storage with expiration
|
|
36
|
+
- Automatic cleanup of expired entries
|
|
37
|
+
- Thread-safe operations (basic)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, default_ttl: float = 300.0, max_size: int | None = 10000) -> None:
|
|
41
|
+
self._cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
|
|
42
|
+
self._default_ttl = default_ttl
|
|
43
|
+
self._max_size = max_size
|
|
44
|
+
self._lock = threading.RLock()
|
|
45
|
+
self._hits = 0
|
|
46
|
+
self._misses = 0
|
|
47
|
+
|
|
48
|
+
def _get_or_default(self, key: str, default: Any) -> Any:
|
|
49
|
+
with self._lock:
|
|
50
|
+
cached_entry = self._cache.get(key)
|
|
51
|
+
if cached_entry is None:
|
|
52
|
+
self._misses += 1
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
value, expiry = cached_entry
|
|
56
|
+
if time.time() > expiry:
|
|
57
|
+
self._cache.pop(key, None)
|
|
58
|
+
self._misses += 1
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
self._hits += 1
|
|
62
|
+
self._cache.move_to_end(key)
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
def get(self, key: str) -> Any | None:
|
|
66
|
+
return self._get_or_default(key, None)
|
|
67
|
+
|
|
68
|
+
def set(self, key: str, value: Any, ttl: float | None = None) -> None:
|
|
69
|
+
if ttl is None:
|
|
70
|
+
ttl = self._default_ttl
|
|
71
|
+
|
|
72
|
+
expiry = time.time() + ttl
|
|
73
|
+
with self._lock:
|
|
74
|
+
if key in self._cache:
|
|
75
|
+
self._cache.pop(key)
|
|
76
|
+
self._cache[key] = (value, expiry)
|
|
77
|
+
if self._max_size is not None and self._max_size > 0:
|
|
78
|
+
while len(self._cache) > self._max_size:
|
|
79
|
+
self._cache.popitem(last=False)
|
|
80
|
+
|
|
81
|
+
def delete(self, key: str) -> None:
|
|
82
|
+
with self._lock:
|
|
83
|
+
self._cache.pop(key, None)
|
|
84
|
+
|
|
85
|
+
def clear(self) -> None:
|
|
86
|
+
with self._lock:
|
|
87
|
+
self._cache.clear()
|
|
88
|
+
|
|
89
|
+
def cleanup(self) -> int:
|
|
90
|
+
with self._lock:
|
|
91
|
+
now = time.time()
|
|
92
|
+
active_cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
|
|
93
|
+
removed = 0
|
|
94
|
+
for key, entry in self._cache.items():
|
|
95
|
+
if now > entry[1]:
|
|
96
|
+
removed += 1
|
|
97
|
+
else:
|
|
98
|
+
active_cache[key] = entry
|
|
99
|
+
self._cache = active_cache
|
|
100
|
+
return removed
|
|
101
|
+
|
|
102
|
+
def size(self) -> int:
|
|
103
|
+
with self._lock:
|
|
104
|
+
return len(self._cache)
|
|
105
|
+
|
|
106
|
+
def __iter__(self) -> Iterator[str]:
|
|
107
|
+
return iter(self.keys())
|
|
108
|
+
|
|
109
|
+
def keys(self) -> list[str]:
|
|
110
|
+
with self._lock:
|
|
111
|
+
return list(self._cache.keys())
|
|
112
|
+
|
|
113
|
+
def get_stats(self) -> dict[str, float]:
|
|
114
|
+
with self._lock:
|
|
115
|
+
total = self._hits + self._misses
|
|
116
|
+
hit_rate = self._hits / total if total else 0.0
|
|
117
|
+
return {
|
|
118
|
+
"size": float(len(self._cache)),
|
|
119
|
+
"hits": float(self._hits),
|
|
120
|
+
"misses": float(self._misses),
|
|
121
|
+
"hit_rate": hit_rate,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ExchangeInfoCache:
|
|
126
|
+
"""Specialized cache for exchange information."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, ttl: float = 3600.0) -> None:
|
|
129
|
+
self._cache = SimpleCache(default_ttl=ttl)
|
|
130
|
+
|
|
131
|
+
def get_trading_pairs(self, exchange: str) -> list[str] | None:
|
|
132
|
+
return self._cache.get(f"{exchange}:trading_pairs")
|
|
133
|
+
|
|
134
|
+
def set_trading_pairs(self, exchange: str, pairs: list[str]) -> None:
|
|
135
|
+
self._cache.set(f"{exchange}:trading_pairs", pairs)
|
|
136
|
+
|
|
137
|
+
def get_exchange_info(self, exchange: str, symbol: str) -> dict[str, Any] | None:
|
|
138
|
+
return self._cache.get(f"{exchange}:{symbol}:info")
|
|
139
|
+
|
|
140
|
+
def set_exchange_info(self, exchange: str, symbol: str, info: dict[str, Any]) -> None:
|
|
141
|
+
self._cache.set(f"{exchange}:{symbol}:info", info)
|
|
142
|
+
|
|
143
|
+
def clear_exchange(self, exchange: str) -> None:
|
|
144
|
+
keys_to_delete = [key for key in self._cache if key.startswith(f"{exchange}:")]
|
|
145
|
+
for key in keys_to_delete:
|
|
146
|
+
self._cache.delete(key)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class MarketDataCache:
|
|
150
|
+
"""Cache for market data with shorter TTL."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, ttl: float = 5.0) -> None:
|
|
153
|
+
self._cache = SimpleCache(default_ttl=ttl)
|
|
154
|
+
|
|
155
|
+
def get_ticker(self, exchange: str, symbol: str) -> Any | None:
|
|
156
|
+
return self._cache.get(f"{exchange}:{symbol}:ticker")
|
|
157
|
+
|
|
158
|
+
def set_ticker(self, exchange: str, symbol: str, ticker: Any) -> None:
|
|
159
|
+
self._cache.set(f"{exchange}:{symbol}:ticker", ticker)
|
|
160
|
+
|
|
161
|
+
def get_orderbook(self, exchange: str, symbol: str) -> Any | None:
|
|
162
|
+
return self._cache.get(f"{exchange}:{symbol}:orderbook")
|
|
163
|
+
|
|
164
|
+
def set_orderbook(self, exchange: str, symbol: str, orderbook: Any) -> None:
|
|
165
|
+
self._cache.set(f"{exchange}:{symbol}:orderbook", orderbook)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cached(
|
|
169
|
+
ttl: float = 300.0,
|
|
170
|
+
cache_instance: SimpleCache | None = None,
|
|
171
|
+
maxsize: int | None = None,
|
|
172
|
+
) -> Callable[[F], F]:
|
|
173
|
+
"""Decorator for caching function results."""
|
|
174
|
+
if cache_instance is None:
|
|
175
|
+
cache_instance = SimpleCache(default_ttl=ttl, max_size=maxsize)
|
|
176
|
+
|
|
177
|
+
def decorator(func: F) -> F:
|
|
178
|
+
@wraps(func)
|
|
179
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
180
|
+
key_parts = [func.__name__]
|
|
181
|
+
key_parts.extend(str(arg) for arg in args)
|
|
182
|
+
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
|
183
|
+
cache_key = ":".join(key_parts)
|
|
184
|
+
|
|
185
|
+
cached_value = cache_instance._get_or_default(cache_key, _CACHE_MISSING)
|
|
186
|
+
if cached_value is not _CACHE_MISSING:
|
|
187
|
+
return cached_value
|
|
188
|
+
|
|
189
|
+
result = func(*args, **kwargs)
|
|
190
|
+
cache_instance.set(cache_key, result, ttl)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
wrapper.cache = cache_instance # type: ignore
|
|
194
|
+
wrapper.clear_cache = cache_instance.clear # type: ignore
|
|
195
|
+
wrapper.cache_stats = cache_instance.get_stats # type: ignore
|
|
196
|
+
|
|
197
|
+
return wrapper # type: ignore
|
|
198
|
+
|
|
199
|
+
return decorator
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Global cache instances
|
|
203
|
+
_exchange_info_cache = ExchangeInfoCache()
|
|
204
|
+
_market_data_cache = MarketDataCache()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_exchange_info_cache() -> ExchangeInfoCache:
|
|
208
|
+
return _exchange_info_cache
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_market_data_cache() -> MarketDataCache:
|
|
212
|
+
return _market_data_cache
|