tradingapi 0.1.7__tar.gz → 0.2.0__tar.gz

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 (28) hide show
  1. {tradingapi-0.1.7 → tradingapi-0.2.0}/PKG-INFO +1 -1
  2. {tradingapi-0.1.7 → tradingapi-0.2.0}/pyproject.toml +1 -1
  3. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/__init__.py +17 -4
  4. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/broker_base.py +1 -2
  5. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/dhan.py +5 -5
  6. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/fivepaisa.py +114 -34
  7. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/flattrade.py +10 -1
  8. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/shoonya.py +21 -1
  9. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/utils.py +54 -0
  10. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/PKG-INFO +1 -1
  11. {tradingapi-0.1.7 → tradingapi-0.2.0}/README.md +0 -0
  12. {tradingapi-0.1.7 → tradingapi-0.2.0}/setup.cfg +0 -0
  13. {tradingapi-0.1.7 → tradingapi-0.2.0}/tests/test_shoonya_symbol_parser.py +0 -0
  14. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/attribution.py +0 -0
  15. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config/commissions_20241216.yaml +0 -0
  16. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config/config_sample.yaml +0 -0
  17. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config.py +0 -0
  18. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/error_handling.py +0 -0
  19. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/exceptions.py +0 -0
  20. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/globals.py +0 -0
  21. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/icicidirect.py +0 -0
  22. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/icicidirect_generate_session.py +0 -0
  23. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/proxy_utils.py +0 -0
  24. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/SOURCES.txt +0 -0
  25. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/dependency_links.txt +0 -0
  26. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/entry_points.txt +0 -0
  27. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/requires.txt +0 -0
  28. {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ packages = ["tradingapi"]
28
28
 
29
29
  [project]
30
30
  name = "tradingapi"
31
- version = "0.1.7"
31
+ version = "0.2.0"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -180,13 +180,26 @@ class TradingAPILogger:
180
180
  pass
181
181
  return {}
182
182
 
183
+ def _sanitize_extra(self, context: Optional[dict] = None) -> dict:
184
+ """Rename reserved LogRecord keys in extra context to avoid logging failures."""
185
+ extra = dict(context or {})
186
+ reserved_keys = set(logging.makeLogRecord({}).__dict__.keys()) | {
187
+ "message",
188
+ "asctime",
189
+ }
190
+ sanitized = {}
191
+ for key, value in extra.items():
192
+ new_key = f"context_{key}" if key in reserved_keys else key
193
+ sanitized[new_key] = value
194
+ return sanitized
195
+
183
196
  def log_error(
184
197
  self, message: str, error: Optional[Exception] = None, context: Optional[dict] = None, exc_info: bool = True
185
198
  ):
186
199
  """Log an error with structured context. Extra values are sanitized to single-line so
187
200
  parse_log_errors.py can reliably match the ERROR line (same format as other tradingapi logs).
188
201
  """
189
- extra = context or {}
202
+ extra = self._sanitize_extra(context)
190
203
  if error:
191
204
  extra["error_type"] = type(error).__name__
192
205
  # Keep error_message single-line so the log line matches parse_log_errors structured format
@@ -206,7 +219,7 @@ class TradingAPILogger:
206
219
 
207
220
  def log_warning(self, message: str, context: Optional[dict] = None):
208
221
  """Log a warning with structured context."""
209
- extra = context or {}
222
+ extra = self._sanitize_extra(context)
210
223
 
211
224
  # Add caller information
212
225
  caller_info = self._get_caller_info()
@@ -216,7 +229,7 @@ class TradingAPILogger:
216
229
 
217
230
  def log_info(self, message: str, context: Optional[dict] = None):
218
231
  """Log an info message with structured context."""
219
- extra = context or {}
232
+ extra = self._sanitize_extra(context)
220
233
 
221
234
  # Add caller information
222
235
  caller_info = self._get_caller_info()
@@ -226,7 +239,7 @@ class TradingAPILogger:
226
239
 
227
240
  def log_debug(self, message: str, context: Optional[dict] = None):
228
241
  """Log a debug message with structured context."""
229
- extra = context or {}
242
+ extra = self._sanitize_extra(context)
230
243
 
231
244
  # Add caller information
232
245
  caller_info = self._get_caller_info()
@@ -11,8 +11,6 @@ import pandas as pd
11
11
  import pytz
12
12
  import redis
13
13
 
14
- from chameli.dateutils import parse_datetime
15
-
16
14
  logger = logging.getLogger(__name__)
17
15
 
18
16
  from .exceptions import (
@@ -26,6 +24,7 @@ from .exceptions import (
26
24
  create_error_context,
27
25
  )
28
26
  from .error_handling import retry_on_error, safe_execute, log_execution_time, handle_broker_errors, validate_inputs
27
+ from chameli.dateutils import parse_datetime
29
28
 
30
29
  # Removed trading_logger import to avoid circular import issues
31
30
 
@@ -14,8 +14,6 @@ import requests
14
14
  import redis
15
15
  import pytz
16
16
 
17
- from chameli.dateutils import parse_datetime
18
-
19
17
  from dhanhq import dhanhq as dhanhq_sdk
20
18
  try:
21
19
  from dhanhq import DhanContext, MarketFeed
@@ -36,6 +34,7 @@ from .broker_base import (
36
34
  _normalize_as_of_date,
37
35
  )
38
36
  from .config import get_config
37
+ from chameli.dateutils import parse_datetime
39
38
  from .utils import (
40
39
  _set_long_symbol_if_not_combo,
41
40
  delete_broker_order_id,
@@ -517,7 +516,7 @@ class Dhan(BrokerBase):
517
516
  @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
518
517
  def get_available_capital(self) -> Dict[str, float]:
519
518
  """
520
- Returns dict with 'cash' and 'collateral' keys.
519
+ Returns dict with 'cash', 'collateral', and 'used' keys.
521
520
 
522
521
  Dhan's get_fund_limits() response data fields:
523
522
  availabelBalance, collateralAmount, utilisedAmount, …
@@ -533,9 +532,10 @@ class Dhan(BrokerBase):
533
532
  data = result.get("data", {})
534
533
  cash = float(data.get("availabelBalance", 0) or 0)
535
534
  collateral = float(data.get("collateralAmount", 0) or 0)
535
+ used = float(data.get("utilisedAmount", 0) or 0)
536
536
 
537
- trading_logger.log_debug("Available capital retrieved", {"cash": cash, "collateral": collateral})
538
- return {"cash": cash, "collateral": collateral}
537
+ trading_logger.log_debug("Available capital retrieved", {"cash": cash, "collateral": collateral, "used": used})
538
+ return {"cash": cash, "collateral": collateral, "used": used}
539
539
 
540
540
  except (BrokerConnectionError, MarketDataError):
541
541
  raise
@@ -875,6 +875,26 @@ class FivePaisa(BrokerBase):
875
875
  except (ValueError, TypeError):
876
876
  continue
877
877
 
878
+ used = 0.0
879
+ used_fields = [
880
+ "UtilizedMargin",
881
+ "UsedMargin",
882
+ "MarginUsed",
883
+ "BookedMargin",
884
+ "TotalMargin",
885
+ "utilizedmargin",
886
+ "usedmargin",
887
+ "marginused",
888
+ "MarginUtilized",
889
+ ]
890
+ for field in used_fields:
891
+ if field in margin_data[0]:
892
+ try:
893
+ used = float(margin_data[0][field])
894
+ break
895
+ except (ValueError, TypeError):
896
+ continue
897
+
878
898
  trading_logger.log_debug(
879
899
  "Available capital retrieved",
880
900
  {
@@ -882,10 +902,11 @@ class FivePaisa(BrokerBase):
882
902
  "funds_payln": funds_payln,
883
903
  "cash": cash,
884
904
  "collateral": collateral,
905
+ "used": used,
885
906
  "total_capital": cash + collateral,
886
907
  },
887
908
  )
888
- return {"cash": cash, "collateral": collateral}
909
+ return {"cash": cash, "collateral": collateral, "used": used}
889
910
 
890
911
  except (BrokerConnectionError, MarketDataError):
891
912
  raise
@@ -916,6 +937,7 @@ class FivePaisa(BrokerBase):
916
937
  if self.api:
917
938
  self.api = None
918
939
  trading_logger.log_info("API reference cleared", {"broker_type": self.broker.name})
940
+ self.subscribe_thread = None
919
941
 
920
942
  trading_logger.log_info("Successfully disconnected from FivePaisa", {"broker_type": self.broker.name})
921
943
  return True
@@ -3206,6 +3228,32 @@ class FivePaisa(BrokerBase):
3206
3228
  context = create_error_context(symbols=symbols, exchange=exchange, error=str(e))
3207
3229
  raise MarketDataError(f"Failed to map exchange: {str(e)}", context)
3208
3230
 
3231
+ def resolve_symbol_and_exchange(json_data):
3232
+ """Resolve a token to the correct symbol/exchange even when the tick exchange key is incomplete."""
3233
+ token = json_data.get("Token")
3234
+ exchange_candidates = []
3235
+ tick_exchange = json_data.get("Exch")
3236
+
3237
+ if tick_exchange in self.exchange_mappings:
3238
+ exchange_candidates.append(tick_exchange)
3239
+
3240
+ if isinstance(tick_exchange, str) and tick_exchange:
3241
+ short_exchange = tick_exchange[0]
3242
+ if short_exchange in self.exchange_mappings and short_exchange not in exchange_candidates:
3243
+ exchange_candidates.append(short_exchange)
3244
+
3245
+ for exchange_key in exchange_candidates:
3246
+ symbol = self.exchange_mappings[exchange_key]["symbol_map_reversed"].get(token)
3247
+ if symbol is not None:
3248
+ return symbol, exchange_key
3249
+
3250
+ for exchange_key, exchange_mapping in self.exchange_mappings.items():
3251
+ symbol = exchange_mapping["symbol_map_reversed"].get(token)
3252
+ if symbol is not None:
3253
+ return symbol, exchange_key
3254
+
3255
+ return None, tick_exchange
3256
+
3209
3257
  def map_to_price(json_data):
3210
3258
  """Map JSON data to Price object."""
3211
3259
  try:
@@ -3220,10 +3268,18 @@ class FivePaisa(BrokerBase):
3220
3268
  price.high = json_data.get("High", float("nan"))
3221
3269
  price.low = json_data.get("Low", float("nan"))
3222
3270
  price.volume = json_data.get("TotalQty", float("nan"))
3223
- price.symbol = self.exchange_mappings[json_data["Exch"]]["symbol_map_reversed"].get(
3224
- json_data.get("Token")
3225
- )
3226
- price.exchange = self.map_exchange_for_db(price.symbol, json_data["Exch"])
3271
+ price.symbol, resolved_exchange = resolve_symbol_and_exchange(json_data)
3272
+ if price.symbol is None:
3273
+ trading_logger.log_warning(
3274
+ "Skipping price data with unmapped token",
3275
+ {
3276
+ "token": json_data.get("Token"),
3277
+ "exchange": json_data.get("Exch"),
3278
+ "exchange_type": json_data.get("ExchType"),
3279
+ },
3280
+ )
3281
+ return None
3282
+ price.exchange = self.map_exchange_for_db(price.symbol, resolved_exchange)
3227
3283
  price.timestamp = self.convert_to_ist(json_data["TickDt"])
3228
3284
  return price
3229
3285
  except Exception as e:
@@ -3237,10 +3293,10 @@ class FivePaisa(BrokerBase):
3237
3293
  json_data = json.loads(data_str)
3238
3294
  if len(json_data) == 1:
3239
3295
  price = map_to_price(json_data[0])
3240
- if ext_callback:
3296
+ if price is not None and ext_callback:
3241
3297
  ext_callback(price)
3242
3298
  except Exception as e:
3243
- trading_logger.log_error("Error processing WebSocket message", e, {"message": message})
3299
+ trading_logger.log_error("Error processing WebSocket message", e, {"ws_message": message})
3244
3300
 
3245
3301
  def error_data(ws, err):
3246
3302
  """Handle WebSocket errors."""
@@ -3281,6 +3337,52 @@ class FivePaisa(BrokerBase):
3281
3337
  trading_logger.log_error("Error in connect_and_receive", e, {"req_data": req_data})
3282
3338
  raise
3283
3339
 
3340
+ def has_live_stream():
3341
+ """Return True only when the streaming thread and websocket are both usable."""
3342
+ return (
3343
+ self.subscribe_thread is not None
3344
+ and self.subscribe_thread.is_alive()
3345
+ and self.api is not None
3346
+ and getattr(self.api, "ws", None) is not None
3347
+ and callable(getattr(self.api.ws, "send", None))
3348
+ )
3349
+
3350
+ def reconnect_stream():
3351
+ """Start a fresh websocket and restore all subscriptions."""
3352
+ try:
3353
+ if self.api is not None and getattr(self.api, "ws", None) is not None:
3354
+ self.api.close_data()
3355
+ except Exception:
3356
+ pass
3357
+ self.subscribe_thread = None
3358
+ req_list_full = expand_symbols_to_request(self.subscribed_symbols)
3359
+ if not req_list_full:
3360
+ context = create_error_context(operation=operation, symbols=symbols, exchange=exchange)
3361
+ raise MarketDataError("No symbols to reconnect after socket closure", context)
3362
+ if self.api is None:
3363
+ raise BrokerConnectionError("API client not initialized")
3364
+ req_data_full = self.api.Request_Feed("mf", "s", req_list_full)
3365
+ self.subscribe_thread = threading.Thread(
3366
+ target=connect_and_receive,
3367
+ args=(req_data_full,),
3368
+ name="MarketDataStreamer",
3369
+ )
3370
+ self.subscribe_thread.start()
3371
+ time.sleep(2)
3372
+ trading_logger.log_info(
3373
+ "Reconnected after socket closure",
3374
+ {"subscribed_count": len(self.subscribed_symbols)},
3375
+ )
3376
+
3377
+ def send_stream_request(req_data):
3378
+ """Send an incremental subscribe/unsubscribe over an existing websocket."""
3379
+ if self.api is None:
3380
+ raise BrokerConnectionError("API client not initialized")
3381
+ ws = getattr(self.api, "ws", None)
3382
+ if ws is None or not callable(getattr(ws, "send", None)):
3383
+ raise AttributeError("WebSocket client does not support send")
3384
+ ws.send(json.dumps(req_data))
3385
+
3284
3386
  def resolve_exchange_from_symbology(long_symbol: str):
3285
3387
  """Resolve API exchange code for a symbol from symbology (which exchange's symbol_map contains it)."""
3286
3388
  for exch in self.exchange_mappings:
@@ -3367,7 +3469,7 @@ class FivePaisa(BrokerBase):
3367
3469
  req_data = self.api.Request_Feed("mf", operation, req_list)
3368
3470
 
3369
3471
  # Start the connection and receiving data in a separate thread
3370
- if self.subscribe_thread is None:
3472
+ if not has_live_stream():
3371
3473
  self.subscribe_thread = threading.Thread(
3372
3474
  target=connect_and_receive, args=(req_data,), name="MarketDataStreamer"
3373
3475
  )
@@ -3379,36 +3481,13 @@ class FivePaisa(BrokerBase):
3379
3481
  "Requesting streaming for existing connection", {"req_data": json.dumps(req_data)}
3380
3482
  )
3381
3483
  try:
3382
- self.api.ws.send(json.dumps(req_data))
3383
- except WebSocketConnectionClosedException:
3484
+ send_stream_request(req_data)
3485
+ except (AttributeError, WebSocketConnectionClosedException):
3384
3486
  trading_logger.log_info(
3385
3487
  "WebSocket closed, reconnecting...",
3386
3488
  {"operation": operation, "symbols_count": len(symbols), "exchange": exchange},
3387
3489
  )
3388
- try:
3389
- if self.api is not None:
3390
- self.api.close_data()
3391
- except Exception:
3392
- pass
3393
- self.subscribe_thread = None
3394
- req_list_full = expand_symbols_to_request(self.subscribed_symbols)
3395
- if not req_list_full:
3396
- context = create_error_context(operation=operation, symbols=symbols, exchange=exchange)
3397
- raise MarketDataError("No symbols to reconnect after socket closure", context)
3398
- if self.api is None:
3399
- raise BrokerConnectionError("API client not initialized")
3400
- req_data_full = self.api.Request_Feed("mf", "s", req_list_full)
3401
- self.subscribe_thread = threading.Thread(
3402
- target=connect_and_receive,
3403
- args=(req_data_full,),
3404
- name="MarketDataStreamer",
3405
- )
3406
- self.subscribe_thread.start()
3407
- time.sleep(2)
3408
- trading_logger.log_info(
3409
- "Reconnected after socket closure",
3410
- {"subscribed_count": len(self.subscribed_symbols)},
3411
- )
3490
+ reconnect_stream()
3412
3491
  # New connection already has full subscription; no need to send again
3413
3492
  else:
3414
3493
  trading_logger.log_warning(
@@ -3440,6 +3519,7 @@ class FivePaisa(BrokerBase):
3440
3519
  try:
3441
3520
  if self.api is not None:
3442
3521
  self.api.close_data()
3522
+ self.subscribe_thread = None
3443
3523
  trading_logger.log_info("Streaming stopped successfully")
3444
3524
  except Exception as e:
3445
3525
  context = create_error_context(error=str(e))
@@ -902,16 +902,25 @@ class FlatTrade(BrokerBase):
902
902
 
903
903
  cash_float = float(cash)
904
904
  collateral = float(limits.get("collateral", 0))
905
+ used = 0.0
906
+ for field in ("usedmargin", "UsedMargin", "utilisedamount", "UtilisedAmount", "marginused", "MarginUsed"):
907
+ if field in limits:
908
+ try:
909
+ used = float(limits[field])
910
+ break
911
+ except (ValueError, TypeError):
912
+ continue
905
913
 
906
914
  trading_logger.log_debug(
907
915
  "Available capital retrieved",
908
916
  {
909
917
  "cash": cash_float,
910
918
  "collateral": collateral,
919
+ "used": used,
911
920
  "total_capital": cash_float + collateral,
912
921
  },
913
922
  )
914
- return {"cash": cash_float, "collateral": collateral}
923
+ return {"cash": cash_float, "collateral": collateral, "used": used}
915
924
 
916
925
  except (BrokerConnectionError, MarketDataError):
917
926
  raise
@@ -796,16 +796,36 @@ class Shoonya(BrokerBase):
796
796
  except (ValueError, TypeError):
797
797
  continue
798
798
 
799
+ used = 0.0
800
+ used_fields = [
801
+ "usedmargin",
802
+ "UsedMargin",
803
+ "utilisedamount",
804
+ "UtilisedAmount",
805
+ "marginused",
806
+ "MarginUsed",
807
+ "spanused",
808
+ "SpanUsed",
809
+ ]
810
+ for field in used_fields:
811
+ if field in limits:
812
+ try:
813
+ used = float(limits[field])
814
+ break
815
+ except (ValueError, TypeError):
816
+ continue
817
+
799
818
  trading_logger.log_debug(
800
819
  "Available capital retrieved",
801
820
  {
802
821
  "cash": cash_float,
803
822
  "collateral": collateral,
823
+ "used": used,
804
824
  "total_capital": cash_float + collateral,
805
825
  "broker": self.broker.name,
806
826
  },
807
827
  )
808
- return {"cash": cash_float, "collateral": collateral}
828
+ return {"cash": cash_float, "collateral": collateral, "used": used}
809
829
 
810
830
  except (BrokerConnectionError, MarketDataError):
811
831
  raise
@@ -368,6 +368,9 @@ def _safe_parse_json_or_dict(json_str):
368
368
  }
369
369
  return eval(json_str, safe_globals, {})
370
370
  except Exception as eval_error:
371
+ parsed_loose_info = _parse_loose_additional_info(json_str)
372
+ if parsed_loose_info is not None:
373
+ return parsed_loose_info
371
374
  raise DataError(
372
375
  f"Cannot parse as JSON or Python dict: {str(e)}",
373
376
  {
@@ -379,6 +382,57 @@ def _safe_parse_json_or_dict(json_str):
379
382
  )
380
383
 
381
384
 
385
+ def _parse_loose_additional_info(info_str):
386
+ info_str = str(info_str).strip()
387
+ if not info_str:
388
+ return {}
389
+
390
+ import shlex
391
+
392
+ def _coerce_value(value):
393
+ lower_value = value.lower()
394
+ if lower_value == "true":
395
+ return True
396
+ if lower_value == "false":
397
+ return False
398
+ if lower_value == "none":
399
+ return None
400
+ try:
401
+ if any(ch in value for ch in [".", "e", "E"]):
402
+ return float(value)
403
+ return int(value)
404
+ except ValueError:
405
+ return value
406
+
407
+ try:
408
+ tokens = shlex.split(info_str)
409
+ except ValueError:
410
+ tokens = info_str.split()
411
+
412
+ parsed = {}
413
+ message_tokens = []
414
+ for token in tokens:
415
+ token_parts = [part for part in token.split(",") if part]
416
+ consumed_key_value = False
417
+ for part in token_parts:
418
+ if "=" not in part:
419
+ if not consumed_key_value:
420
+ message_tokens.append(part)
421
+ continue
422
+ key, value = part.split("=", 1)
423
+ if not key:
424
+ continue
425
+ parsed[key] = _coerce_value(value)
426
+ consumed_key_value = True
427
+ if "=" not in token and not token_parts:
428
+ message_tokens.append(token)
429
+
430
+ if message_tokens:
431
+ parsed["message"] = " ".join(message_tokens)
432
+
433
+ return parsed or {"message": info_str}
434
+
435
+
382
436
  @log_execution_time
383
437
  def _merge_additional_info(old_info, new_info):
384
438
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
File without changes
File without changes