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.
Files changed (93) hide show
  1. bt_api_base/__init__.py +7 -0
  2. bt_api_base/_compat.py +29 -0
  3. bt_api_base/_plugin_shims.py +28 -0
  4. bt_api_base/_version.py +5 -0
  5. bt_api_base/auth_config.py +171 -0
  6. bt_api_base/balance_utils.py +99 -0
  7. bt_api_base/cache.py +212 -0
  8. bt_api_base/config_loader.py +288 -0
  9. bt_api_base/connection_pool.py +247 -0
  10. bt_api_base/containers/__init__.py +45 -0
  11. bt_api_base/containers/accounts/__init__.py +5 -0
  12. bt_api_base/containers/accounts/account.py +182 -0
  13. bt_api_base/containers/auto_init_mixin.py +63 -0
  14. bt_api_base/containers/balances/__init__.py +5 -0
  15. bt_api_base/containers/balances/balance.py +260 -0
  16. bt_api_base/containers/bars/__init__.py +5 -0
  17. bt_api_base/containers/bars/bar.py +90 -0
  18. bt_api_base/containers/exchanges/__init__.py +5 -0
  19. bt_api_base/containers/exchanges/exchange_data.py +89 -0
  20. bt_api_base/containers/fundingrates/__init__.py +5 -0
  21. bt_api_base/containers/fundingrates/funding_rate.py +122 -0
  22. bt_api_base/containers/greeks/__init__.py +5 -0
  23. bt_api_base/containers/greeks/greeks.py +63 -0
  24. bt_api_base/containers/incomes/__init__.py +5 -0
  25. bt_api_base/containers/incomes/income.py +77 -0
  26. bt_api_base/containers/instrument.py +177 -0
  27. bt_api_base/containers/liquidations/__init__.py +5 -0
  28. bt_api_base/containers/liquidations/liquidation.py +61 -0
  29. bt_api_base/containers/markprices/__init__.py +5 -0
  30. bt_api_base/containers/markprices/mark_price.py +70 -0
  31. bt_api_base/containers/orderbooks/__init__.py +5 -0
  32. bt_api_base/containers/orderbooks/orderbook.py +94 -0
  33. bt_api_base/containers/orders/__init__.py +5 -0
  34. bt_api_base/containers/orders/order.py +290 -0
  35. bt_api_base/containers/positions/__init__.py +5 -0
  36. bt_api_base/containers/positions/position.py +196 -0
  37. bt_api_base/containers/requestdatas/__init__.py +5 -0
  38. bt_api_base/containers/requestdatas/request_data.py +173 -0
  39. bt_api_base/containers/symbols/__init__.py +5 -0
  40. bt_api_base/containers/symbols/symbol.py +125 -0
  41. bt_api_base/containers/tickers/__init__.py +5 -0
  42. bt_api_base/containers/tickers/ticker.py +169 -0
  43. bt_api_base/containers/timers/__init__.py +5 -0
  44. bt_api_base/containers/timers/timer.py +15 -0
  45. bt_api_base/containers/trades/__init__.py +5 -0
  46. bt_api_base/containers/trades/trade.py +151 -0
  47. bt_api_base/core/__init__.py +46 -0
  48. bt_api_base/core/async_context.py +475 -0
  49. bt_api_base/core/dependency_injection.py +261 -0
  50. bt_api_base/core/interfaces.py +325 -0
  51. bt_api_base/core/services.py +609 -0
  52. bt_api_base/error.py +432 -0
  53. bt_api_base/event_bus.py +136 -0
  54. bt_api_base/exceptions.py +359 -0
  55. bt_api_base/feeds/__init__.py +18 -0
  56. bt_api_base/feeds/abstract_feed.py +305 -0
  57. bt_api_base/feeds/base_stream.py +116 -0
  58. bt_api_base/feeds/capability.py +110 -0
  59. bt_api_base/feeds/connection_mixin.py +71 -0
  60. bt_api_base/feeds/feed.py +633 -0
  61. bt_api_base/feeds/http_client.py +304 -0
  62. bt_api_base/feeds/my_websocket_app.py +324 -0
  63. bt_api_base/functions/__init__.py +7 -0
  64. bt_api_base/functions/async_base.py +132 -0
  65. bt_api_base/functions/calculate_time.py +131 -0
  66. bt_api_base/functions/log_message.py +124 -0
  67. bt_api_base/functions/utils.py +301 -0
  68. bt_api_base/gateway/__init__.py +22 -0
  69. bt_api_base/gateway/adapters/__init__.py +42 -0
  70. bt_api_base/gateway/adapters/base.py +56 -0
  71. bt_api_base/gateway/adapters/plugin_adapter.py +81 -0
  72. bt_api_base/gateway/models.py +53 -0
  73. bt_api_base/gateway/protocol.py +39 -0
  74. bt_api_base/gateway/registrar.py +37 -0
  75. bt_api_base/instrument_manager.py +122 -0
  76. bt_api_base/logging_factory.py +109 -0
  77. bt_api_base/plugins/__init__.py +19 -0
  78. bt_api_base/plugins/errors.py +17 -0
  79. bt_api_base/plugins/loader.py +229 -0
  80. bt_api_base/plugins/protocol.py +16 -0
  81. bt_api_base/rate_limiter.py +304 -0
  82. bt_api_base/registry.py +274 -0
  83. bt_api_base/security.py +209 -0
  84. bt_api_base/utils/__init__.py +3 -0
  85. bt_api_base/utils/time.py +29 -0
  86. bt_api_base/websocket/__init__.py +41 -0
  87. bt_api_base/websocket/exchange_adapters.py +488 -0
  88. bt_api_base/websocket_manager.py +594 -0
  89. bt_api_base-0.15.1.dist-info/METADATA +536 -0
  90. bt_api_base-0.15.1.dist-info/RECORD +93 -0
  91. bt_api_base-0.15.1.dist-info/WHEEL +5 -0
  92. bt_api_base-0.15.1.dist-info/licenses/LICENSE +21 -0
  93. bt_api_base-0.15.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from bt_api_base._version import __version__
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ ]
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
@@ -0,0 +1,5 @@
1
+ """Package version metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.15.0"
@@ -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