tradingapi 0.4.0__tar.gz → 0.5.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 (39) hide show
  1. {tradingapi-0.4.0 → tradingapi-0.5.0}/PKG-INFO +5 -1
  2. {tradingapi-0.4.0 → tradingapi-0.5.0}/README.md +4 -0
  3. {tradingapi-0.4.0 → tradingapi-0.5.0}/pyproject.toml +1 -1
  4. tradingapi-0.5.0/tests/test_flattrade_symbology.py +50 -0
  5. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/dhan.py +16 -0
  6. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/flattrade.py +179 -98
  7. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/shoonya.py +75 -20
  8. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/utils.py +14 -1
  9. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/PKG-INFO +5 -1
  10. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/SOURCES.txt +1 -0
  11. {tradingapi-0.4.0 → tradingapi-0.5.0}/setup.cfg +0 -0
  12. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_broker_side_terminal_order.py +0 -0
  13. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_calculate_delta_realtime_quotes.py +0 -0
  14. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_find_option_with_delta.py +0 -0
  15. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_fivepaisa_historical_recovery.py +0 -0
  16. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_fivepaisa_stream_reconnect.py +0 -0
  17. {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_market_hours_config.py +0 -0
  18. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/__init__.py +0 -0
  19. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/allocation.py +0 -0
  20. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/attribution.py +0 -0
  21. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/broker_base.py +0 -0
  22. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config/commissions_20241216.yaml +0 -0
  23. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config/config_sample.yaml +0 -0
  24. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config.py +0 -0
  25. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/error_handling.py +0 -0
  26. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/exceptions.py +0 -0
  27. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/fivepaisa.py +0 -0
  28. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/globals.py +0 -0
  29. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/ib.py +0 -0
  30. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/icicidirect.py +0 -0
  31. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/icicidirect_generate_session.py +0 -0
  32. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/market_data_exchanges.py +0 -0
  33. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/market_hours.py +0 -0
  34. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/proxy_utils.py +0 -0
  35. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/span.py +0 -0
  36. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/dependency_links.txt +0 -0
  37. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/entry_points.txt +0 -0
  38. {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/requires.txt +0 -0
  39. {tradingapi-0.4.0 → tradingapi-0.5.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.4.0
3
+ Version: 0.5.0
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.5.0"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -0,0 +1,50 @@
1
+ import datetime as dt
2
+ import sys
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ sys.path.insert(0, "/home/psharma/onedrive/code/tradingapi2")
9
+
10
+ from tradingapi import flattrade
11
+
12
+
13
+ class FlatTradeSymbologyTest(unittest.TestCase):
14
+ def test_update_uses_bod_filename(self):
15
+ with tempfile.TemporaryDirectory() as folder:
16
+ symbols_path = Path(folder) / "20260902_symbols.csv"
17
+ symbols_path.write_text(
18
+ "long_symbol,LotSize,Scripcode,Exch,ExchType,TickSize,trading_symbol\n"
19
+ "NIFTY_IND___,1,26000,NSE,NSE,0.05,Nifty 50\n",
20
+ encoding="utf-8",
21
+ )
22
+ broker = object.__new__(flattrade.FlatTrade)
23
+ broker.account_key = "FLATTRADE"
24
+ with (
25
+ patch.object(
26
+ flattrade.config,
27
+ "get",
28
+ return_value=folder,
29
+ ),
30
+ patch.object(
31
+ flattrade,
32
+ "get_tradingapi_now",
33
+ return_value=dt.datetime(2026, 9, 2, 9, 57),
34
+ ),
35
+ patch.object(
36
+ flattrade,
37
+ "save_symbol_data",
38
+ side_effect=AssertionError("BOD file should be reused"),
39
+ ),
40
+ ):
41
+ broker.update_symbology()
42
+
43
+ self.assertEqual(
44
+ broker.exchange_mappings["NSE"]["symbol_map"]["NIFTY_IND___"],
45
+ 26000,
46
+ )
47
+
48
+
49
+ if __name__ == "__main__":
50
+ unittest.main()
@@ -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
 
@@ -110,6 +110,19 @@ def save_symbol_data(saveToFolder: bool = True):
110
110
  return lst[0]
111
111
 
112
112
  bhavcopyfolder = config.get("bhavcopy_folder")
113
+ symbol_columns = [
114
+ "long_symbol",
115
+ "LotSize",
116
+ "Scripcode",
117
+ "Exch",
118
+ "ExchType",
119
+ "TickSize",
120
+ "trading_symbol",
121
+ ]
122
+ codes_nse_cash = pd.DataFrame(columns=symbol_columns)
123
+ codes_bse_cash = pd.DataFrame(columns=symbol_columns)
124
+ codes_nse_fno = pd.DataFrame(columns=symbol_columns)
125
+ codes_bse_fno = pd.DataFrame(columns=symbol_columns)
113
126
  _proxies = None
114
127
  try:
115
128
  from .proxy_utils import get_proxies_for_broker
@@ -152,14 +165,14 @@ def save_symbol_data(saveToFolder: bool = True):
152
165
  codes = codes[(codes.Instrument.isin(["EQ", "BE", "XX", "BZ", "RR", "IV", "INDEX"]))]
153
166
  codes["long_symbol"] = None
154
167
 
155
- def process_row(row):
168
+ def process_nse_cash_row(row):
156
169
  symbol = row["Symbol"]
157
170
  if row["Instrument"] == "INDEX":
158
171
  return f"{symbol}_IND___".upper()
159
172
  else:
160
173
  return f"{symbol}_STK___".upper()
161
174
 
162
- codes["long_symbol"] = codes.apply(process_row, axis=1)
175
+ codes["long_symbol"] = codes.apply(process_nse_cash_row, axis=1)
163
176
  codes["Exch"] = "NSE"
164
177
  codes["ExchType"] = "CASH"
165
178
  new_column_names = {
@@ -206,14 +219,14 @@ def save_symbol_data(saveToFolder: bool = True):
206
219
  ]
207
220
  codes["long_symbol"] = None
208
221
 
209
- def process_row(row):
222
+ def process_bse_cash_row(row):
210
223
  symbol = row["Symbol"]
211
224
  if row["Instrument"] == "INDEX":
212
225
  return f"{symbol}_IND___".upper()
213
226
  else:
214
227
  return f"{symbol}_STK___".upper()
215
228
 
216
- codes["long_symbol"] = codes.apply(process_row, axis=1)
229
+ codes["long_symbol"] = codes.apply(process_bse_cash_row, axis=1)
217
230
  codes["Exch"] = "BSE"
218
231
  codes["ExchType"] = "CASH"
219
232
  new_column_names = {
@@ -266,14 +279,18 @@ def save_symbol_data(saveToFolder: bool = True):
266
279
  codes_fno["Expiry"], format="%d-%b-%Y", errors="coerce"
267
280
  ).dt.strftime("%Y%m%d")
268
281
 
269
- def process_row(row):
282
+ def process_nse_fno_row(row):
270
283
  symbol = row["Symbol"]
271
- if row["Instrument"].startswith("OPT"):
284
+ instrument = row["Instrument"]
285
+ if pd.isna(instrument) or pd.isna(symbol):
286
+ return None
287
+ if str(instrument).startswith("OPT"):
272
288
  return f"{symbol}_OPT_{row['Expiry']}_{'CALL' if row['OptionType']=='CE' else 'PUT'}_{row['StrikePrice']:g}".upper()
273
289
  else:
274
290
  return f"{symbol}_FUT_{row['Expiry']}__".upper()
275
291
 
276
- codes_fno["long_symbol"] = codes_fno.apply(process_row, axis=1)
292
+ codes_fno["long_symbol"] = codes_fno.apply(process_nse_fno_row, axis=1)
293
+ codes_fno = codes_fno.dropna(subset=["long_symbol"])
277
294
  codes_fno["Exch"] = "NFO"
278
295
  codes_fno["ExchType"] = "NFO"
279
296
  new_column_names = {
@@ -312,18 +329,24 @@ def save_symbol_data(saveToFolder: bool = True):
312
329
  codes_fno["Expiry"] = pd.to_datetime(
313
330
  codes_fno["Expiry"], format="%d-%b-%Y", errors="coerce"
314
331
  ).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})"
332
+ codes_fno[["Symbol", "ts_rest"]] = codes_fno["TradingSymbol"].str.extract(
333
+ r"^(SENSEX50|[A-Z0-9&-]*?[A-Z&-])(?=(?:\d{2}|FUT))(.*)$",
334
+ expand=True,
317
335
  )
336
+ codes_fno["Symbol"] = codes_fno["Symbol"].fillna(codes_fno["TradingSymbol"])
318
337
 
319
- def process_row(row):
338
+ def process_bse_fno_row(row):
320
339
  symbol = row["Symbol"]
321
- if row["Instrument"].startswith("OPT"):
340
+ instrument = row["Instrument"]
341
+ if pd.isna(instrument) or pd.isna(symbol):
342
+ return None
343
+ if str(instrument).startswith("OPT"):
322
344
  return f"{symbol}_OPT_{row['Expiry']}_{'CALL' if row['OptionType']=='CE' else 'PUT'}_{row['StrikePrice']:g}".upper()
323
345
  else:
324
346
  return f"{symbol}_FUT_{row['Expiry']}__".upper()
325
347
 
326
- codes_fno["long_symbol"] = codes_fno.apply(process_row, axis=1)
348
+ codes_fno["long_symbol"] = codes_fno.apply(process_bse_fno_row, axis=1)
349
+ codes_fno = codes_fno.dropna(subset=["long_symbol"])
327
350
  codes_fno["Exch"] = "BFO"
328
351
  codes_fno["ExchType"] = "BFO"
329
352
  new_column_names = {
@@ -365,6 +388,7 @@ class FlatTrade(BrokerBase):
365
388
  self.api = None
366
389
  self.subscribe_thread = None
367
390
  self.subscribed_symbols = []
391
+ self.subscribed_req_tokens = []
368
392
  self.socket_opened = False
369
393
  self._is_connected_cache_ttl_secs = 3.0
370
394
  self._last_is_connected_check_ts = 0.0
@@ -501,6 +525,20 @@ class FlatTrade(BrokerBase):
501
525
  """Verify if the current session is valid using existing is_connected method."""
502
526
  return self.is_connected()
503
527
 
528
+ def _apply_api_session(user, pwd, susertoken):
529
+ self.api = FlatTradeApiPy()
530
+ kwargs = {"userid": user, "password": pwd, "usertoken": susertoken}
531
+ try:
532
+ params = inspect.signature(self.api.set_session).parameters
533
+ except (TypeError, ValueError):
534
+ params = {}
535
+ if "accesstoken" in params:
536
+ kwargs["accesstoken"] = susertoken
537
+ self.api.set_session(**kwargs)
538
+ inject_oauth_header = getattr(self.api, "injectOAuthHeader", None)
539
+ if callable(inject_oauth_header):
540
+ inject_oauth_header(susertoken, user, user)
541
+
504
542
  def _restore_session_from_token(susertoken_path):
505
543
  """Attempt to restore session from existing token."""
506
544
  try:
@@ -515,8 +553,7 @@ class FlatTrade(BrokerBase):
515
553
  trading_logger.log_warning("Empty token file", {"broker": self.broker.name})
516
554
  return False
517
555
 
518
- self.api = FlatTradeApiPy()
519
- self.api.set_session(userid=user, password=pwd, usertoken=susertoken)
556
+ _apply_api_session(user, pwd, susertoken)
520
557
 
521
558
  # Verify the session is actually working
522
559
  if _verify_session(self):
@@ -557,14 +594,28 @@ class FlatTrade(BrokerBase):
557
594
  "Accept": "application/json",
558
595
  "Accept-Language": "en-US,en;q=0.5",
559
596
  "Host": "authapi.flattrade.in",
560
- "Origin": f"{HOST}",
561
- "Referer": f"{HOST}/",
597
+ "Origin": HOST,
598
+ "Referer": f"{HOST}/?app_key={api_key}",
562
599
  }
563
600
 
564
601
  def encode_item(item):
565
602
  encoded_item = hashlib.sha256(item.encode()).hexdigest()
566
603
  return encoded_item
567
604
 
605
+ def ftauth_payload(sid, override=""):
606
+ return {
607
+ "UserName": user,
608
+ "Password": encode_item(pwd),
609
+ "App": "",
610
+ "ClientID": "",
611
+ "Key": "",
612
+ "APIKey": api_key,
613
+ "PAN_DOB": pyotp.TOTP(token).now(),
614
+ "Sid": sid,
615
+ "Override": override,
616
+ "Version": "2",
617
+ }
618
+
568
619
  def get_authcode():
569
620
  try:
570
621
  with requests.Session() as session:
@@ -578,17 +629,7 @@ class FlatTrade(BrokerBase):
578
629
 
579
630
  response = session.post(
580
631
  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
- },
632
+ json=ftauth_payload(sid),
592
633
  )
593
634
 
594
635
  if response.status_code == 200:
@@ -600,17 +641,7 @@ class FlatTrade(BrokerBase):
600
641
  )
