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.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. 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