walrasquant-lib 0.4.20__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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Any, Mapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OkxHttpError(Exception):
|
|
5
|
+
"""
|
|
6
|
+
Define the class for all OKX http specific errors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self, status_code: int, message: str, headers: Mapping[str, Any]
|
|
11
|
+
) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Define the base class for all OKX http specific errors.
|
|
14
|
+
"""
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.status = status_code
|
|
17
|
+
self.message = message
|
|
18
|
+
self.headers = headers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OkxRequestError(Exception):
|
|
22
|
+
"""
|
|
23
|
+
The base class for all OKX specific errors.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
https://www.okx.com/docs-v5/en/?python#error-code
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, error_code: int, status_code: int | None, message: str | None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.code = error_code
|
|
34
|
+
self.status_code = status_code
|
|
35
|
+
self.message = message
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
return f"{type(self).__name__}(code={self.code}, message='{self.message}')"
|
|
39
|
+
|
|
40
|
+
__str__ = __repr__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def retry_check(exc: BaseException) -> bool:
|
|
44
|
+
if isinstance(exc, OkxRequestError):
|
|
45
|
+
return exc.code in [50001, 50013, 50026, 51054, 51149, 51412]
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OkxRateLimitError(Exception):
|
|
50
|
+
"""
|
|
51
|
+
Raised when OKX API rate limit is exceeded.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
retry_after: float = 0.0,
|
|
58
|
+
scope: str | None = None,
|
|
59
|
+
endpoint: str | None = None,
|
|
60
|
+
code: int | None = None,
|
|
61
|
+
):
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.retry_after = retry_after
|
|
64
|
+
self.scope = scope
|
|
65
|
+
self.endpoint = endpoint
|
|
66
|
+
self.code = code
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
from walrasquant.base import ExchangeManager
|
|
3
|
+
import ccxt
|
|
4
|
+
import msgspec
|
|
5
|
+
from walrasquant.config import BasicConfig
|
|
6
|
+
from walrasquant.exchange.okx.schema import OkxMarket
|
|
7
|
+
from walrasquant.exchange.okx.constants import OkxAccountType
|
|
8
|
+
from walrasquant.constants import AccountType, ConfigType
|
|
9
|
+
from walrasquant.schema import InstrumentId
|
|
10
|
+
from walrasquant.error import EngineBuildError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OkxExchangeManager(ExchangeManager):
|
|
14
|
+
api: ccxt.okx
|
|
15
|
+
market: Dict[str, OkxMarket] # symbol -> okx market
|
|
16
|
+
market_id: Dict[str, str] # symbol -> exchange symbol id
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: ConfigType | None = None):
|
|
19
|
+
resolved_config: ConfigType = {
|
|
20
|
+
"apiKey": config.get("apiKey", "") if config else "",
|
|
21
|
+
"secret": config.get("secret", "") if config else "",
|
|
22
|
+
"exchange_id": config.get("exchange_id", "okx") if config else "okx",
|
|
23
|
+
"sandbox": config.get("sandbox", False) if config else False,
|
|
24
|
+
}
|
|
25
|
+
if config and "password" in config:
|
|
26
|
+
resolved_config["password"] = config["password"]
|
|
27
|
+
super().__init__(resolved_config)
|
|
28
|
+
self.passphrase = resolved_config.get("password")
|
|
29
|
+
self._public_conn_account_type: OkxAccountType | None = None
|
|
30
|
+
|
|
31
|
+
def load_markets(self, reload: bool = False) -> None:
|
|
32
|
+
market = self.api.load_markets(reload=reload)
|
|
33
|
+
for symbol, mkt in market.items():
|
|
34
|
+
try:
|
|
35
|
+
mkt_json = msgspec.json.encode(mkt)
|
|
36
|
+
mkt = msgspec.json.decode(mkt_json, type=OkxMarket)
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
mkt.spot or mkt.linear or mkt.inverse or mkt.future
|
|
40
|
+
) and not mkt.option:
|
|
41
|
+
symbol = self._parse_symbol(mkt, exchange_suffix="OKX")
|
|
42
|
+
mkt.symbol = symbol
|
|
43
|
+
self.market[symbol] = mkt
|
|
44
|
+
self.market_id[mkt.id] = (
|
|
45
|
+
symbol # since okx symbol id is identical, no need to distinguish spot, linear, inverse
|
|
46
|
+
)
|
|
47
|
+
except msgspec.ValidationError as ve:
|
|
48
|
+
self._log.warning(f"Symbol Format Error: {ve}, {symbol}, {mkt}")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
def option(
|
|
52
|
+
self,
|
|
53
|
+
base: str | None = None,
|
|
54
|
+
quote: str | None = None,
|
|
55
|
+
exclude: List[str] | None = None,
|
|
56
|
+
) -> List[str]:
|
|
57
|
+
raise NotImplementedError("Option is not supported for OKX")
|
|
58
|
+
|
|
59
|
+
def validate_public_connector_config(
|
|
60
|
+
self, account_type: AccountType, basic_config: BasicConfig | None = None
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Validate public connector configuration for OKX exchange"""
|
|
63
|
+
if basic_config is None:
|
|
64
|
+
raise EngineBuildError("basic_config is required for OKX")
|
|
65
|
+
if not isinstance(account_type, OkxAccountType):
|
|
66
|
+
raise EngineBuildError(f"Expected OkxAccountType, got {type(account_type)}")
|
|
67
|
+
|
|
68
|
+
if basic_config.testnet != account_type.is_testnet:
|
|
69
|
+
raise EngineBuildError(
|
|
70
|
+
f"The `testnet` setting of OKX is not consistent with the public connector's account type `{account_type}`."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def validate_public_connector_limits(
|
|
74
|
+
self, existing_connectors: Dict[AccountType, Any]
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Validate public connector limits for OKX exchange"""
|
|
77
|
+
okx_connectors = [
|
|
78
|
+
c
|
|
79
|
+
for c in existing_connectors.values()
|
|
80
|
+
if hasattr(c, "account_type") and isinstance(c.account_type, OkxAccountType)
|
|
81
|
+
]
|
|
82
|
+
if len(okx_connectors) > 1:
|
|
83
|
+
raise EngineBuildError(
|
|
84
|
+
"Only one public connector is supported for OKX, please remove the extra public connector config."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def set_public_connector_account_type(self, account_type: OkxAccountType) -> None:
|
|
88
|
+
"""Set the account type for public connector configuration"""
|
|
89
|
+
self._public_conn_account_type = account_type
|
|
90
|
+
|
|
91
|
+
def instrument_id_to_account_type(self, instrument_id: InstrumentId) -> AccountType:
|
|
92
|
+
"""Convert an instrument ID to the appropriate account type for OKX exchange"""
|
|
93
|
+
if self._public_conn_account_type is None:
|
|
94
|
+
raise EngineBuildError(
|
|
95
|
+
"Public connector account type not set for OKX. Please add OKX in public_conn_config."
|
|
96
|
+
)
|
|
97
|
+
return self._public_conn_account_type
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
okx = OkxExchangeManager()
|
|
102
|
+
# print(okx.market)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OKX exchange factory implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from walrasquant.constants import ExchangeType, AccountType, ConfigType
|
|
8
|
+
from walrasquant.config import (
|
|
9
|
+
PublicConnectorConfig,
|
|
10
|
+
PrivateConnectorConfig,
|
|
11
|
+
BasicConfig,
|
|
12
|
+
)
|
|
13
|
+
from walrasquant.exchange.base_factory import ExchangeFactory, BuildContext
|
|
14
|
+
from walrasquant.base import (
|
|
15
|
+
ExchangeManager,
|
|
16
|
+
PublicConnector,
|
|
17
|
+
PrivateConnector,
|
|
18
|
+
ExecutionManagementSystem,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from walrasquant.exchange.okx.exchange import OkxExchangeManager
|
|
22
|
+
from walrasquant.exchange.okx.connector import OkxPublicConnector, OkxPrivateConnector
|
|
23
|
+
from walrasquant.exchange.okx.ems import OkxExecutionManagementSystem
|
|
24
|
+
from walrasquant.exchange.okx.constants import OkxAccountType
|
|
25
|
+
from walrasquant.exchange.okx.schema import OkxMarket
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OkxFactory(ExchangeFactory):
|
|
29
|
+
"""Factory for creating OKX exchange components."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def exchange_type(self) -> ExchangeType:
|
|
33
|
+
return ExchangeType.OKX
|
|
34
|
+
|
|
35
|
+
def create_manager(self, basic_config: BasicConfig) -> ExchangeManager:
|
|
36
|
+
"""Create OkxExchangeManager."""
|
|
37
|
+
ccxt_config: ConfigType = {
|
|
38
|
+
"apiKey": basic_config.api_key or "",
|
|
39
|
+
"secret": basic_config.secret or "",
|
|
40
|
+
"exchange_id": "okx",
|
|
41
|
+
"sandbox": basic_config.testnet,
|
|
42
|
+
}
|
|
43
|
+
if basic_config.passphrase:
|
|
44
|
+
ccxt_config["password"] = basic_config.passphrase
|
|
45
|
+
|
|
46
|
+
return OkxExchangeManager(ccxt_config)
|
|
47
|
+
|
|
48
|
+
def create_public_connector(
|
|
49
|
+
self,
|
|
50
|
+
config: PublicConnectorConfig,
|
|
51
|
+
exchange: ExchangeManager,
|
|
52
|
+
context: BuildContext,
|
|
53
|
+
) -> PublicConnector:
|
|
54
|
+
"""Create OkxPublicConnector."""
|
|
55
|
+
if not isinstance(config.account_type, OkxAccountType):
|
|
56
|
+
raise TypeError("OKX public connector requires OkxAccountType")
|
|
57
|
+
connector = OkxPublicConnector(
|
|
58
|
+
account_type=config.account_type,
|
|
59
|
+
exchange=cast(OkxExchangeManager, exchange),
|
|
60
|
+
msgbus=context.msgbus,
|
|
61
|
+
clock=context.clock,
|
|
62
|
+
task_manager=context.task_manager,
|
|
63
|
+
enable_rate_limit=config.enable_rate_limit,
|
|
64
|
+
custom_url=config.custom_url,
|
|
65
|
+
max_subscriptions_per_client=config.max_subscriptions_per_client,
|
|
66
|
+
max_clients=config.max_clients,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# OKX needs post-creation setup
|
|
70
|
+
self.setup_public_connector(exchange, connector, config.account_type)
|
|
71
|
+
return connector
|
|
72
|
+
|
|
73
|
+
def create_private_connector(
|
|
74
|
+
self,
|
|
75
|
+
config: PrivateConnectorConfig,
|
|
76
|
+
exchange: ExchangeManager,
|
|
77
|
+
context: BuildContext,
|
|
78
|
+
account_type: AccountType | None = None,
|
|
79
|
+
) -> PrivateConnector:
|
|
80
|
+
"""Create OkxPrivateConnector."""
|
|
81
|
+
# Use provided account_type or determine from exchange testnet status
|
|
82
|
+
if account_type:
|
|
83
|
+
final_account_type = account_type
|
|
84
|
+
else:
|
|
85
|
+
final_account_type = self.get_private_account_type(exchange, config)
|
|
86
|
+
|
|
87
|
+
if not isinstance(final_account_type, OkxAccountType):
|
|
88
|
+
raise TypeError("OKX private connector requires OkxAccountType")
|
|
89
|
+
return OkxPrivateConnector(
|
|
90
|
+
exchange=cast(OkxExchangeManager, exchange),
|
|
91
|
+
account_type=final_account_type,
|
|
92
|
+
cache=context.cache,
|
|
93
|
+
clock=context.clock,
|
|
94
|
+
msgbus=context.msgbus,
|
|
95
|
+
registry=context.registry,
|
|
96
|
+
enable_rate_limit=config.enable_rate_limit,
|
|
97
|
+
task_manager=context.task_manager,
|
|
98
|
+
max_retries=config.max_retries,
|
|
99
|
+
delay_initial_ms=config.delay_initial_ms,
|
|
100
|
+
delay_max_ms=config.delay_max_ms,
|
|
101
|
+
backoff_factor=config.backoff_factor,
|
|
102
|
+
max_subscriptions_per_client=config.max_subscriptions_per_client,
|
|
103
|
+
max_clients=config.max_clients,
|
|
104
|
+
order_query_config=context.order_query_config,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def create_ems(
|
|
108
|
+
self, exchange: ExchangeManager, context: BuildContext
|
|
109
|
+
) -> ExecutionManagementSystem:
|
|
110
|
+
"""Create OkxExecutionManagementSystem."""
|
|
111
|
+
return OkxExecutionManagementSystem(
|
|
112
|
+
market=cast(dict[str, OkxMarket], exchange.market),
|
|
113
|
+
cache=context.cache,
|
|
114
|
+
msgbus=context.msgbus,
|
|
115
|
+
clock=context.clock,
|
|
116
|
+
task_manager=context.task_manager,
|
|
117
|
+
registry=context.registry,
|
|
118
|
+
queue_maxsize=context.queue_maxsize,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def get_private_account_type(
|
|
122
|
+
self, exchange: ExchangeManager, config: PrivateConnectorConfig
|
|
123
|
+
) -> AccountType:
|
|
124
|
+
"""Determine account type based on testnet status."""
|
|
125
|
+
return OkxAccountType.DEMO if exchange.is_testnet else OkxAccountType.LIVE
|
|
126
|
+
|
|
127
|
+
def setup_public_connector(
|
|
128
|
+
self,
|
|
129
|
+
exchange: ExchangeManager,
|
|
130
|
+
connector: PublicConnector,
|
|
131
|
+
account_type: AccountType,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Setup public connector account type."""
|
|
134
|
+
if not isinstance(account_type, OkxAccountType):
|
|
135
|
+
raise TypeError("OKX public connector requires OkxAccountType")
|
|
136
|
+
cast(OkxExchangeManager, exchange).set_public_connector_account_type(
|
|
137
|
+
account_type
|
|
138
|
+
)
|