siglab-py 0.1.29__py3-none-any.whl → 0.6.12__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.

Potentially problematic release.


This version of siglab-py might be problematic. Click here for more details.

Files changed (32) hide show
  1. siglab_py/constants.py +26 -1
  2. siglab_py/exchanges/binance.py +38 -0
  3. siglab_py/exchanges/deribit.py +83 -0
  4. siglab_py/exchanges/futubull.py +12 -2
  5. siglab_py/market_data_providers/candles_provider.py +2 -2
  6. siglab_py/market_data_providers/candles_ta_provider.py +3 -3
  7. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
  8. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  9. siglab_py/market_data_providers/google_monitor.py +320 -0
  10. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  11. siglab_py/market_data_providers/tg_monitor.py +428 -0
  12. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  13. siglab_py/ordergateway/client.py +172 -41
  14. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  15. siglab_py/ordergateway/gateway.py +456 -347
  16. siglab_py/ordergateway/test_ordergateway.py +8 -7
  17. siglab_py/tests/integration/market_data_util_tests.py +35 -1
  18. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  19. siglab_py/tests/unit/simple_math_tests.py +235 -0
  20. siglab_py/tests/unit/trading_util_tests.py +65 -0
  21. siglab_py/util/analytic_util.py +478 -69
  22. siglab_py/util/market_data_util.py +487 -100
  23. siglab_py/util/notification_util.py +78 -0
  24. siglab_py/util/retry_util.py +11 -3
  25. siglab_py/util/simple_math.py +240 -0
  26. siglab_py/util/slack_notification_util.py +59 -0
  27. siglab_py/util/trading_util.py +118 -0
  28. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/METADATA +5 -9
  29. siglab_py-0.6.12.dist-info/RECORD +44 -0
  30. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/WHEEL +1 -1
  31. siglab_py-0.1.29.dist-info/RECORD +0 -34
  32. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
 
2
2
  from typing import List, Dict, Any, Union
3
3
  import json
4
+ from datetime import datetime
4
5
  import time
5
6
  from redis import StrictRedis
6
7
 
@@ -46,6 +47,11 @@ class Order:
46
47
  self.reduce_only = reduce_only
47
48
  self.fees_ccy = fees_ccy
48
49
 
50
+ self.dispatched_amount : float = 0 # This is amount in base ccy, with rounding + multiplier applied (So for contracts with multiplier!=1, this is no longer amount in base ccy.)
51
+ self.dispatched_price : Union[None, float] = None
52
+
53
+ self.timestamp_ms = int(datetime.now().timestamp() * 1000)
54
+
49
55
  def to_dict(self) -> Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES]:
50
56
  return {
51
57
  "ticker" : self.ticker,
@@ -54,7 +60,10 @@ class Order:
54
60
  "order_type" : self.order_type,
55
61
  "leg_room_bps" : self.leg_room_bps,
56
62
  "reduce_only" : self.reduce_only,
57
- "fees_ccy" : self.fees_ccy
63
+ "fees_ccy" : self.fees_ccy,
64
+ "dispatched_amount" : self.dispatched_amount,
65
+ "dispatched_price" : self.dispatched_price,
66
+ "timestamp_ms" : self.timestamp_ms
58
67
  }
59
68
 
