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.
- {tradingapi-0.4.0 → tradingapi-0.5.0}/PKG-INFO +5 -1
- {tradingapi-0.4.0 → tradingapi-0.5.0}/README.md +4 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/pyproject.toml +1 -1
- tradingapi-0.5.0/tests/test_flattrade_symbology.py +50 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/dhan.py +16 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/flattrade.py +179 -98
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/shoonya.py +75 -20
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/utils.py +14 -1
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/PKG-INFO +5 -1
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/SOURCES.txt +1 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/setup.cfg +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_broker_side_terminal_order.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_calculate_delta_realtime_quotes.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_find_option_with_delta.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_fivepaisa_historical_recovery.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_fivepaisa_stream_reconnect.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tests/test_market_hours_config.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/__init__.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/allocation.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/attribution.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/broker_base.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/config.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/fivepaisa.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/globals.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/ib.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/market_hours.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi/span.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.4.0 → tradingapi-0.5.0}/tradingapi.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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/
|
|
99
|
-
websocket="wss://piconnect.flattrade.in/
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
282
|
+
def process_nse_fno_row(row):
|
|
270
283
|
symbol = row["Symbol"]
|
|
271
|
-
|
|
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(
|
|
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-
|
|
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
|
|
338
|
+
def process_bse_fno_row(row):
|
|
320
339
|
symbol = row["Symbol"]
|
|
321
|
-
|
|
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(
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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(
|
|
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"],
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1913
|
-
if not
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
2768
|
-
if
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
3005
|
-
if
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
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
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|