tradingapi 0.4.0__tar.gz → 0.4.1__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 (38) hide show
  1. {tradingapi-0.4.0 → tradingapi-0.4.1}/PKG-INFO +5 -1
  2. {tradingapi-0.4.0 → tradingapi-0.4.1}/README.md +4 -0
  3. {tradingapi-0.4.0 → tradingapi-0.4.1}/pyproject.toml +1 -1
  4. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/dhan.py +16 -0
  5. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/flattrade.py +133 -59
  6. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/shoonya.py +75 -20
  7. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/utils.py +14 -1
  8. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/PKG-INFO +5 -1
  9. {tradingapi-0.4.0 → tradingapi-0.4.1}/setup.cfg +0 -0
  10. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_broker_side_terminal_order.py +0 -0
  11. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_calculate_delta_realtime_quotes.py +0 -0
  12. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_find_option_with_delta.py +0 -0
  13. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_fivepaisa_historical_recovery.py +0 -0
  14. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_fivepaisa_stream_reconnect.py +0 -0
  15. {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_market_hours_config.py +0 -0
  16. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/__init__.py +0 -0
  17. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/allocation.py +0 -0
  18. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/attribution.py +0 -0
  19. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/broker_base.py +0 -0
  20. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config/commissions_20241216.yaml +0 -0
  21. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config/config_sample.yaml +0 -0
  22. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config.py +0 -0
  23. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/error_handling.py +0 -0
  24. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/exceptions.py +0 -0
  25. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/fivepaisa.py +0 -0
  26. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/globals.py +0 -0
  27. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/ib.py +0 -0
  28. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/icicidirect.py +0 -0
  29. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/icicidirect_generate_session.py +0 -0
  30. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/market_data_exchanges.py +0 -0
  31. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/market_hours.py +0 -0
  32. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/proxy_utils.py +0 -0
  33. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/span.py +0 -0
  34. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/SOURCES.txt +0 -0
  35. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/dependency_links.txt +0 -0
  36. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/entry_points.txt +0 -0
  37. {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/requires.txt +0 -0
  38. {tradingapi-0.4.0 → tradingapi-0.4.1}/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.4.0
3
+ Version: 0.4.1
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -170,6 +170,10 @@ The package requires:
170
170
  - Redis (for order tracking and logging)
171
171
  - Broker-specific SDKs (automatically installed via dependencies)
172
172
 
173
+ ## Adding a new broker
174
+
175
+ Step-by-step checklist (package, YAML, website, PnL/EOD, order manager): [`docs/ADDING_A_BROKER.md`](docs/ADDING_A_BROKER.md).
176
+
173
177
  ## Configuration
174
178
 
175
179
  ### Configuration File Structure
@@ -151,6 +151,10 @@ The package requires:
151
151
  - Redis (for order tracking and logging)
152
152
  - Broker-specific SDKs (automatically installed via dependencies)
153
153
 
154
+ ## Adding a new broker
155
+
156
+ Step-by-step checklist (package, YAML, website, PnL/EOD, order manager): [`docs/ADDING_A_BROKER.md`](docs/ADDING_A_BROKER.md).
157
+
154
158
  ## Configuration
155
159
 
156
160
  ### Configuration File Structure
@@ -28,7 +28,7 @@ packages = ["tradingapi"]
28
28
 
29
29
  [project]
30
30
  name = "tradingapi"
31
- version = "0.4.0"
31
+ version = "0.4.1"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -1605,6 +1605,22 @@ class Dhan(BrokerBase):
1605
1605
  data = data[0] if data else {}
1606
1606
  if not isinstance(data, dict):
1607
1607
  raise OrderError(f"Unexpected get_order_by_id payload type: {type(data).__name__}")
1608
+ # success + empty payload = wrong account / order not visible — do not invent a zero fill
1609
+ if not data:
1610
+ trading_logger.log_warning(
1611
+ "get_order_by_id returned empty data; skipping status update",
1612
+ {"order_id": broker_order_id, "account": self.account_key},
1613
+ )
1614
+ return OrderInfo(
1615
+ order_size=order.quantity,
1616
+ order_price=order.price,
1617
+ fill_size=0,
1618
+ fill_price=0,
1619
+ status=OrderStatus.UNDEFINED,
1620
+ broker_order_id=broker_order_id,
1621
+ exchange_order_id=getattr(order, "exch_order_id", "") or "",
1622
+ broker=self.broker,
1623
+ )
1608
1624
 
1609
1625
  # Dhan order status mapping
1610
1626
  dhan_status = str(data.get("orderStatus", "")).upper()
@@ -95,8 +95,8 @@ class FlatTradeApiPy(NorenApi):
95
95
  def __init__(self):
96
96
  NorenApi.__init__(
97
97
  self,
98
- host="https://piconnect.flattrade.in/PiConnectTP/",
99
- websocket="wss://piconnect.flattrade.in/PiConnectWSTp/",
98
+ host="https://piconnect.flattrade.in/PiConnectAPI",
99
+ websocket="wss://piconnect.flattrade.in/PiConnectWSAPI/",
100
100
  )
101
101
 
102
102
 
@@ -268,12 +268,16 @@ def save_symbol_data(saveToFolder: bool = True):
268
268
 
269
269
  def process_row(row):
270
270
  symbol = row["Symbol"]
271
- if row["Instrument"].startswith("OPT"):
271
+ instrument = row["Instrument"]
272
+ if pd.isna(instrument) or pd.isna(symbol):
273
+ return None
274
+ if str(instrument).startswith("OPT"):
272
275
  return f"{symbol}_OPT_{row['Expiry']}_{'CALL' if row['OptionType']=='CE' else 'PUT'}_{row['StrikePrice']:g}".upper()
273
276
  else:
274
277
  return f"{symbol}_FUT_{row['Expiry']}__".upper()
275
278
 
276
279
  codes_fno["long_symbol"] = codes_fno.apply(process_row, axis=1)
280
+ codes_fno = codes_fno.dropna(subset=["long_symbol"])
277
281
  codes_fno["Exch"] = "NFO"
278
282
  codes_fno["ExchType"] = "NFO"
279
283
  new_column_names = {
@@ -312,18 +316,24 @@ def save_symbol_data(saveToFolder: bool = True):
312
316
  codes_fno["Expiry"] = pd.to_datetime(
313
317
  codes_fno["Expiry"], format="%d-%b-%Y", errors="coerce"
314
318
  ).dt.strftime("%Y%m%d")
315
- codes_fno["Symbol"] = codes_fno["TradingSymbol"].str.extract(
316
- r"^([A-Z]+(?:50)?)(?=\d{2}(?:[A-Z]\d+|\d+)[A-Z]{2})"
319
+ codes_fno[["Symbol", "ts_rest"]] = codes_fno["TradingSymbol"].str.extract(
320
+ r"^(SENSEX50|[A-Z0-9&-]*?[A-Z&-])(?=(?:\d{2}|FUT))(.*)$",
321
+ expand=True,
317
322
  )
323
+ codes_fno["Symbol"] = codes_fno["Symbol"].fillna(codes_fno["TradingSymbol"])
318
324
 
319
325
  def process_row(row):
320
326
  symbol = row["Symbol"]
321
- if row["Instrument"].startswith("OPT"):
327
+ instrument = row["Instrument"]
328
+ if pd.isna(instrument) or pd.isna(symbol):
329
+ return None
330
+ if str(instrument).startswith("OPT"):
322
331
  return f"{symbol}_OPT_{row['Expiry']}_{'CALL' if row['OptionType']=='CE' else 'PUT'}_{row['StrikePrice']:g}".upper()
323
332
  else:
324
333
  return f"{symbol}_FUT_{row['Expiry']}__".upper()
325
334
 
326
335
  codes_fno["long_symbol"] = codes_fno.apply(process_row, axis=1)
336
+ codes_fno = codes_fno.dropna(subset=["long_symbol"])
327
337
  codes_fno["Exch"] = "BFO"
328
338
  codes_fno["ExchType"] = "BFO"
329
339
  new_column_names = {
@@ -365,6 +375,7 @@ class FlatTrade(BrokerBase):
365
375
  self.api = None
366
376
  self.subscribe_thread = None
367
377
  self.subscribed_symbols = []
378
+ self.subscribed_req_tokens = []
368
379
  self.socket_opened = False
369
380
  self._is_connected_cache_ttl_secs = 3.0
370
381
  self._last_is_connected_check_ts = 0.0
@@ -501,6 +512,19 @@ class FlatTrade(BrokerBase):
501
512
  """Verify if the current session is valid using existing is_connected method."""
502
513
  return self.is_connected()
503
514
 
515
+ def _apply_api_session(user, pwd, susertoken):
516
+ self.api = FlatTradeApiPy()
517
+ kwargs = {"userid": user, "password": pwd, "usertoken": susertoken}
518
+ try:
519
+ params = inspect.signature(self.api.set_session).parameters
520
+ except (TypeError, ValueError):
521
+ params = {}
522
+ if "accesstoken" in params:
523
+ kwargs["accesstoken"] = susertoken
524
+ self.api.set_session(**kwargs)
525
+ if hasattr(self.api, "injectOAuthHeader"):
526
+ self.api.injectOAuthHeader(susertoken, user, user)
527
+
504
528
  def _restore_session_from_token(susertoken_path):
505
529
  """Attempt to restore session from existing token."""
506
530
  try:
@@ -515,8 +539,7 @@ class FlatTrade(BrokerBase):
515
539
  trading_logger.log_warning("Empty token file", {"broker": self.broker.name})
516
540
  return False
517
541
 
518
- self.api = FlatTradeApiPy()
519
- self.api.set_session(userid=user, password=pwd, usertoken=susertoken)
542
+ _apply_api_session(user, pwd, susertoken)
520
543
 
521
544
  # Verify the session is actually working
522
545
  if _verify_session(self):
@@ -557,14 +580,28 @@ class FlatTrade(BrokerBase):
557
580
  "Accept": "application/json",
558
581
  "Accept-Language": "en-US,en;q=0.5",
559
582
  "Host": "authapi.flattrade.in",
560
- "Origin": f"{HOST}",
561
- "Referer": f"{HOST}/",
583
+ "Origin": HOST,
584
+ "Referer": f"{HOST}/?app_key={api_key}",
562
585
  }
563
586
 
564
587
  def encode_item(item):
565
588
  encoded_item = hashlib.sha256(item.encode()).hexdigest()
566
589
  return encoded_item
567
590
 
591
+ def ftauth_payload(sid, override=""):
592
+ return {
593
+ "UserName": user,
594
+ "Password": encode_item(pwd),
595
+ "App": "",
596
+ "ClientID": "",
597
+ "Key": "",
598
+ "APIKey": api_key,
599
+ "PAN_DOB": pyotp.TOTP(token).now(),
600
+ "Sid": sid,
601
+ "Override": override,
602
+ "Version": "2",
603
+ }
604
+
568
605
  def get_authcode():
569
606
  try:
570
607
  with requests.Session() as session:
@@ -578,17 +615,7 @@ class FlatTrade(BrokerBase):
578
615
 
579
616
  response = session.post(
580
617
  routes["ftauth"],
581
- json={
582
- "UserName": user,
583
- "Password": encode_item(pwd),
584
- "App": "",
585
- "ClientID": "",
586
- "Key": "",
587
- "APIKey": api_key,
588
- "PAN_DOB": pyotp.TOTP(token).now(),
589
- "Sid": sid,
590
- "Override": "",
591
- },
618
+ json=ftauth_payload(sid),
592
619
  )
593
620
 
594
621
  if response.status_code == 200:
@@ -600,17 +627,7 @@ class FlatTrade(BrokerBase):
600
627
  )
601
628
  response = session.post(
602
629
  routes["ftauth"],
603
- json={
604
- "UserName": user,
605
- "Password": encode_item(pwd),
606
- "App": "",
607
- "ClientID": "",
608
- "Key": "",
609
- "APIKey": api_key,
610
- "PAN_DOB": pyotp.TOTP(token).now(),
611
- "Sid": sid,
612
- "Override": "Y",
613
- },
630
+ json=ftauth_payload(sid, "Y"),
614
631
  )
615
632
  if response.status_code == 200:
616
633
  response_data = response.json()
@@ -716,8 +733,7 @@ class FlatTrade(BrokerBase):
716
733
 
717
734
  # Initialize API
718
735
  try:
719
- self.api = FlatTradeApiPy()
720
- self.api.set_session(userid=user, password=pwd, usertoken=susertoken)
736
+ _apply_api_session(user, pwd, susertoken)
721
737
  trading_logger.log_info("API session established", {"user": user})
722
738
  except Exception as e:
723
739
  context = create_error_context(user=user, error=str(e))
@@ -1165,13 +1181,17 @@ class FlatTrade(BrokerBase):
1165
1181
  # Iterate through the data frame and create mappings based on exchange
1166
1182
 
1167
1183
  for exchange, group in codes.groupby("Exch"):
1184
+ scrip_int = pd.to_numeric(group["Scripcode"], errors="coerce")
1185
+ scrip_int = scrip_int.apply(lambda x: int(x) if pd.notna(x) else None)
1168
1186
  self.exchange_mappings[exchange] = {
1169
- "symbol_map": dict(zip(group["long_symbol"], group["Scripcode"])),
1187
+ "symbol_map": dict(zip(group["long_symbol"], scrip_int)),
1170
1188
  "contractsize_map": dict(zip(group["long_symbol"], group["LotSize"])),
1171
1189
  "exchange_map": dict(zip(group["long_symbol"], group["Exch"])),
1172
1190
  "exchangetype_map": dict(zip(group["long_symbol"], group["ExchType"])),
1173
1191
  "contracttick_map": dict(zip(group["long_symbol"], group["TickSize"])),
1174
- "symbol_map_reversed": dict(zip(group["Scripcode"], group["long_symbol"])),
1192
+ "symbol_map_reversed": {
1193
+ code: name for code, name in zip(scrip_int, group["long_symbol"]) if code is not None
1194
+ },
1175
1195
  "tradingsymbol_map": dict(zip(group["long_symbol"], group["trading_symbol"])),
1176
1196
  }
1177
1197
  return codes
@@ -2647,7 +2667,9 @@ class FlatTrade(BrokerBase):
2647
2667
  list(self.subscribed_symbols) if hasattr(self, "subscribed_symbols") else []
2648
2668
  )
2649
2669
  if active_symbols:
2650
- reconnect_req_list = expand_symbols_to_request(active_symbols)
2670
+ reconnect_req_list = list(self.subscribed_req_tokens) or expand_symbols_to_request(
2671
+ active_symbols
2672
+ )
2651
2673
  trading_logger.log_info(
2652
2674
  "Resubscribing to symbols after reconnection",
2653
2675
  {"symbols_count": len(active_symbols), "req_list": reconnect_req_list},
@@ -2700,11 +2722,20 @@ class FlatTrade(BrokerBase):
2700
2722
  time.sleep(1)
2701
2723
 
2702
2724
  def resolve_exchange_from_symbology(long_symbol: str):
2703
- """Resolve API exchange for a symbol from symbology (which exchange's symbol_map contains it)."""
2725
+ """Prefer the caller's exchange so dual-listed names (e.g. INFY) stay on NSE vs BSE."""
2726
+ preferred = None
2727
+ try:
2728
+ preferred = self.map_exchange_for_api(long_symbol, exchange)
2729
+ except Exception:
2730
+ preferred = mapped_exchange
2731
+ if preferred:
2732
+ symbol_map = self.exchange_mappings.get(preferred, {}).get("symbol_map", {})
2733
+ if long_symbol in symbol_map:
2734
+ return preferred
2704
2735
  for exch in self.exchange_mappings:
2705
2736
  if long_symbol in self.exchange_mappings[exch]["symbol_map"]:
2706
2737
  return exch
2707
- return None
2738
+ return preferred
2708
2739
 
2709
2740
  # Function to expand symbols into request format
2710
2741
  def expand_symbols_to_request(symbol_list):
@@ -2745,34 +2776,32 @@ class FlatTrade(BrokerBase):
2745
2776
  return req_list
2746
2777
 
2747
2778
  # Function to update the subscription list
2748
- def update_subscription_list(operation, symbols):
2779
+ def update_subscription_list(operation, symbols, tokens):
2749
2780
  if operation == "s":
2750
2781
  self.subscribed_symbols = list(set(self.subscribed_symbols + symbols))
2782
+ self.subscribed_req_tokens = list(set(self.subscribed_req_tokens + tokens))
2751
2783
  elif operation == "u":
2752
2784
  self.subscribed_symbols = list(set(self.subscribed_symbols) - set(symbols))
2785
+ self.subscribed_req_tokens = [t for t in self.subscribed_req_tokens if t not in tokens]
2753
2786
 
2754
- # Update subscriptions and request list
2755
- update_subscription_list(operation, symbols)
2756
2787
  req_list = expand_symbols_to_request(symbols)
2788
+ update_subscription_list(operation, symbols, req_list)
2757
2789
 
2758
- # Start the WebSocket connection if not already started
2759
2790
  if self.subscribe_thread is None:
2760
2791
  self.subscribe_thread = threading.Thread(target=connect_and_subscribe, name="MarketDataStreamer")
2761
2792
  self.subscribe_thread.start()
2762
2793
 
2763
- # Wait until the socket is opened before subscribing/unsubscribing
2764
2794
  while not self.socket_opened:
2765
2795
  time.sleep(1)
2766
2796
 
2767
- # Manage subscription based on operation
2768
- if req_list:
2769
- if operation == "s":
2770
- trading_logger.log_info("Requesting streaming", {"req_list": req_list})
2771
- self._wait_for_stream_request_rate_limit()
2772
- self.api.subscribe(req_list)
2773
- elif operation == "u":
2774
- trading_logger.log_info("Unsubscribing streaming", {"req_list": req_list})
2775
- self.api.unsubscribe(req_list)
2797
+ if req_list:
2798
+ if operation == "s":
2799
+ trading_logger.log_info("Requesting streaming", {"req_list": req_list})
2800
+ self._wait_for_stream_request_rate_limit()
2801
+ self.api.subscribe(req_list)
2802
+ elif operation == "u":
2803
+ trading_logger.log_info("Unsubscribing streaming", {"req_list": req_list})
2804
+ self.api.unsubscribe(req_list)
2776
2805
  except Exception as e:
2777
2806
  trading_logger.log_error(
2778
2807
  "Unexpected error in start_quotes_streaming",
@@ -2780,6 +2809,25 @@ class FlatTrade(BrokerBase):
2780
2809
  {"operation": operation, "symbols_count": len(symbols) if symbols else 0, "exchange": exchange},
2781
2810
  )
2782
2811
 
2812
+ @log_execution_time
2813
+ @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
2814
+ def stop_streaming(self):
2815
+ try:
2816
+ trading_logger.log_info("Stopping quotes streaming")
2817
+ if hasattr(self, "api") and self.api and getattr(self, "socket_opened", False):
2818
+ try:
2819
+ self.api.close_websocket()
2820
+ except Exception as e:
2821
+ trading_logger.log_warning("Failed to close WebSocket", {"error": str(e)})
2822
+ self.socket_opened = False
2823
+ self.subscribe_thread = None
2824
+ self.subscribed_symbols = []
2825
+ self.subscribed_req_tokens = []
2826
+ trading_logger.log_info("Streaming stopped successfully")
2827
+ except Exception as e:
2828
+ context = create_error_context(error=str(e))
2829
+ raise BrokerConnectionError(f"Failed to stop streaming: {str(e)}", context)
2830
+
2783
2831
  @log_execution_time
2784
2832
  @validate_inputs(long_symbol=lambda x: x is None or (isinstance(x, str) and len(x.strip()) >= 0))
2785
2833
  @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
@@ -2802,7 +2850,8 @@ class FlatTrade(BrokerBase):
2802
2850
  holding = pd.DataFrame([raw_holding])
2803
2851
  else:
2804
2852
  holding = pd.DataFrame(columns=["long_symbol", "quantity"])
2805
-
2853
+ if len(holding) == 0:
2854
+ holding = pd.DataFrame(columns=["long_symbol", "quantity"]).astype({"quantity": float})
2806
2855
  if len(holding) > 0:
2807
2856
  try:
2808
2857
  if "exch" not in holding.columns and "exch_tsym" not in holding.columns:
@@ -3056,14 +3105,29 @@ class FlatTrade(BrokerBase):
3056
3105
  {"row_count": len(scripcode)},
3057
3106
  )
3058
3107
 
3108
+ def mapping_keys(exchange_val):
3109
+ raw = "" if exchange_val is None or (isinstance(exchange_val, float) and pd.isna(exchange_val)) else str(exchange_val).strip()
3110
+ if raw in self.exchange_mappings:
3111
+ return [raw]
3112
+ up = raw.upper()
3113
+ if up in self.exchange_mappings:
3114
+ return [up]
3115
+ if not up:
3116
+ return list(self.exchange_mappings.keys())
3117
+ keyed = [k for k in self.exchange_mappings if str(k).upper().startswith(up[0])]
3118
+ return keyed if keyed else list(self.exchange_mappings.keys())
3119
+
3059
3120
  def lookup(scripcode_val, exchange_val):
3060
3121
  try:
3061
- exch_map = self.exchange_mappings.get(exchange_val, {})
3062
- rev = exch_map.get("symbol_map_reversed", {})
3063
- code = int(scripcode_val) if scripcode_val is not None else None
3122
+ code = int(float(scripcode_val)) if scripcode_val is not None and not pd.isna(scripcode_val) else None
3064
3123
  if code is None:
3065
3124
  return None
3066
- return rev.get(code) or rev.get(scripcode_val)
3125
+ for key in mapping_keys(exchange_val):
3126
+ rev = self.exchange_mappings.get(key, {}).get("symbol_map_reversed", {})
3127
+ found = rev.get(code) or rev.get(scripcode_val)
3128
+ if found:
3129
+ return found
3130
+ return None
3067
3131
  except (TypeError, ValueError, KeyError):
3068
3132
  return None
3069
3133
 
@@ -3114,9 +3178,19 @@ class FlatTrade(BrokerBase):
3114
3178
  )
3115
3179
  raise SymbolError(f"Exchange mapping not found: {str(e)}", context)
3116
3180
 
3181
+ lot_from_map = self.exchange_mappings[exchange]["contractsize_map"].get(long_symbol)
3182
+ if lot_from_map is not None and pd.notna(lot_from_map):
3183
+ return int(lot_from_map)
3184
+
3117
3185
  if code is not None:
3118
3186
  try:
3119
- series = cast(pd.Series, self.codes.loc[self.codes.Scripcode == code, "LotSize"])
3187
+ series = cast(
3188
+ pd.Series,
3189
+ self.codes.loc[
3190
+ (self.codes.Scripcode == code) & (self.codes.Exch == exchange),
3191
+ "LotSize",
3192
+ ],
3193
+ )
3120
3194
  if len(series) > 0:
3121
3195
  lot_size = int(series.iloc[0])
3122
3196
  else:
@@ -392,6 +392,7 @@ class Shoonya(BrokerBase):
392
392
  self.api = None
393
393
  self.subscribe_thread = None
394
394
  self.subscribed_symbols = []
395
+ self.subscribed_req_tokens: List[str] = []
395
396
  self.socket_opened = False
396
397
  self._is_connected_cache_ttl_secs = 3.0
397
398
  self._last_is_connected_check_ts = 0.0
@@ -2880,7 +2881,9 @@ class Shoonya(BrokerBase):
2880
2881
  list(self.subscribed_symbols) if hasattr(self, "subscribed_symbols") else []
2881
2882
  )