60
69
  '''
@@ -86,8 +95,14 @@ class DivisiblePosition(Order):
86
95
  self.fees : Union[float, None] = None
87
96
  self.pos : Union[float, None] = None # in base ccy, after execution. (Not in USDT or quote ccy, Not in # contracts)
88
97
 
98
+ self.done : bool = False
99
+ self.execution_err : str = ""
100
+
101
+ self.dispatched_slices : List[Order] = []
89
102
  self.executions : Dict[str, Dict[str, Any]] = {}
90
103
 
104
+ self.timestamp_ms = int(datetime.now().timestamp() * 1000)
105
+
91
106
  def to_slices(self) -> List[Order]:
92
107
  slices : List[Order] = []
93
108
 
@@ -136,67 +151,179 @@ class DivisiblePosition(Order):
136
151
  def get_executions(self) -> Dict[str, Dict[str, Any]]:
137
152
  return self.executions
138
153
 
139
- def get_filled_amount(self) -> float:
140
- # filled_amount is in base ccy
141
- filled_amount = sum([ self.executions[order_id]['filled'] * self.multiplier for order_id in self.executions ])
142
- if self.side=='sell':
143
- filled_amount = -1 * filled_amount
144
- return filled_amount
145
-
146
- def get_average_cost(self) -> float:
147
- '''
148
- Example hyperliquid don't have 'average' in response JSON:
154
+ '''
155
+ The purpose of patch_executions is to fix erroneous exchange response where filled, amount or average None when status already 'closed'.
149
156
 
157
+ Example hyperliquid don't have 'average' in response JSON:
150
158
  {
151
159
  'info': {
152
160
  'order': {
153
- 'coin': 'SUSHI',
154
- 'side': 'B',
155
- 'limitPx': '0.79252',
156
- 'sz': '0.0',
157
- 'oid': xxx,
158
- 'timestamp': xxx,
159
- 'origSz': '30.0'
161
+ 'coin': 'SUSHI',
162
+ 'side': 'B',
163
+ 'limitPx': '0.79252',
164
+ 'sz': '0.0',
165
+ 'oid': xxx,
166
+ 'timestamp': xxx,
167
+ 'origSz': '30.0'
168
+ },
169
+ 'status': 'filled',
170
+ 'statusTimestamp': xxx},
171
+ 'id': 'xxx',
172
+ 'clientOrderId': None,
173
+ 'timestamp': xxx,
174
+ 'datetime': 'xxx',
175
+ 'lastTradeTimestamp': None,
176
+ 'lastUpdateTimestamp': xxx,
177
+ 'symbol': 'SUSHI/USDC:USDC',
178
+ 'type': None,
179
+ 'timeInForce': None,
180
+ 'postOnly': None,
181
+ 'reduceOnly': None,
182
+ 'side': 'buy',
183
+ 'price': 0.79252,
184
+ 'triggerPrice': None,
185
+ 'amount': 30.0,
186
+ 'cost': 23.7756,
187
+ 'average': None, <-- No average cost?
188
+ 'filled': 30.0,
189
+ 'remaining': 0.0,
190
+ 'status': 'closed',
191
+ 'fee': None,
192
+ 'trades': [],
193
+ 'fees': [],
194
+ 'stopPrice': None,
195
+ 'takeProfitPrice': None,
196
+ 'stopLossPrice': None
197
+ }
198
+
199
+ A second example, sometimes even 'amount', 'filled' and 'average' both None, but status is 'filled':
200
+ [
201
+ {
202
+ 'info': {
203
+ 'order': {
204
+ 'coin': 'BTC',
205
+ 'side': 'A',
206
+ 'limitPx': '84389.0',
207
+ 'sz': '0.0',
208
+ 'oid': xxx,
209
+ 'timestamp': xxx,
210
+ 'origSz': '0.01184'
211
+ },
212
+ 'status': 'filled',
213
+ 'statusTimestamp': xxx
160
214
  },
161
- 'status': 'filled',
162
- 'statusTimestamp': xxx},
163
- 'id': 'xxx',
215
+ 'id': 'xxx',
164
216
  'clientOrderId': None,
165
217
  'timestamp': xxx,
166
- 'datetime': '2025-02-26T01:00:00.522Z',
218
+ 'datetime': 'xxx',
167
219
  'lastTradeTimestamp': None,
168
- 'lastUpdateTimestamp': xxx,
169
- 'symbol': 'SUSHI/USDC:USDC',
220
+ 'lastUpdateTimestamp': None,
221
+ 'symbol': 'BTC/USDC:USDC',
170
222
  'type': None,
171
- 'timeInForce': None,
223
+ 'timeInForce': None,
172
224
  'postOnly': None,
173
225
  'reduceOnly': None,
174
- 'side': 'buy',
175
- 'price': 0.79252,
226
+ 'side': 'sell',
227
+ 'price': 84389.0,
176
228
  'triggerPrice': None,
177
- 'amount': 30.0,
178
- 'cost': 23.7756,
179
- 'average': None, <-- No average cost?
180
- 'filled': 30.0,
229
+ 'amount': None, <-- No amount!?!
230
+ 'cost': None,
231
+ 'average': None, <-- No average!?!
232
+ 'filled': None, <-- No filled!?!
181
233
  'remaining': 0.0,
182
234
  'status': 'closed',
183
- 'fee': None,
184
- 'trades': [],
185
- 'fees': [],
186
- 'stopPrice': None,
187
- 'takeProfitPrice': None,
188
- 'stopLossPrice': None
189
- }
235
+ 'fee': None, 'trades': [], 'fees':
236
+ [], 'stopPrice': None, 'takeProfitPrice': None, 'stopLossPrice': None}
237
+ ]
190
238
  '''