601
642
  response = session.post(
602
643
  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
- },
644
+ json=ftauth_payload(sid, "Y"),
614
645
  )
615
646
  if response.status_code == 200:
616
647
  response_data = response.json()
@@ -716,8 +747,7 @@ class FlatTrade(BrokerBase):
716
747
 
717
748
  # Initialize API
718
749
  try:
719
- self.api = FlatTradeApiPy()
720
- self.api.set_session(userid=user, password=pwd, usertoken=susertoken)
750
+ _apply_api_session(user, pwd, susertoken)
721
751
  trading_logger.log_info("API session established", {"user": user})
722
752
  except Exception as e:
723
753
  context = create_error_context(user=user, error=str(e))
@@ -1152,7 +1182,10 @@ class FlatTrade(BrokerBase):
1152
1182
  @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
1153
1183
  def update_symbology(self, **kwargs):
1154
1184
  dt_today = get_tradingapi_now().date()
1155
- symbols_path = os.path.join(config.get(f"{self.account_key}.SYMBOLCODES"), f"{dt_today}_symbols.csv")
1185
+ symbols_path = os.path.join(
1186
+ config.get(f"{self.account_key}.SYMBOLCODES"),
1187
+ f"{dt_today:%Y%m%d}_symbols.csv",
1188
+ )
1156
1189
  if not os.path.exists(symbols_path):
