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,321 @@
|
|
|
1
|
+
import asyncpg
|
|
2
|
+
import psycopg2
|
|
3
|
+
import psycopg2.extensions
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Dict, Set, List, Optional, Any
|
|
6
|
+
|
|
7
|
+
from walrasquant.backends.db import StorageBackend
|
|
8
|
+
from walrasquant.schema import Order, Position, Balance, AccountBalance
|
|
9
|
+
from walrasquant.constants import AccountType, ExchangeType, get_postgresql_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PostgreSQLBackend(StorageBackend):
|
|
13
|
+
def __init__(
|
|
14
|
+
self, strategy_id: str, user_id: str, table_prefix: str, log, **kwargs
|
|
15
|
+
):
|
|
16
|
+
super().__init__(strategy_id, user_id, table_prefix, log, **kwargs)
|
|
17
|
+
self._pg_async: Optional[asyncpg.Pool] = None
|
|
18
|
+
self._pg: Optional[psycopg2.extensions.connection] = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def _pool(self) -> asyncpg.Pool:
|
|
22
|
+
assert self._pg_async is not None
|
|
23
|
+
return self._pg_async
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def _sync_conn(self) -> psycopg2.extensions.connection:
|
|
27
|
+
assert self._pg is not None
|
|
28
|
+
return self._pg
|
|
29
|
+
|
|
30
|
+
async def _init_conn(self) -> None:
|
|
31
|
+
pg_config = get_postgresql_config()
|
|
32
|
+
self._pg_async = await asyncpg.create_pool(**pg_config)
|
|
33
|
+
self._pg = psycopg2.connect(**pg_config)
|
|
34
|
+
|
|
35
|
+
async def _init_table(self) -> None:
|
|
36
|
+
async with self._pool.acquire() as conn:
|
|
37
|
+
await conn.execute(f"""
|
|
38
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_orders (
|
|
39
|
+
timestamp BIGINT,
|
|
40
|
+
oid TEXT PRIMARY KEY,
|
|
41
|
+
eid TEXT,
|
|
42
|
+
symbol TEXT,
|
|
43
|
+
side TEXT,
|
|
44
|
+
type TEXT,
|
|
45
|
+
amount TEXT,
|
|
46
|
+
price DOUBLE PRECISION,
|
|
47
|
+
status TEXT,
|
|
48
|
+
fee TEXT,
|
|
49
|
+
fee_currency TEXT,
|
|
50
|
+
data BYTEA
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_orders_symbol
|
|
54
|
+
ON {self.table_prefix}_orders(symbol);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_positions (
|
|
57
|
+
symbol TEXT PRIMARY KEY,
|
|
58
|
+
exchange TEXT,
|
|
59
|
+
side TEXT,
|
|
60
|
+
amount TEXT,
|
|
61
|
+
data BYTEA
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_open_orders (
|
|
65
|
+
oid TEXT PRIMARY KEY,
|
|
66
|
+
eid TEXT,
|
|
67
|
+
exchange TEXT,
|
|
68
|
+
symbol TEXT
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_balances (
|
|
72
|
+
asset TEXT,
|
|
73
|
+
account_type TEXT,
|
|
74
|
+
free TEXT,
|
|
75
|
+
locked TEXT,
|
|
76
|
+
PRIMARY KEY (asset, account_type)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_params (
|
|
80
|
+
key TEXT PRIMARY KEY,
|
|
81
|
+
value BYTEA,
|
|
82
|
+
timestamp BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT
|
|
83
|
+
);
|
|
84
|
+
""")
|
|
85
|
+
|
|
86
|
+
async def close(self) -> None:
|
|
87
|
+
if self._pg_async:
|
|
88
|
+
await self._pg_async.close()
|
|
89
|
+
if self._pg:
|
|
90
|
+
self._pg.close()
|
|
91
|
+
|
|
92
|
+
async def sync_orders(self, mem_orders: Dict[str, Order]) -> None:
|
|
93
|
+
async with self._pool.acquire() as conn:
|
|
94
|
+
for order in mem_orders.copy().values():
|
|
95
|
+
await conn.execute(
|
|
96
|
+
f"INSERT INTO {self.table_prefix}_orders "
|
|
97
|
+
"(timestamp, oid, eid, symbol, side, type, amount, price, status, fee, fee_currency, data) "
|
|
98
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) "
|
|
99
|
+
"ON CONFLICT (oid) DO UPDATE SET "
|
|
100
|
+
"timestamp = EXCLUDED.timestamp, "
|
|
101
|
+
"eid = EXCLUDED.eid, "
|
|
102
|
+
"symbol = EXCLUDED.symbol, "
|
|
103
|
+
"side = EXCLUDED.side, "
|
|
104
|
+
"type = EXCLUDED.type, "
|
|
105
|
+
"amount = EXCLUDED.amount, "
|
|
106
|
+
"price = EXCLUDED.price, "
|
|
107
|
+
"status = EXCLUDED.status, "
|
|
108
|
+
"fee = EXCLUDED.fee, "
|
|
109
|
+
"fee_currency = EXCLUDED.fee_currency, "
|
|
110
|
+
"data = EXCLUDED.data",
|
|
111
|
+
order.timestamp or 0,
|
|
112
|
+
str(order.oid),
|
|
113
|
+
str(order.eid),
|
|
114
|
+
str(order.symbol),
|
|
115
|
+
str(order.side.value) if order.side is not None else None,
|
|
116
|
+
str(order.type.value) if order.type is not None else None,
|
|
117
|
+
str(order.amount),
|
|
118
|
+
float(_p)
|
|
119
|
+
if (_p := order.price or order.average) is not None
|
|
120
|
+
else None,
|
|
121
|
+
str(order.status.value),
|
|
122
|
+
str(order.fee) if order.fee is not None else None,
|
|
123
|
+
order.fee_currency,
|
|
124
|
+
self._encode(order),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def sync_positions(self, mem_positions: Dict[str, Position]) -> None:
|
|
128
|
+
async with self._pool.acquire() as conn:
|
|
129
|
+
db_positions = await conn.fetch(
|
|
130
|
+
f"SELECT symbol FROM {self.table_prefix}_positions"
|
|
131
|
+
)
|
|
132
|
+
db_position_symbols = {row["symbol"] for row in db_positions}
|
|
133
|
+
|
|
134
|
+
positions_to_delete = db_position_symbols - set(mem_positions.keys())
|
|
135
|
+
if positions_to_delete:
|
|
136
|
+
for symbol in positions_to_delete:
|
|
137
|
+
await conn.execute(
|
|
138
|
+
f"DELETE FROM {self.table_prefix}_positions WHERE symbol = $1",
|
|
139
|
+
str(symbol),
|
|
140
|
+
)
|
|
141
|
+
self._log.debug(
|
|
142
|
+
f"Deleted {len(positions_to_delete)} stale positions from database"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
for symbol, position in mem_positions.copy().items():
|
|
146
|
+
await conn.execute(
|
|
147
|
+
f"INSERT INTO {self.table_prefix}_positions "
|
|
148
|
+
"(symbol, exchange, side, amount, data) VALUES ($1, $2, $3, $4, $5) "
|
|
149
|
+
"ON CONFLICT (symbol) DO UPDATE SET "
|
|
150
|
+
"exchange = EXCLUDED.exchange, "
|
|
151
|
+
"side = EXCLUDED.side, "
|
|
152
|
+
"amount = EXCLUDED.amount, "
|
|
153
|
+
"data = EXCLUDED.data",
|
|
154
|
+
str(symbol),
|
|
155
|
+
str(position.exchange.value),
|
|
156
|
+
str(position.side.value) if position.side else "FLAT",
|
|
157
|
+
str(position.amount),
|
|
158
|
+
self._encode(position),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def sync_open_orders(
|
|
162
|
+
self,
|
|
163
|
+
mem_open_orders: Dict[ExchangeType, Set[str]],
|
|
164
|
+
mem_orders: Dict[str, Order],
|
|
165
|
+
) -> None:
|
|
166
|
+
async with self._pool.acquire() as conn:
|
|
167
|
+
await conn.execute(f"DELETE FROM {self.table_prefix}_open_orders")
|
|
168
|
+
|
|
169
|
+
for exchange, oids in mem_open_orders.copy().items():
|
|
170
|
+
for oid in oids.copy():
|
|
171
|
+
order = mem_orders.get(oid)
|
|
172
|
+
if order:
|
|
173
|
+
await conn.execute(
|
|
174
|
+
f"INSERT INTO {self.table_prefix}_open_orders "
|
|
175
|
+
"(oid, eid, exchange, symbol) VALUES ($1, $2, $3, $4)",
|
|
176
|
+
str(oid),
|
|
177
|
+
str(order.eid),
|
|
178
|
+
str(exchange.value),
|
|
179
|
+
str(order.symbol),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def sync_balances(
|
|
183
|
+
self, mem_account_balance: Dict[AccountType, AccountBalance]
|
|
184
|
+
) -> None:
|
|
185
|
+
async with self._pool.acquire() as conn:
|
|
186
|
+
for account_type, balance in mem_account_balance.copy().items():
|
|
187
|
+
for asset, amount in balance.balances.items():
|
|
188
|
+
await conn.execute(
|
|
189
|
+
f"INSERT INTO {self.table_prefix}_balances "
|
|
190
|
+
"(asset, account_type, free, locked) VALUES ($1, $2, $3, $4) "
|
|
191
|
+
"ON CONFLICT (asset, account_type) DO UPDATE SET "
|
|
192
|
+
"free = EXCLUDED.free, "
|
|
193
|
+
"locked = EXCLUDED.locked",
|
|
194
|
+
str(asset),
|
|
195
|
+
str(account_type.value),
|
|
196
|
+
str(amount.free),
|
|
197
|
+
str(amount.locked),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def get_order(
|
|
201
|
+
self,
|
|
202
|
+
oid: str,
|
|
203
|
+
mem_orders: Dict[str, Order],
|
|
204
|
+
) -> Optional[Order]:
|
|
205
|
+
if order := mem_orders.get(oid):
|
|
206
|
+
return order
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
cursor = self._sync_conn.cursor()
|
|
210
|
+
with cursor:
|
|
211
|
+
cursor.execute(
|
|
212
|
+
f"SELECT data FROM {self.table_prefix}_orders WHERE oid = %s",
|
|
213
|
+
(oid,),
|
|
214
|
+
)
|
|
215
|
+
row = cursor.fetchone()
|
|
216
|
+
if row:
|
|
217
|
+
order = self._decode(row[0], Order)
|
|
218
|
+
mem_orders[oid] = order
|
|
219
|
+
return order
|
|
220
|
+
return None
|
|
221
|
+
except psycopg2.DatabaseError as error:
|
|
222
|
+
self._log.error(f"Error getting order from PostgreSQL: {error}")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def get_symbol_orders(self, symbol: str) -> Set[str]:
|
|
226
|
+
cursor = self._sync_conn.cursor()
|
|
227
|
+
with cursor:
|
|
228
|
+
cursor.execute(
|
|
229
|
+
f"SELECT oid FROM {self.table_prefix}_orders WHERE symbol = %s",
|
|
230
|
+
(symbol,),
|
|
231
|
+
)
|
|
232
|
+
result = {row[0] for row in cursor.fetchall()}
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
def get_all_positions(self, exchange_id: ExchangeType) -> Dict[str, Position]:
|
|
236
|
+
positions = {}
|
|
237
|
+
cursor = self._sync_conn.cursor()
|
|
238
|
+
cursor.execute(
|
|
239
|
+
f"SELECT symbol, data FROM {self.table_prefix}_positions WHERE exchange = %s",
|
|
240
|
+
(exchange_id.value,),
|
|
241
|
+
)
|
|
242
|
+
for row in cursor.fetchall():
|
|
243
|
+
position = self._decode(row[1], Position)
|
|
244
|
+
if position.side:
|
|
245
|
+
positions[position.symbol] = position
|
|
246
|
+
cursor.close()
|
|
247
|
+
return positions
|
|
248
|
+
|
|
249
|
+
def get_all_balances(self, account_type: AccountType) -> List[Balance]:
|
|
250
|
+
balances = []
|
|
251
|
+
cursor = self._sync_conn.cursor()
|
|
252
|
+
cursor.execute(
|
|
253
|
+
f"SELECT asset, free, locked FROM {self.table_prefix}_balances WHERE account_type = %s",
|
|
254
|
+
(account_type.value,),
|
|
255
|
+
)
|
|
256
|
+
for row in cursor.fetchall():
|
|
257
|
+
balances.append(
|
|
258
|
+
Balance(asset=row[0], free=Decimal(row[1]), locked=Decimal(row[2]))
|
|
259
|
+
)
|
|
260
|
+
cursor.close()
|
|
261
|
+
return balances
|
|
262
|
+
|
|
263
|
+
async def sync_params(self, mem_params: Dict[str, Any]) -> None:
|
|
264
|
+
import msgspec
|
|
265
|
+
|
|
266
|
+
async with self._pool.acquire() as conn:
|
|
267
|
+
for key, value in mem_params.copy().items():
|
|
268
|
+
try:
|
|
269
|
+
serialized_value = self._encode_param(value)
|
|
270
|
+
await conn.execute(
|
|
271
|
+
f"INSERT INTO {self.table_prefix}_params "
|
|
272
|
+
"(key, value) VALUES ($1, $2) "
|
|
273
|
+
"ON CONFLICT (key) DO UPDATE SET "
|
|
274
|
+
"value = EXCLUDED.value, "
|
|
275
|
+
"timestamp = DEFAULT",
|
|
276
|
+
str(key),
|
|
277
|
+
serialized_value,
|
|
278
|
+
)
|
|
279
|
+
except msgspec.EncodeError as e:
|
|
280
|
+
self._log.error(f"Error serializing parameter {key}: {e}")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self._log.error(f"Error storing parameter {key}: {e}")
|
|
283
|
+
|
|
284
|
+
def get_param(self, key: str, default: Any = None) -> Any:
|
|
285
|
+
import msgspec
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
cursor = self._sync_conn.cursor()
|
|
289
|
+
cursor.execute(
|
|
290
|
+
f"SELECT value FROM {self.table_prefix}_params WHERE key = %s", (key,)
|
|
291
|
+
)
|
|
292
|
+
row = cursor.fetchone()
|
|
293
|
+
cursor.close()
|
|
294
|
+
|
|
295
|
+
if row:
|
|
296
|
+
try:
|
|
297
|
+
return self._decode_param(row[0])
|
|
298
|
+
except msgspec.DecodeError as e:
|
|
299
|
+
self._log.error(f"Error deserializing parameter {key}: {e}")
|
|
300
|
+
return default
|
|
301
|
+
return default
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self._log.error(f"Error getting parameter {key}: {e}")
|
|
304
|
+
return default
|
|
305
|
+
|
|
306
|
+
def get_all_params(self) -> Dict[str, Any]:
|
|
307
|
+
import msgspec
|
|
308
|
+
|
|
309
|
+
params = {}
|
|
310
|
+
try:
|
|
311
|
+
cursor = self._sync_conn.cursor()
|
|
312
|
+
cursor.execute(f"SELECT key, value FROM {self.table_prefix}_params")
|
|
313
|
+
for row in cursor.fetchall():
|
|
314
|
+
try:
|
|
315
|
+
params[row[0]] = self._decode_param(row[1])
|
|
316
|
+
except msgspec.DecodeError as e:
|
|
317
|
+
self._log.error(f"Error deserializing parameter {row[0]}: {e}")
|
|
318
|
+
cursor.close()
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self._log.error(f"Error getting all parameters: {e}")
|
|
321
|
+
return params
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import aiosqlite
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Set, List, Optional, Any
|
|
5
|
+
|
|
6
|
+
from walrasquant.backends.db import StorageBackend
|
|
7
|
+
from walrasquant.schema import Order, Position, Balance, AccountBalance
|
|
8
|
+
from walrasquant.constants import AccountType, ExchangeType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQLiteBackend(StorageBackend):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
strategy_id: str,
|
|
15
|
+
user_id: str,
|
|
16
|
+
table_prefix: str,
|
|
17
|
+
log,
|
|
18
|
+
db_path: str = ".keys/cache.db",
|
|
19
|
+
**kwargs,
|
|
20
|
+
):
|
|
21
|
+
super().__init__(strategy_id, user_id, table_prefix, log, **kwargs)
|
|
22
|
+
self.db_path = db_path
|
|
23
|
+
self._db_async: Optional[aiosqlite.Connection] = None
|
|
24
|
+
self._db: Optional[sqlite3.Connection] = None
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def _async_conn(self) -> aiosqlite.Connection:
|
|
28
|
+
assert self._db_async is not None
|
|
29
|
+
return self._db_async
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def _sync_db(self) -> sqlite3.Connection:
|
|
33
|
+
assert self._db is not None
|
|
34
|
+
return self._db
|
|
35
|
+
|
|
36
|
+
async def _init_conn(self) -> None:
|
|
37
|
+
db_path = Path(self.db_path)
|
|
38
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
self._db_async = await aiosqlite.connect(str(db_path))
|
|
40
|
+
self._db = sqlite3.connect(str(db_path))
|
|
41
|
+
|
|
42
|
+
async def _init_table(self) -> None:
|
|
43
|
+
async with self._async_conn.cursor() as cursor:
|
|
44
|
+
await cursor.executescript(f"""
|
|
45
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_orders (
|
|
46
|
+
timestamp INTEGER,
|
|
47
|
+
oid TEXT PRIMARY KEY,
|
|
48
|
+
eid TEXT,
|
|
49
|
+
symbol TEXT,
|
|
50
|
+
side TEXT,
|
|
51
|
+
type TEXT,
|
|
52
|
+
amount TEXT,
|
|
53
|
+
price REAL,
|
|
54
|
+
status TEXT,
|
|
55
|
+
fee TEXT,
|
|
56
|
+
fee_currency TEXT,
|
|
57
|
+
data BLOB
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_orders_symbol
|
|
61
|
+
ON {self.table_prefix}_orders(symbol);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_positions (
|
|
64
|
+
symbol PRIMARY KEY,
|
|
65
|
+
exchange TEXT,
|
|
66
|
+
side TEXT,
|
|
67
|
+
amount TEXT,
|
|
68
|
+
data BLOB
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_open_orders (
|
|
72
|
+
oid PRIMARY KEY,
|
|
73
|
+
eid TEXT,
|
|
74
|
+
exchange TEXT,
|
|
75
|
+
symbol TEXT
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_balances (
|
|
79
|
+
asset TEXT,
|
|
80
|
+
account_type TEXT,
|
|
81
|
+
free TEXT,
|
|
82
|
+
locked TEXT,
|
|
83
|
+
PRIMARY KEY (asset, account_type)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS {self.table_prefix}_params (
|
|
87
|
+
key TEXT PRIMARY KEY,
|
|
88
|
+
value BLOB,
|
|
89
|
+
timestamp INTEGER DEFAULT (strftime('%s', 'now') * 1000)
|
|
90
|
+
);
|
|
91
|
+
""")
|
|
92
|
+
await self._async_conn.commit()
|
|
93
|
+
|
|
94
|
+
def _get_cursor(self):
|
|
95
|
+
"""Get a thread-safe SQLite cursor, creating new connection if needed."""
|
|
96
|
+
try:
|
|
97
|
+
return self._sync_db.cursor()
|
|
98
|
+
except sqlite3.ProgrammingError:
|
|
99
|
+
# SQLite connection is from different thread, create new one
|
|
100
|
+
db_path = Path(self.db_path)
|
|
101
|
+
return sqlite3.connect(str(db_path)).cursor()
|
|
102
|
+
|
|
103
|
+
async def close(self) -> None:
|
|
104
|
+
if self._db_async:
|
|
105
|
+
await self._db_async.close()
|
|
106
|
+
if self._db:
|
|
107
|
+
self._db.close()
|
|
108
|
+
|
|
109
|
+
async def sync_orders(self, mem_orders: Dict[str, Order]) -> None:
|
|
110
|
+
async with self._async_conn.cursor() as cursor:
|
|
111
|
+
for order in mem_orders.copy().values():
|
|
112
|
+
await cursor.execute(
|
|
113
|
+
f"INSERT OR REPLACE INTO {self.table_prefix}_orders "
|
|
114
|
+
"(timestamp, oid, eid, symbol, side, type, amount, price, status, fee, fee_currency, data) "
|
|
115
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
116
|
+
(
|
|
117
|
+
order.timestamp,
|
|
118
|
+
order.oid,
|
|
119
|
+
order.eid,
|
|
120
|
+
order.symbol,
|
|
121
|
+
order.side.value if order.side is not None else None,
|
|
122
|
+
order.type.value if order.type is not None else None,
|
|
123
|
+
str(order.amount),
|
|
124
|
+
order.price or order.average,
|
|
125
|
+
order.status.value,
|
|
126
|
+
str(order.fee) if order.fee is not None else None,
|
|
127
|
+
order.fee_currency,
|
|
128
|
+
self._encode(order),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
await self._async_conn.commit()
|
|
132
|
+
|
|
133
|
+
async def sync_positions(self, mem_positions: Dict[str, Position]) -> None:
|
|
134
|
+
async with self._async_conn.cursor() as cursor:
|
|
135
|
+
await cursor.execute(f"SELECT symbol FROM {self.table_prefix}_positions")
|
|
136
|
+
db_positions = {row[0] for row in await cursor.fetchall()}
|
|
137
|
+
|
|
138
|
+
positions_to_delete = db_positions - set(mem_positions.keys())
|
|
139
|
+
if positions_to_delete:
|
|
140
|
+
await cursor.executemany(
|
|
141
|
+
f"DELETE FROM {self.table_prefix}_positions WHERE symbol = ?",
|
|
142
|
+
[(symbol,) for symbol in positions_to_delete],
|
|
143
|
+
)
|
|
144
|
+
self._log.debug(
|
|
145
|
+
f"Deleted {len(positions_to_delete)} stale positions from database"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
for symbol, position in mem_positions.copy().items():
|
|
149
|
+
await cursor.execute(
|
|
150
|
+
f"INSERT OR REPLACE INTO {self.table_prefix}_positions "
|
|
151
|
+
"(symbol, exchange, side, amount, data) VALUES (?, ?, ?, ?, ?)",
|
|
152
|
+
(
|
|
153
|
+
symbol,
|
|
154
|
+
position.exchange.value,
|
|
155
|
+
position.side.value if position.side else "FLAT",
|
|
156
|
+
str(position.amount),
|
|
157
|
+
self._encode(position),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
await self._async_conn.commit()
|
|
161
|
+
|
|
162
|
+
async def sync_open_orders(
|
|
163
|
+
self,
|
|
164
|
+
mem_open_orders: Dict[ExchangeType, Set[str]],
|
|
165
|
+
mem_orders: Dict[str, Order],
|
|
166
|
+
) -> None:
|
|
167
|
+
async with self._async_conn.cursor() as cursor:
|
|
168
|
+
await cursor.execute(f"DELETE FROM {self.table_prefix}_open_orders")
|
|
169
|
+
|
|
170
|
+
for exchange, oids in mem_open_orders.copy().items():
|
|
171
|
+
for oid in oids.copy():
|
|
172
|
+
order = mem_orders.get(oid)
|
|
173
|
+
if order:
|
|
174
|
+
await cursor.execute(
|
|
175
|
+
f"INSERT INTO {self.table_prefix}_open_orders "
|
|
176
|
+
"(oid, eid, exchange, symbol) VALUES (?, ?, ?, ?)",
|
|
177
|
+
(oid, order.eid, exchange.value, order.symbol),
|
|
178
|
+
)
|
|
179
|
+
await self._async_conn.commit()
|
|
180
|
+
|
|
181
|
+
async def sync_balances(
|
|
182
|
+
self, mem_account_balance: Dict[AccountType, AccountBalance]
|
|
183
|
+
) -> None:
|
|
184
|
+
async with self._async_conn.cursor() as cursor:
|
|
185
|
+
for account_type, balance in mem_account_balance.copy().items():
|
|
186
|
+
for asset, amount in balance.balances.items():
|
|
187
|
+
await cursor.execute(
|
|
188
|
+
f"INSERT OR REPLACE INTO {self.table_prefix}_balances "
|
|
189
|
+
"(asset, account_type, free, locked) VALUES (?, ?, ?, ?)",
|
|
190
|
+
(
|
|
191
|
+
asset,
|
|
192
|
+
account_type.value,
|
|
193
|
+
str(amount.free),
|
|
194
|
+
str(amount.locked),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
await self._async_conn.commit()
|
|
198
|
+
|
|
199
|
+
def get_order(
|
|
200
|
+
self,
|
|
201
|
+
oid: str,
|
|
202
|
+
mem_orders: Dict[str, Order],
|
|
203
|
+
) -> Optional[Order]:
|
|
204
|
+
if order := mem_orders.get(oid):
|
|
205
|
+
return order
|
|
206
|
+
|
|
207
|
+
cursor = self._get_cursor()
|
|
208
|
+
try:
|
|
209
|
+
cursor.execute(
|
|
210
|
+
f"SELECT data FROM {self.table_prefix}_orders WHERE oid = ?",
|
|
211
|
+
(oid,),
|
|
212
|
+
)
|
|
213
|
+
if row := cursor.fetchone():
|
|
214
|
+
order = self._decode(row[0], Order)
|
|
215
|
+
mem_orders[oid] = order
|
|
216
|
+
return order
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
except sqlite3.Error as e:
|
|
220
|
+
self._log.error(f"Error getting order from SQLite: {e}")
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def get_symbol_orders(self, symbol: str) -> Set[str]:
|
|
224
|
+
cursor = self._get_cursor()
|
|
225
|
+
cursor.execute(
|
|
226
|
+
f"SELECT oid FROM {self.table_prefix}_orders WHERE symbol = ?",
|
|
227
|
+
(symbol,),
|
|
228
|
+
)
|
|
229
|
+
return {row[0] for row in cursor.fetchall()}
|
|
230
|
+
|
|
231
|
+
def get_all_positions(self, exchange_id: ExchangeType) -> Dict[str, Position]:
|
|
232
|
+
positions = {}
|
|
233
|
+
cursor = self._get_cursor()
|
|
234
|
+
cursor.execute(
|
|
235
|
+
f"SELECT symbol, data FROM {self.table_prefix}_positions WHERE exchange = ?",
|
|
236
|
+
(exchange_id.value,),
|
|
237
|
+
)
|
|
238
|
+
for row in cursor.fetchall():
|
|
239
|
+
position = self._decode(row[1], Position)
|
|
240
|
+
if position.side:
|
|
241
|
+
positions[position.symbol] = position
|
|
242
|
+
return positions
|
|
243
|
+
|
|
244
|
+
def get_all_balances(self, account_type: AccountType) -> List[Balance]:
|
|
245
|
+
from decimal import Decimal
|
|
246
|
+
|
|
247
|
+
balances = []
|
|
248
|
+
cursor = self._get_cursor()
|
|
249
|
+
cursor.execute(
|
|
250
|
+
f"SELECT asset, free, locked FROM {self.table_prefix}_balances WHERE account_type = ?",
|
|
251
|
+
(account_type.value,),
|
|
252
|
+
)
|
|
253
|
+
for row in cursor.fetchall():
|
|
254
|
+
balances.append(
|
|
255
|
+
Balance(asset=row[0], free=Decimal(row[1]), locked=Decimal(row[2]))
|
|
256
|
+
)
|
|
257
|
+
return balances
|
|
258
|
+
|
|
259
|
+
async def sync_params(self, mem_params: Dict[str, Any]) -> None:
|
|
260
|
+
import msgspec
|
|
261
|
+
|
|
262
|
+
async with self._async_conn.cursor() as cursor:
|
|
263
|
+
for key, value in mem_params.copy().items():
|
|
264
|
+
try:
|
|
265
|
+
serialized_value = self._encode_param(value)
|
|
266
|
+
await cursor.execute(
|
|
267
|
+
f"INSERT OR REPLACE INTO {self.table_prefix}_params "
|
|
268
|
+
"(key, value) VALUES (?, ?)",
|
|
269
|
+
(key, serialized_value),
|
|
270
|
+
)
|
|
271
|
+
except msgspec.EncodeError as e:
|
|
272
|
+
self._log.error(f"Error serializing parameter {key}: {e}")
|
|
273
|
+
except sqlite3.Error as e:
|
|
274
|
+
self._log.error(f"Error storing parameter {key}: {e}")
|
|
275
|
+
await self._async_conn.commit()
|
|
276
|
+
|
|
277
|
+
def get_param(self, key: str, default: Any = None) -> Any:
|
|
278
|
+
import msgspec
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
cursor = self._get_cursor()
|
|
282
|
+
cursor.execute(
|
|
283
|
+
f"SELECT value FROM {self.table_prefix}_params WHERE key = ?", (key,)
|
|
284
|
+
)
|
|
285
|
+
if row := cursor.fetchone():
|
|
286
|
+
try:
|
|
287
|
+
return self._decode_param(row[0])
|
|
288
|
+
except msgspec.DecodeError as e:
|
|
289
|
+
self._log.error(f"Error deserializing parameter {key}: {e}")
|
|
290
|
+
return default
|
|
291
|
+
return default
|
|
292
|
+
except sqlite3.Error as e:
|
|
293
|
+
self._log.error(f"Error getting parameter {key}: {e}")
|
|
294
|
+
return default
|
|
295
|
+
|
|
296
|
+
def get_all_params(self) -> Dict[str, Any]:
|
|
297
|
+
import msgspec
|
|
298
|
+
|
|
299
|
+
params = {}
|
|
300
|
+
try:
|
|
301
|
+
cursor = self._get_cursor()
|
|
302
|
+
cursor.execute(f"SELECT key, value FROM {self.table_prefix}_params")
|
|
303
|
+
for row in cursor.fetchall():
|
|
304
|
+
try:
|
|
305
|
+
params[row[0]] = self._decode_param(row[1])
|
|
306
|
+
except msgspec.DecodeError as e:
|
|
307
|
+
self._log.error(f"Error deserializing parameter {row[0]}: {e}")
|
|
308
|
+
except sqlite3.Error as e:
|
|
309
|
+
self._log.error(f"Error getting all parameters: {e}")
|
|
310
|
+
return params
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from walrasquant.base.exchange import ExchangeManager
|
|
2
|
+
from walrasquant.base.ws_client import WSClient
|
|
3
|
+
from walrasquant.base.api_client import ApiClient
|
|
4
|
+
from walrasquant.base.oms import OrderManagementSystem
|
|
5
|
+
from walrasquant.base.ems import ExecutionManagementSystem
|
|
6
|
+
from walrasquant.base.sms import SubscriptionManagementSystem
|
|
7
|
+
from walrasquant.base.connector import (
|
|
8
|
+
PublicConnector,
|
|
9
|
+
PrivateConnector,
|
|
10
|
+
)
|
|
11
|
+
from walrasquant.base.retry import RetryManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ExchangeManager",
|
|
16
|
+
"WSClient",
|
|
17
|
+
"ApiClient",
|
|
18
|
+
"OrderManagementSystem",
|
|
19
|
+
"ExecutionManagementSystem",
|
|
20
|
+
"PublicConnector",
|
|
21
|
+
"SubscriptionManagementSystem",
|
|
22
|
+
"PrivateConnector",
|
|
23
|
+
"RetryManager",
|
|
24
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
import aiohttp
|
|
3
|
+
import nexuslog as logging
|
|
4
|
+
from typing import Optional, Any, Dict
|
|
5
|
+
from walrasquant.core.nautilius_core import LiveClock
|
|
6
|
+
from walrasquant.constants import RateLimiter
|
|
7
|
+
from walrasquant.base.retry import RetryManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiClient(ABC):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
clock: LiveClock,
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
secret: Optional[str] = None,
|
|
16
|
+
timeout: int = 10,
|
|
17
|
+
rate_limiter: Optional[RateLimiter] = None,
|
|
18
|
+
retry_manager: Optional[RetryManager] = None,
|
|
19
|
+
):
|
|
20
|
+
self._api_key = api_key
|
|
21
|
+
self._secret = secret
|
|
22
|
+
self._timeout = timeout
|
|
23
|
+
self._log = logging.getLogger(name=type(self).__name__)
|
|
24
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
25
|
+
self._clock = clock
|
|
26
|
+
self._limiter = rate_limiter
|
|
27
|
+
self._retry_manager: Optional[RetryManager] = retry_manager
|
|
28
|
+
self._base_url: str | None = None
|
|
29
|
+
|
|
30
|
+
async def _init_session(self, base_url: str | None = None):
|
|
31
|
+
if self._session is None:
|
|
32
|
+
kwargs: Dict[str, Any] = {
|
|
33
|
+
"timeout": aiohttp.ClientTimeout(total=self._timeout),
|
|
34
|
+
}
|
|
35
|
+
if base_url:
|
|
36
|
+
kwargs["base_url"] = base_url
|
|
37
|
+
self._session = aiohttp.ClientSession(**kwargs)
|
|
38
|
+
|
|
39
|
+
def _get_rate_limit_cost(self, cost: int = 1):
|
|
40
|
+
return cost
|
|
41
|
+
|
|
42
|
+
async def close_session(self):
|
|
43
|
+
"""Close the session"""
|
|
44
|
+
if self._session:
|
|
45
|
+
await self._session.close()
|
|
46
|
+
self._session = None
|