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.
- {tradingapi-0.4.0 → tradingapi-0.4.1}/PKG-INFO +5 -1
- {tradingapi-0.4.0 → tradingapi-0.4.1}/README.md +4 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/pyproject.toml +1 -1
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/dhan.py +16 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/flattrade.py +133 -59
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/shoonya.py +75 -20
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/utils.py +14 -1
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/PKG-INFO +5 -1
- {tradingapi-0.4.0 → tradingapi-0.4.1}/setup.cfg +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_broker_side_terminal_order.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_calculate_delta_realtime_quotes.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_find_option_with_delta.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_fivepaisa_historical_recovery.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_fivepaisa_stream_reconnect.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tests/test_market_hours_config.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/__init__.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/allocation.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/attribution.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/broker_base.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/config.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/fivepaisa.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/globals.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/ib.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/market_hours.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi/span.py +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/SOURCES.txt +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.4.0 → tradingapi-0.4.1}/tradingapi.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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"],
|
|
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":
|
|
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(
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
2768
|
-
if
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.4.
|
|
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
|
|
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
|