1157
1190
  codes = save_symbol_data(saveToFolder=False)
1158
1191
  codes = codes.dropna(subset=["long_symbol"])
@@ -1165,13 +1198,17 @@ class FlatTrade(BrokerBase):
1165
1198
  # Iterate through the data frame and create mappings based on exchange
1166
1199
 
1167
1200
  for exchange, group in codes.groupby("Exch"):
1201
+ scrip_int = pd.to_numeric(group["Scripcode"], errors="coerce")
1202
+ scrip_int = scrip_int.apply(lambda x: int(x) if pd.notna(x) else None)
1168
1203
  self.exchange_mappings[exchange] = {
1169
- "symbol_map": dict(zip(group["long_symbol"], group["Scripcode"])),
1204
+ "symbol_map": dict(zip(group["long_symbol"], scrip_int)),
1170
1205
  "contractsize_map": dict(zip(group["long_symbol"], group["LotSize"])),
1171
1206
  "exchange_map": dict(zip(group["long_symbol"], group["Exch"])),
1172
1207
  "exchangetype_map": dict(zip(group["long_symbol"], group["ExchType"])),
1173
1208
  "contracttick_map": dict(zip(group["long_symbol"], group["TickSize"])),
1174
- "symbol_map_reversed": dict(zip(group["Scripcode"], group["long_symbol"])),
1209
+ "symbol_map_reversed": {
1210
+ code: name for code, name in zip(scrip_int, group["long_symbol"]) if code is not None
1211
+ },
1175
1212
  "tradingsymbol_map": dict(zip(group["long_symbol"], group["trading_symbol"])),
1176
1213
  }
