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

@@ -46,6 +46,9 @@ class Order:
46
46
  self.reduce_only = reduce_only
47
47
  self.fees_ccy = fees_ccy
48
48
 
49
+ 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.)
50
+ self.dispatched_price : Union[None, float] = None
51
+
49
52
  def to_dict(self) -> Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES]:
50
53
  return {
51
54
  "ticker" : self.ticker,
@@ -54,7 +57,9 @@ class Order:
54
57
  "order_type" : self.order_type,
55
58
  "leg_room_bps" : self.leg_room_bps,
56
59
  "reduce_only" : self.reduce_only,
57
- "fees_ccy" : self.fees_ccy
60
+ "fees_ccy" : self.fees_ccy,
61
+ "dispatched_amount" : self.dispatched_amount,
62
+ "dispatched_price" : self.dispatched_price
58
63
  }
59
64
 
60
65
  '''
@@ -86,6 +91,7 @@ class DivisiblePosition(Order):
86
91
  self.fees : Union[float, None] = None
87
92
  self.pos : Union[float, None] = None # in base ccy, after execution. (Not in USDT or quote ccy, Not in # contracts)
88
93
 
94
+ self.dispatched_slices : List[Order] = []
89
95
  self.executions : Dict[str, Dict[str, Any]] = {}
90
96
 
91
97
  def to_slices(self) -> List[Order]:
@@ -136,60 +142,172 @@ class DivisiblePosition(Order):
136
142
  def get_executions(self) -> Dict[str, Dict[str, Any]]:
137
143
  return self.executions
138
144
 
139
- def get_filled_amount(self) -> float:
140
- # filled_amount is in base ccy
141
- filled_amount = sum([ self.executions[order_id]['filled'] if 'filled' in self.executions[order_id] and self.executions[order_id]['filled'] else 0 * 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:
145
+ '''
146
+ The purpose of patch_executions is to fix erroneous exchange response where filled, amount or average None when status already 'closed'.
149
147
 