2882
2883
  if active_symbols:
2883
- reconnect_req_list = expand_symbols_to_request(active_symbols)
2884
+ reconnect_req_list = list(self.subscribed_req_tokens) or expand_symbols_to_request(
2885
+ active_symbols
2886
+ )
2884
2887
  trading_logger.log_info(
2885
2888
  "Resubscribing to symbols after reconnection",
2886
2889
  {
@@ -2937,11 +2940,20 @@ class Shoonya(BrokerBase):
2937
2940
  time.sleep(1)
2938
2941
 
2939
2942
  def resolve_exchange_from_symbology(long_symbol: str):
2940
- """Resolve API exchange for a symbol from symbology (which exchange's symbol_map contains it)."""
2943
+ """Prefer the caller's exchange so dual-listed names (e.g. INFY) stay on NSE vs BSE."""
2944
+ preferred = None
2945
+ try:
2946
+ preferred = self.map_exchange_for_api(long_symbol, exchange)
2947
+ except Exception:
2948
+ preferred = mapped_exchange
2949
+ if preferred:
2950
+ symbol_map = self.exchange_mappings.get(preferred, {}).get("symbol_map", {})
2951
+ if long_symbol in symbol_map:
2952
+ return preferred
2941
2953
  for exch in self.exchange_mappings:
2942
2954
  if long_symbol in self.exchange_mappings[exch]["symbol_map"]:
2943
2955
  return exch
2944
- return None
2956
+ return preferred
2945
2957
 
2946
2958
  # Function to expand symbols into request format
2947
2959
  def expand_symbols_to_request(symbol_list) -> List[str]:
@@ -2982,34 +2994,33 @@ class Shoonya(BrokerBase):
2982
2994
  return req_list
2983
2995
 
2984
2996
  # Function to update the subscription list
2985
- def update_subscription_list(operation, symbols) -> None:
2997
+ def update_subscription_list(operation, symbols, tokens) -> None:
2986
2998
  if operation == "s":
2987
2999
  self.subscribed_symbols = list(set(self.subscribed_symbols + symbols))
3000
+ self.subscribed_req_tokens = list(set(self.subscribed_req_tokens + tokens))
2988
3001
  elif operation == "u":
2989
3002
  self.subscribed_symbols = list(set(self.subscribed_symbols) - set(symbols))
3003
+ self.subscribed_req_tokens = [t for t in self.subscribed_req_tokens if t not in tokens]
2990
3004
 
2991
- # Update subscriptions and request list
2992
- update_subscription_list(operation, symbols)
2993
3005
  req_list = expand_symbols_to_request(symbols)
3006
+ update_subscription_list(operation, symbols, req_list)
2994
3007
 
2995
3008
  # Start the WebSocket connection if not already started
2996
3009
  if self.subscribe_thread is None:
2997
3010
  self.subscribe_thread = threading.Thread(target=connect_and_subscribe, name="MarketDataStreamer")
2998
3011
  self.subscribe_thread.start()
2999
3012
 
3000
- # Wait until the socket is opened before subscribing/unsubscribing
3001
3013
  while not self.socket_opened:
3002
3014
  time.sleep(1)
3003
3015
 
3004
- # Manage subscription based on operation
3005
- if req_list:
3006
- if operation == "s":
3007
- trading_logger.log_info("Requesting streaming", {"req_list": req_list})
3008
- self._wait_for_stream_request_rate_limit()
3009
- self.api.subscribe(req_list)
3010
- elif operation == "u":
3011
- trading_logger.log_info("Unsubscribing streaming", {"req_list": req_list})
3012
- self.api.unsubscribe(req_list)
3016
+ if req_list:
3017
+ if operation == "s":
3018
+ trading_logger.log_info("Requesting streaming", {"req_list": req_list})
3019
+ self._wait_for_stream_request_rate_limit()
3020
+ self.api.subscribe(req_list)
3021
+ elif operation == "u":
3022
+ trading_logger.log_info("Unsubscribing streaming", {"req_list": req_list})
3023
+ self.api.unsubscribe(req_list)
3013
3024
  except Exception as e:
3014
3025
  trading_logger.log_error(
3015
3026
  "Unexpected error in start_quotes_streaming",
@@ -3017,6 +3028,25 @@ class Shoonya(BrokerBase):
3017
3028
  {"operation": operation, "symbols_count": len(symbols) if symbols else 0, "exchange": exchange},
3018
3029
  )
3019
3030
 
3031
+ @log_execution_time
3032
+ @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
3033
+ def stop_streaming(self):
3034
+ try:
3035
+ trading_logger.log_info("Stopping quotes streaming")
3036
+ if hasattr(self, "api") and self.api and getattr(self, "socket_opened", False):
3037
+ try:
3038
+ self.api.close_websocket()
3039
+ except Exception as e:
3040
+ trading_logger.log_warning("Failed to close WebSocket", {"error": str(e)})
3041
+ self.socket_opened = False
3042
+ self.subscribe_thread = None
3043
+ self.subscribed_symbols = []
3044
+ self.subscribed_req_tokens = []
3045
+ trading_logger.log_info("Streaming stopped successfully")
3046
+ except Exception as e:
3047
+ context = create_error_context(error=str(e))
3048
+ raise BrokerConnectionError(f"Failed to stop streaming: {str(e)}", context)
3049
+
3020
3050
  @log_execution_time
3021
3051
  @validate_inputs(long_symbol=lambda x: x is None or (isinstance(x, str) and len(x.strip()) >= 0))
3022
3052
  @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
@@ -3039,6 +3069,8 @@ class Shoonya(BrokerBase):
3039
3069
  holding = pd.DataFrame([raw_holding])
3040
3070
  else:
3041
3071
  holding = pd.DataFrame(columns=["long_symbol", "quantity"])
3072
+ if len(holding) == 0:
3073
+ holding = pd.DataFrame(columns=["long_symbol", "quantity"]).astype({"quantity": float})
3042
3074
  if len(holding) > 0:
3043
3075
  try:
3044
3076
  # Resolve long_symbol from mappings using Scripcode + Exchange
@@ -3310,14 +3342,37 @@ class Shoonya(BrokerBase):
3310
3342
  {"row_count": len(scripcode)},
3311
3343
  )
3312
3344
 
3345
+ def mapping_keys(exchange_val):
3346
+ raw = (
3347
+ ""
3348
+ if exchange_val is None or (isinstance(exchange_val, float) and pd.isna(exchange_val))
3349
+ else str(exchange_val).strip()
3350
+ )
3351
+ if raw in self.exchange_mappings:
3352
+ return [raw]
3353
+ up = raw.upper()
3354
+ if up in self.exchange_mappings:
3355
+ return [up]
3356
+ if not up:
3357
+ return list(self.exchange_mappings.keys())
3358
+ keyed = [k for k in self.exchange_mappings if str(k).upper().startswith(up[0])]
3359
+ return keyed if keyed else list(self.exchange_mappings.keys())
3360
+
3313
3361
  def lookup(scripcode_val, exchange_val):
3314
3362
  try:
3315
- exch_map = self.exchange_mappings.get(exchange_val, {})
3316
- rev = exch_map.get("symbol_map_reversed", {})
3317
- code = int(scripcode_val) if scripcode_val is not None else None
3363
+ code = (
3364
+ int(float(scripcode_val))
3365
+ if scripcode_val is not None and not pd.isna(scripcode_val)
3366
+ else None
3367
+ )
3318
3368
  if code is None:
3319
3369
  return None
3320
- return rev.get(code) or rev.get(scripcode_val)
3370
+ for key in mapping_keys(exchange_val):
3371
+ rev = self.exchange_mappings.get(key, {}).get("symbol_map_reversed", {})
3372
+ found = rev.get(code) or rev.get(scripcode_val)
3373
+ if found:
3374
+ return found
3375
+ return None
3321
3376
  except (TypeError, ValueError, KeyError):
3322
3377
  return None
3323
3378
 
@@ -2172,8 +2172,21 @@ def update_order_status(
2172
2172
  broker.redis_o.hset(broker_order_id, "quantity", str(fills.fill_size))
2173
2173
  broker.redis_o.hset(broker_order_id, "status", fills.status.name)
2174
2174
  broker.redis_o.hset(broker_order_id, "exch_order_id", fills.exchange_order_id)
2175
- else:
2175
+ elif fills.status in (OrderStatus.CANCELLED, OrderStatus.REJECTED, OrderStatus.FILLED):
2176
+ # Broker confirmed terminal zero-fill — safe to prune.
2176
2177
  delete_broker_order_id(broker, internal_order_id, broker_order_id)
2178
+ else:
2179
+ # UNDEFINED/OPEN/PENDING with fill_size=0 is often a failed/wrong-account
2180
+ # lookup; never delete Redis state in that case.
2181
+ trading_logger.log_warning(
2182
+ "EOD skip delete for ambiguous zero-fill order status",
2183
+ {
2184
+ "internal_order_id": internal_order_id,
2185
+ "broker_order_id": broker_order_id,
2186
+ "status": getattr(fills.status, "name", str(fills.status)),
2187
+ "fill_size": fills.fill_size,
2188
+ },
2189
+ )
2177
2190
  else:
2178
2191
  if fills.status == OrderStatus.HISTORICAL:
2179
2192
  return fills
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -170,6 +170,10 @@ The package requires:
170
170
  - Redis (for order tracking and logging)
171
171
  - Broker-specific SDKs (automatically installed via dependencies)
172
172
 
173
+ ## Adding a new broker
174
+
175
+ Step-by-step checklist (package, YAML, website, PnL/EOD, order manager): [`docs/ADDING_A_BROKER.md`](docs/ADDING_A_BROKER.md).
176
+
173
177
  ## Configuration
174
178
 
175
179
  ### Configuration File Structure
File without changes
File without changes