191
- total_amount : float = sum([ self.executions[order_id]['amount'] for order_id in self.executions ])
192
- average_cost = sum([ (self.executions[order_id]['average'] if self.executions[order_id]['average'] else self.executions[order_id]['price']) * self.executions[order_id]['amount'] for order_id in self.executions ])
239
+ def patch_executions(self):
240
+ i : int = 0
241
+ for order_id in self.executions:
242
+ execution = self.executions[order_id]
243
+ execution['patch'] = {}
244
+ execution['patch']['average'] = None
245
+ execution['patch']['filled'] = 0
246
+ execution['patch']['amount'] = 0
247
+
248
+ corresponding_slice = self.dispatched_slices[i]
249
+
250
+ status = execution['status']
251
+ if status and status.strip().lower()=='closed':
252
+ if 'average' in execution:
253
+ if not execution['average']:
254
+ if 'price' in execution and execution['price']:
255
+ execution['patch']['average'] = execution['price']
256
+ else:
257
+ execution['patch']['average'] = corresponding_slice.dispatched_price
258
+ else:
259
+ execution['patch']['average'] = execution['average']
260
+
261
+ if 'filled' in execution:
262
+ if not execution['filled']:
263
+ execution['patch']['filled'] = corresponding_slice.dispatched_amount
264
+ else:
265
+ execution['patch']['filled'] = execution['filled']
266
+
267
+ if 'amount' in execution:
268
+ if not execution['amount']:
269
+ execution['patch']['amount'] = corresponding_slice.dispatched_amount
270
+ else:
271
+ execution['patch']['amount'] = execution['amount']
272
+
273
+ i += 1
274
+
275
+ def get_filled_amount(self) -> float:
276
+ # filled_amount is in base ccy
277
+ filled_amount = sum(
278
+ [
279
+ (
280
+ self.executions[order_id]['filled']
281
+ if 'filled' in self.executions[order_id] and self.executions[order_id]['filled']
282
+ else self.executions[order_id]['patch']['dispatched_amount']
283
+ ) * self.multiplier
284
+ for order_id in self.executions
285
+ ]
286
+ )
287
+ if self.side=='sell':
288
+ filled_amount = -1 * filled_amount
289
+ return filled_amount
290
+
291
+ def get_average_cost(self) -> float:
292
+ total_amount : float = sum(
293
+ [
294
+ self.executions[order_id]['amount']
295
+ if 'amount' in self.executions[order_id] and self.executions[order_id]['amount']
296
+ else self.executions[order_id]['patch']['dispatched_amount']
297
+ for order_id in self.executions
298
+ ]
299
+ )
300
+
301
+ average_cost = sum(
302
+ [
303
+ (
304
+ self.executions[order_id]['average']
305
+ if 'average' in self.executions[order_id] and self.executions[order_id]['average']
306
+ else (
307
+ self.executions[order_id]['price']
308
+ if self.executions[order_id]['price']
309
+ else self.executions[order_id]['patch']['dispatched_price']
310
+ )
311
+ ) *
312
+ (
313
+ self.executions[order_id]['amount']
314
+ if 'amount' in self.executions[order_id] and self.executions[order_id]['amount']
315
+ else self.executions[order_id]['patch']['dispatched_amount']
316
+ )
317
+ for order_id in self.executions
318
+ ]
319
+ )
193
320
  average_cost = average_cost / total_amount if total_amount!=0 else 0
194
321
  return average_cost
195
322
 
196
323
  def get_fees(self) -> float:
197
324
  fees : float = 0
198
325
  if self.fees_ccy:
199
- fees = sum([ float(self.executions[order_id]['fee']['cost']) for order_id in self.executions if self.executions[order_id]['fee'] and self.executions[order_id]['fee']['currency'].strip().upper()==self.fees_ccy.strip().upper() ])
326
+ fees = sum([ float(self.executions[order_id]['fee']['cost'] if self.executions[order_id]['fee'] and self.executions[order_id]['fee']['cost'] else 0) for order_id in self.executions if self.executions[order_id]['fee'] and self.executions[order_id]['fee']['currency'].strip().upper()==self.fees_ccy.strip().upper() ])
200
327
  return fees
201
328
 
202
329
  def to_dict(self) -> Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES]:
@@ -206,7 +333,11 @@ class DivisiblePosition(Order):
206
333
  rv['executions'] = self.executions
207
334
  rv['filled_amount'] = self.filled_amount
208
335
  rv['average_cost'] = self.average_cost
336
+ rv['fees'] = self.fees
209
337
  rv['pos'] = self.pos
338
+ rv['done'] = self.done
339
+ rv['execution_err'] = self.execution_err
340
+ rv['timestamp_ms'] = self.timestamp_ms
210
341
  return rv
211
342
 
212
343
  def execute_positions(
@@ -1,5 +1,5 @@
1
1
  from typing import Union
2
- from util.aws_util import AwsKmsUtil
2
+ from siglab_py.util.aws_util import AwsKmsUtil
3
3
 
4
4
  '''
5
5
  From command line, run 'aws configure' with IAM user's Access key ID and Secret access key. (Assume you have awscli installed)