1177
1214
  return codes
@@ -1379,6 +1416,7 @@ class FlatTrade(BrokerBase):
1379
1416
  order.message = "No broker order ID in response"
1380
1417
  return order
1381
1418
 
1419
+ fills = None
1382
1420
  try:
1383
1421
  fills = self.get_order_info(broker_order_id=order.broker_order_id)
1384
1422
  order.exch_order_id = fills.exchange_order_id
@@ -1397,9 +1435,8 @@ class FlatTrade(BrokerBase):
1397
1435
  "Error getting order history", e, {"broker_order_id": order.broker_order_id}
1398
1436
  )
1399
1437
 
1400
- if order.price == 0:
1401
- if fills.fill_price > 0 and order.price == 0:
1402
- order.price = fills.fill_price
1438
+ if order.price == 0 and fills is not None and fills.fill_price > 0:
1439
+ order.price = fills.fill_price
1403
1440
 
1404
1441
  trading_logger.log_info("Placed Order", {"order": str(order)})
1405
1442
  else:
@@ -1698,13 +1735,13 @@ class FlatTrade(BrokerBase):
1698
1735
  broker=self.broker,
1699
1736
  )
1700
1737
 
1738
+ valid_date = None
1701
1739
  try:
1702
1740
  valid_date = parse_datetime(order.remote_order_id[:8])