148
+ Example hyperliquid don't have 'average' in response JSON:
150
149
  {
151
150
  'info': {
152
151
  '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'
152
+ 'coin': 'SUSHI',
153
+ 'side': 'B',
154
+ 'limitPx': '0.79252',
155
+ 'sz': '0.0',
156
+ 'oid': xxx,
157
+ 'timestamp': xxx,
158
+ 'origSz': '30.0'
159
+ },
160
+ 'status': 'filled',
161
+ 'statusTimestamp': xxx},
162
+ 'id': 'xxx',
163
+ 'clientOrderId': None,
164
+ 'timestamp': xxx,
165
+ 'datetime': 'xxx',
166
+ 'lastTradeTimestamp': None,
167
+ 'lastUpdateTimestamp': xxx,
168
+ 'symbol': 'SUSHI/USDC:USDC',
169
+ 'type': None,
170
+ 'timeInForce': None,
171
+ 'postOnly': None,
172
+ 'reduceOnly': None,
173
+ 'side': 'buy',
174
+ 'price': 0.79252,
175
+ 'triggerPrice': None,
176
+ 'amount': 30.0,
177
+ 'cost': 23.7756,
178
+ 'average': None, <-- No average cost?
179
+ 'filled': 30.0,
180
+ 'remaining': 0.0,
181
+ 'status': 'closed',
182
+ 'fee': None,
183
+ 'trades': [],
184
+ 'fees': [],
185
+ 'stopPrice': None,
186
+ 'takeProfitPrice': None,
187
+ 'stopLossPrice': None
188
+ }
189
+
190
+ A second example, sometimes even 'amount', 'filled' and 'average' both None, but status is 'filled':
191
+ [
192
+ {
193
+ 'info': {
194
+ 'order': {
195
+ 'coin': 'BTC',
196
+ 'side': 'A',
197
+ 'limitPx': '84389.0',
198
+ 'sz': '0.0',
199
+ 'oid': xxx,
200
+ 'timestamp': xxx,
201
+ 'origSz': '0.01184'
202
+ },
203
+ 'status': 'filled',
204
+ 'statusTimestamp': xxx
160
205
  },
161
- 'status': 'filled',
162
- 'statusTimestamp': xxx},
163
- 'id': 'xxx',
206
+ 'id': 'xxx',
164
207
  'clientOrderId': None,
165
208
  'timestamp': xxx,
166
- 'datetime': '2025-02-26T01:00:00.522Z',
209
+ 'datetime': 'xxx',
167
210
  'lastTradeTimestamp': None,
168
- 'lastUpdateTimestamp': xxx,
169
- 'symbol': 'SUSHI/USDC:USDC',
211
+ 'lastUpdateTimestamp': None,
212
+ 'symbol': 'BTC/USDC:USDC',
170
213
  'type': None,
171
- 'timeInForce': None,
214
+ 'timeInForce': None,
172
215
  'postOnly': None,
173
216
  'reduceOnly': None,
174
- 'side': 'buy',
175
- 'price': 0.79252,
217
+ 'side': 'sell',
218
+ 'price': 84389.0,
176
219
  'triggerPrice': None,
177
- 'amount': 30.0,
178
- 'cost': 23.7756,
179
- 'average': None, <-- No average cost?
180
- 'filled': 30.0,
220
+ 'amount': None, <-- No amount!?!
221
+ 'cost': None,
222
+ 'average': None, <-- No average!?!
223
+ 'filled': None, <-- No filled!?!
181
224
  'remaining': 0.0,
182
225
  'status': 'closed',
183
- 'fee': None,
184
- 'trades': [],
185
- 'fees': [],
186
- 'stopPrice': None,
187
- 'takeProfitPrice': None,
188
- 'stopLossPrice': None
189
- }
226
+ 'fee': None, 'trades': [], 'fees':
227
+ [], 'stopPrice': None, 'takeProfitPrice': None, 'stopLossPrice': None}
228
+ ]
190
229
  '''
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 'average' in self.executions[order_id] and self.executions[order_id]['average'] else 0 if self.executions[order_id]['average'] else self.executions[order_id]['price']) * self.executions[order_id]['amount'] for order_id in self.executions ])
230
+ def patch_executions(self):
231
+ i : int = 0
232
+ for order_id in self.executions:
233
+ execution = self.executions[order_id]
234
+ execution['patch'] = {}
235
+ execution['patch']['average'] = None
236
+ execution['patch']['filled'] = 0
237
+ execution['patch']['amount'] = 0
238
+
239
+ corresponding_slice = self.dispatched_slices[i]
240
+
241
+ status = execution['status']
242
+ if status and status.strip().lower()=='closed':
243
+ if 'average' in execution:
244
+ if not execution['average']:
245
+ if 'price' in execution and execution['price']:
246
+ execution['patch']['average'] = execution['price']
247
+ else:
248
+ execution['patch']['average'] = corresponding_slice.dispatched_price
249
+ else:
250
+ execution['patch']['average'] = execution['average']
251
+
252
+ if 'filled' in execution:
253
+ if not execution['filled']:
254
+ execution['patch']['filled'] = corresponding_slice.dispatched_amount
255
+ else:
256
+ execution['patch']['filled'] = execution['filled']
257
+
258
+ if 'amount' in execution:
259
+ if not execution['amount']:
260
+ execution['patch']['amount'] = corresponding_slice.dispatched_amount
261
+ else:
262
+ execution['patch']['amount'] = execution['amount']
263
+
264
+ i += 1
265
+
266
+ def get_filled_amount(self) -> float:
267
+ # filled_amount is in base ccy
268
+ filled_amount = sum(
269
+ [
270
+ (
271
+ self.executions[order_id]['filled']
272
+ if 'filled' in self.executions[order_id] and self.executions[order_id]['filled']
273
+ else self.executions[order_id]['patch']['dispatched_amount']
274
+ ) * self.multiplier
275
+ for order_id in self.executions
276
+ ]
277
+ )
278
+ if self.side=='sell':
279
+ filled_amount = -1 * filled_amount
280
+ return filled_amount
281
+
282
+ def get_average_cost(self) -> float:
283
+ total_amount : float = sum(
284
+ [
285
+ self.executions[order_id]['amount']
286
+ if 'amount' in self.executions[order_id] and self.executions[order_id]['amount']
287
+ else self.executions[order_id]['patch']['dispatched_amount']
288
+ for order_id in self.executions
289
+ ]
290
+ )
291
+
292
+ average_cost = sum(
293
+ [
294
+ (
295
+ self.executions[order_id]['average']
296
+ if 'average' in self.executions[order_id] and self.executions[order_id]['average']
297
+ else (
298
+ self.executions[order_id]['price']
299
+ if self.executions[order_id]['price']
300
+ else self.executions[order_id]['patch']['dispatched_price']
301
+ )
302
+ ) *
303
+ (
304
+ self.executions[order_id]['amount']
305
+ if 'amount' in self.executions[order_id] and self.executions[order_id]['amount']
306
+ else self.executions[order_id]['patch']['dispatched_amount']
307
+ )
308
+ for order_id in self.executions
309
+ ]
310
+ )
193
311
  average_cost = average_cost / total_amount if total_amount!=0 else 0
194
312
  return average_cost
195
313
 
@@ -461,7 +461,7 @@ async def execute_one_position(
461
461
  dt_now : datetime = datetime.now()
462
462
 
463
463
  slice_amount_in_base_ccy : float = slice.amount
464
- rounded_slice_amount_in_base_ccy = slice_amount_in_base_ccy / multiplier # After devided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
464
+ rounded_slice_amount_in_base_ccy = slice_amount_in_base_ccy / multiplier # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
465
465
  rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, rounded_slice_amount_in_base_ccy) # type: ignore
466
466
  rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy) if rounded_slice_amount_in_base_ccy else 0
467
467
  rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
@@ -660,6 +660,10 @@ async def execute_one_position(
660
660
 
661
661
  log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
662
662
 
663
+ slice.dispatched_price = rounded_limit_price
664
+ slice.dispatched_amount = rounded_slice_amount_in_base_ccy
665
+ position.dispatched_slices.append(slice)
666
+
663
667
  log(f"Executed slice #{i}", log_level=LogLevel.INFO)
664
668
  log(f"{position.ticker}, multiplier: {multiplier}, slice_amount_in_base_ccy: {slice_amount_in_base_ccy}, rounded_slice_amount_in_base_ccy, {rounded_slice_amount_in_base_ccy}", log_level=LogLevel.INFO)
665
669
  if position.order_type=='limit':
@@ -673,6 +677,7 @@ async def execute_one_position(
673
677
  finally:
674
678
  i += 1
675
679
 
680
+ position.patch_executions()
676
681
  position.filled_amount = position.get_filled_amount()
677
682
  position.average_cost = position.get_average_cost()
678
683
  position.fees = position.get_fees()
@@ -18,9 +18,10 @@ import yfinance as yf
18
18
  from siglab_py.exchanges.futubull import Futubull
19
19
 
20
20
  def timestamp_to_datetime_cols(pd_candles : pd.DataFrame):
21
- pd_candles['datetime'] = pd_candles['timestamp_ms'].apply(
22
- lambda x: datetime.fromtimestamp(int(x.timestamp()) if isinstance(x, pd.Timestamp) else (int(x / 1000) if len(str(int(x)))==13 else int(x)) )
21
+ pd_candles['timestamp_ms'] = pd_candles['timestamp_ms'].apply(
22
+ lambda x: int(x.timestamp()) if isinstance(x, pd.Timestamp) else (int(x / 1000) if len(str(int(x)))==13 else int(x))
23
23
  )
24
+ pd_candles['datetime'] = pd_candles['timestamp_ms'].apply(lambda x: datetime.fromtimestamp(int(x)))
24
25
  pd_candles['datetime'] = pd.to_datetime(pd_candles['datetime'])
25
26
  pd_candles['datetime'] = pd_candles['datetime'].dt.tz_localize(None)
26
27
  pd_candles['datetime_utc'] = pd_candles['timestamp_ms'].apply(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab-py
3
- Version: 0.1.35
3
+ Version: 0.1.37
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -13,9 +13,9 @@ siglab_py/market_data_providers/futu_candles_ta_to_csv.py,sha256=S4GXaJ7AveEh-Cm
13
13
  siglab_py/market_data_providers/orderbooks_provider.py,sha256=olt-3LIkoyzQWfNNQRhJtKibLbkTutt_q_rCCTM7i1g,16216
14
14
  siglab_py/market_data_providers/test_provider.py,sha256=wBLCgcWjs7FGZJXWsNyn30lkOLa_cgpuvqRakMC0wbA,2221
15
15
  siglab_py/ordergateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- siglab_py/ordergateway/client.py,sha256=XgY4l8ue2f1hrFAla6VwzpFzsOmIW-6SkvulOGSVAGQ,9841
16
+ siglab_py/ordergateway/client.py,sha256=gtUyR41xXuAcBNoVCSlcg99koiQphe8gUBxlEgvWYt8,14637
17
17
  siglab_py/ordergateway/encrypt_keys_util.py,sha256=-qi87db8To8Yf1WS1Q_Cp2Ya7ZqgWlRqSHfNXCM7wE4,1339
18
- siglab_py/ordergateway/gateway.py,sha256=FoE-OUBrHJ2fdKAfvGue1yQ2P67falWoyoEGCV8ScfA,37219
18
+ siglab_py/ordergateway/gateway.py,sha256=VI-OIc3enhQ78cFNzAbLgX0Pzyjr2tl9VDV3IZTAtMQ,37438
19
19
  siglab_py/ordergateway/test_ordergateway.py,sha256=_Gz2U_VqljogGWqGyNDYYls1INqUiig9veyPttfGRpg,3901
20
20
  siglab_py/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  siglab_py/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -26,9 +26,9 @@ siglab_py/tests/unit/market_data_util_tests.py,sha256=A1y83itISmMJdn6wLpfwcr4tGo
26
26
  siglab_py/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  siglab_py/util/analytic_util.py,sha256=xo9gD1ELQt_1v84yu9d4NgxtOIXthePZGipvDrjZAQ8,43834
28
28
  siglab_py/util/aws_util.py,sha256=KGmjHrr1rpnnxr33nXHNzTul4tvyyxl9p6gpwNv0Ygc,2557
29
- siglab_py/util/market_data_util.py,sha256=mXEcWO1FirySHNBYFe-PFnBJp4FR9PlruQyaSCHN0MI,19358
29
+ siglab_py/util/market_data_util.py,sha256=E8jCOU67tyZu6GKXCnGRJjq777n3co3-xCZkMDh5uHs,19443
30
30
  siglab_py/util/retry_util.py,sha256=mxYuRFZRZoaQQjENcwPmxhxixtd1TFvbxIdPx4RwfRc,743
31
- siglab_py-0.1.35.dist-info/METADATA,sha256=RGBNsgJFUo8x1aD3KbqAuDqRxOgMFKO99frX-8gEDwI,980
32
- siglab_py-0.1.35.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
33
- siglab_py-0.1.35.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
34
- siglab_py-0.1.35.dist-info/RECORD,,
31
+ siglab_py-0.1.37.dist-info/METADATA,sha256=FjYav-AWsN8fa7Q3HLLTbkqt0MTAGFzngS9lsQsesZ4,980
32
+ siglab_py-0.1.37.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
33
+ siglab_py-0.1.37.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
34
+ siglab_py-0.1.37.dist-info/RECORD,,