1703
- date_valid = True
1704
1741
  except (ValueError, TypeError):
1705
- date_valid = False
1742
+ pass
1706
1743
  # Compare as string: valid_date is datetime from parse_datetime; only use return_db_as_fills for past days
1707
- if date_valid and (
1744
+ if valid_date is not None and (
1708
1745
  valid_date.strftime("%Y-%m-%d") != dt.datetime.today().strftime("%Y-%m-%d")
1709
1746
  or (order.remote_order_id != "" and order.broker != self.broker)
1710
1747
  ):
@@ -1904,16 +1941,14 @@ class FlatTrade(BrokerBase):
1904
1941
  if refresh_mapping:
1905
1942
  try:
1906
1943
  # Parse date_end to get YYYYMMDD format
1944
+ date_end_for_mapping = None
1907
1945
  try:
1908
- date_end_dt = parse_datetime(date_end)
1909
- date_end_str = date_end_dt.strftime("%Y-%m-%d")
1910
- date_end_valid = True
1946
+ date_end_for_mapping = parse_datetime(date_end)
1911
1947
  except (ValueError, TypeError):
1912
- date_end_valid = False
1913
- if not date_end_valid:
1948
+ pass
1949
+ if not isinstance(date_end_for_mapping, (dt.datetime, dt.date)):
1914
1950
  raise ValueError(f"Invalid date_end format: {date_end}")
1915
- date_end_obj = dt.datetime.strptime(str(date_end_str), "%Y-%m-%d")
1916
- date_end_yyyymmdd = date_end_obj.strftime("%Y%m%d")
1951
+ date_end_yyyymmdd = date_end_for_mapping.strftime("%Y%m%d")
1917
1952
 
1918
1953
  # Get symbol codes path from config
1919
1954
  symbol_codes_path = config.get(f"{self.account_key}.SYMBOLCODES")
@@ -2010,16 +2045,12 @@ class FlatTrade(BrokerBase):
2010
2045
  # once NSENIFTY is amended to NIFTY in databae, we can remove this line.
2011
2046
 
2012
2047
  # Parse date_start (accepts datetime, date, or string) -> datetime for API
2048
+ date_start_parsed = None
2013
2049
  try:
2014
2050
  date_start_parsed = parse_datetime(date_start)
2015
- date_start_valid = True
2016
2051
  except (ValueError, TypeError):
2017
- date_start_valid = False
2018
- if (
2019
- not date_start_valid
2020
- or date_start_parsed is None
2021
- or not isinstance(date_start_parsed, (dt.datetime, dt.date))
2022
- ):
2052
+ pass
2053
+ if not isinstance(date_start_parsed, (dt.datetime, dt.date)):
2023
2054
  raise ValueError(f"Invalid date_start format: {date_start}")
2024
2055
  date_start_dt = dt.datetime.strptime(
2025
2056
  date_start_parsed.strftime("%Y-%m-%d") + " " + resolved_market_open_time,
@@ -2027,16 +2058,12 @@ class FlatTrade(BrokerBase):
2027
2058
  )
2028
2059
 
2029
2060
  # Parse date_end (accepts datetime, date, or string) -> datetime for API
2061
+ date_end_parsed = None
2030
2062
  try:
2031
2063
  date_end_parsed = parse_datetime(date_end)
2032
- date_end_valid = True
2033
2064
  except (ValueError, TypeError):
2034
- date_end_valid = False
2035
- if (
2036
- not date_end_valid
2037
- or date_end_parsed is None
2038
- or not isinstance(date_end_parsed, (dt.datetime, dt.date))
2039
- ):
2065
+ pass
2066
+ if not isinstance(date_end_parsed, (dt.datetime, dt.date)):
2040
2067
  raise ValueError(f"Invalid date_end format: {date_end}")
2041
2068
  date_end_dt = dt.datetime.strptime(
2042
2069
  date_end_parsed.strftime("%Y-%m-%d") + " " + resolved_market_close_time,
@@ -2352,12 +2379,12 @@ class FlatTrade(BrokerBase):
2352
2379
  if self.api is None:
2353
2380
  raise BrokerConnectionError("FlatTrade API is not initialized")
2354
2381
 
2382
+ market_feed = Price()
2383
+ market_feed.src = "sh"
2384
+ market_feed.symbol = long_symbol
2355
2385
  try:
2356
2386
  trading_logger.log_debug("Fetching quote", {"long_symbol": long_symbol, "exchange": exchange})
2357
2387
  mapped_exchange = self.map_exchange_for_api(long_symbol, exchange)
2358
- market_feed = Price() # Initialize with default values
2359
- market_feed.src = "sh"
2360
- market_feed.symbol = long_symbol
2361
2388
 
2362
2389
  # Validate exchange mapping exists
2363
2390
  if mapped_exchange not in self.exchange_mappings:
@@ -2647,7 +2674,9 @@ class FlatTrade(BrokerBase):
2647
2674
  list(self.subscribed_symbols) if hasattr(self, "subscribed_symbols") else []
2648
2675
  )
2649
2676
  if active_symbols:
2650
- reconnect_req_list = expand_symbols_to_request(active_symbols)
2677
+ reconnect_req_list = list(self.subscribed_req_tokens) or expand_symbols_to_request(
2678
+ active_symbols
2679
+ )
2651
2680
  trading_logger.log_info(
2652
2681
  "Resubscribing to symbols after reconnection",
2653
2682
  {"symbols_count": len(active_symbols), "req_list": reconnect_req_list},
@@ -2700,11 +2729,20 @@ class FlatTrade(BrokerBase):
2700
2729
  time.sleep(1)
2701
2730
 
2702
2731
  def resolve_exchange_from_symbology(long_symbol: str):
2703
- """Resolve API exchange for a symbol from symbology (which exchange's symbol_map contains it)."""
2732
+ """Prefer the caller's exchange so dual-listed names (e.g. INFY) stay on NSE vs BSE."""
2733
+ preferred = None
2734
+ try:
2735
+ preferred = self.map_exchange_for_api(long_symbol, exchange)
2736
+ except Exception:
2737
+ preferred = mapped_exchange
2738
+ if preferred:
2739
+ symbol_map = self.exchange_mappings.get(preferred, {}).get("symbol_map", {})
2740
+ if long_symbol in symbol_map:
2741
+ return preferred
2704
2742
  for exch in self.exchange_mappings:
2705
2743
  if long_symbol in self.exchange_mappings[exch]["symbol_map"]:
2706
2744
  return exch
2707
- return None
2745
+ return preferred
2708
2746
 
2709
2747
  # Function to expand symbols into request format
2710
2748
  def expand_symbols_to_request(symbol_list):
@@ -2745,34 +2783,32 @@ class FlatTrade(BrokerBase):
2745
2783
  return req_list
2746
2784
 
2747
2785
  # Function to update the subscription list
2748
- def update_subscription_list(operation, symbols):
2786
+ def update_subscription_list(operation, symbols, tokens):
2749
2787
  if operation == "s":
2750
2788
  self.subscribed_symbols = list(set(self.subscribed_symbols + symbols))
2789
+ self.subscribed_req_tokens = list(set(self.subscribed_req_tokens + tokens))
2751
2790
  elif operation == "u":
2752
2791
  self.subscribed_symbols = list(set(self.subscribed_symbols) - set(symbols))
2792
+ self.subscribed_req_tokens = [t for t in self.subscribed_req_tokens if t not in tokens]
2753
2793
 
2754
- # Update subscriptions and request list
2755
- update_subscription_list(operation, symbols)
2756
2794
  req_list = expand_symbols_to_request(symbols)
2795
+ update_subscription_list(operation, symbols, req_list)
2757
2796
 
2758
- # Start the WebSocket connection if not already started
2759
2797
  if self.subscribe_thread is None:
2760
2798
  self.subscribe_thread = threading.Thread(target=connect_and_subscribe, name="MarketDataStreamer")
2761
2799
  self.subscribe_thread.start()
2762
2800
 
2763
- # Wait until the socket is opened before subscribing/unsubscribing
2764
2801
  while not self.socket_opened:
2765
2802
  time.sleep(1)
2766
2803
 
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)
2804
+ if req_list:
2805
+ if operation == "s":
2806
+ trading_logger.log_info("Requesting streaming", {"req_list": req_list})
2807
+ self._wait_for_stream_request_rate_limit()
2808
+ self.api.subscribe(req_list)
2809
+ elif operation == "u":
2810
+ trading_logger.log_info("Unsubscribing streaming", {"req_list": req_list})
2811
+ self.api.unsubscribe(req_list)
2776
2812
  except Exception as e:
2777
2813
  trading_logger.log_error(
2778
2814
  "Unexpected error in start_quotes_streaming",
@@ -2780,6 +2816,25 @@ class FlatTrade(BrokerBase):
2780
2816
  {"operation": operation, "symbols_count": len(symbols) if symbols else 0, "exchange": exchange},
2781
2817
  )
2782
2818
 
2819
+ @log_execution_time
2820
+ @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
2821
+ def stop_streaming(self):
2822
+ try:
2823
+ trading_logger.log_info("Stopping quotes streaming")
2824
+ if hasattr(self, "api") and self.api and getattr(self, "socket_opened", False):
2825
+ try:
2826
+ self.api.close_websocket()
2827
+ except Exception as e:
2828
+ trading_logger.log_warning("Failed to close WebSocket", {"error": str(e)})
2829
+ self.socket_opened = False
2830
+ self.subscribe_thread = None
2831
+ self.subscribed_symbols = []
2832
+ self.subscribed_req_tokens = []
2833
+ trading_logger.log_info("Streaming stopped successfully")
2834
+ except Exception as e:
2835
+ context = create_error_context(error=str(e))
2836
+ raise BrokerConnectionError(f"Failed to stop streaming: {str(e)}", context)
2837
+
2783
2838
  @log_execution_time
2784
2839
  @validate_inputs(long_symbol=lambda x: x is None or (isinstance(x, str) and len(x.strip()) >= 0))
2785
2840
  @retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
@@ -2802,7 +2857,8 @@ class FlatTrade(BrokerBase):
2802
2857
  holding = pd.DataFrame([raw_holding])
2803
2858
  else:
2804
2859
  holding = pd.DataFrame(columns=["long_symbol", "quantity"])
2805
-
2860
+ if len(holding) == 0:
2861
+ holding = pd.DataFrame(columns=["long_symbol", "quantity"]).astype({"quantity": float})
2806
2862
  if len(holding) > 0:
2807
2863
  try:
2808
2864
  if "exch" not in holding.columns and "exch_tsym" not in holding.columns:
@@ -3056,14 +3112,29 @@ class FlatTrade(BrokerBase):
3056
3112
  {"row_count": len(scripcode)},
3057
3113
  )
3058
3114
 
3115
+ def mapping_keys(exchange_val):
3116
+ raw = "" if exchange_val is None or (isinstance(exchange_val, float) and pd.isna(exchange_val)) else str(exchange_val).strip()
3117
+ if raw in self.exchange_mappings:
3118
+ return [raw]
3119
+ up = raw.upper()
3120
+ if up in self.exchange_mappings:
3121
+ return [up]
3122
+ if not up:
3123
+ return list(self.exchange_mappings.keys())
3124
+ keyed = [k for k in self.exchange_mappings if str(k).upper().startswith(up[0])]
3125
+ return keyed if keyed else list(self.exchange_mappings.keys())
3126
+
3059
3127
  def lookup(scripcode_val, exchange_val):
3060
3128
  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
3129
+ code = int(float(scripcode_val)) if scripcode_val is not None and not pd.isna(scripcode_val) else None
3064
3130
  if code is None:
3065
3131
  return None
3066
- return rev.get(code) or rev.get(scripcode_val)
3132
+ for key in mapping_keys(exchange_val):
3133
+ rev = self.exchange_mappings.get(key, {}).get("symbol_map_reversed", {})
3134
+ found = rev.get(code) or rev.get(scripcode_val)
3135
+ if found:
3136
+ return found
3137
+ return None
3067
3138
  except (TypeError, ValueError, KeyError):
3068
3139
  return None
3069
3140
 
@@ -3114,9 +3185,19 @@ class FlatTrade(BrokerBase):
3114
3185
  )
3115
3186
  raise SymbolError(f"Exchange mapping not found: {str(e)}", context)
3116
3187
 
3188
+ lot_from_map = self.exchange_mappings[exchange]["contractsize_map"].get(long_symbol)
3189
+ if lot_from_map is not None and pd.notna(lot_from_map):
3190
+ return int(lot_from_map)
3191
+
3117
3192
  if code is not None:
3118
3193
  try:
3119
- series = cast(pd.Series, self.codes.loc[self.codes.Scripcode == code, "LotSize"])
3194
+ series = cast(
3195
+ pd.Series,
3196
+ self.codes.loc[
3197
+ (self.codes.Scripcode == code) & (self.codes.Exch == exchange),
3198
+ "LotSize",
3199
+ ],
3200
+ )
3120
3201
  if len(series) > 0:
3121
3202
  lot_size = int(series.iloc[0])
3122
3203
  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.5.0
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
@@ -5,6 +5,7 @@ tests/test_calculate_delta_realtime_quotes.py
5
5
  tests/test_find_option_with_delta.py
6
6
  tests/test_fivepaisa_historical_recovery.py
7
7
  tests/test_fivepaisa_stream_reconnect.py
8
+ tests/test_flattrade_symbology.py
8
9
  tests/test_market_hours_config.py
9
10
  tradingapi/__init__.py
10
11
  tradingapi/allocation.py
File without changes
File without changes