tradingapi 0.2.1__tar.gz → 0.3.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.2.1 → tradingapi-0.3.1}/PKG-INFO +1 -1
- {tradingapi-0.2.1 → tradingapi-0.3.1}/pyproject.toml +1 -1
- tradingapi-0.3.1/tests/test_find_option_with_delta.py +122 -0
- tradingapi-0.3.1/tradingapi/allocation.py +163 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/broker_base.py +1 -1
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/config.py +16 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/dhan.py +102 -51
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/fivepaisa.py +184 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/flattrade.py +82 -1
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/shoonya.py +100 -16
- tradingapi-0.3.1/tradingapi/span.py +837 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/utils.py +324 -135
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/PKG-INFO +1 -1
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/SOURCES.txt +3 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/README.md +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/setup.cfg +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/__init__.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/attribution.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/globals.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.2.1 → tradingapi-0.3.1}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Standalone test for find_option_with_delta NaN-robustness fix.
|
|
2
|
+
|
|
3
|
+
Run: /home/psharma/envs/base/bin/python3 tradingapi2/tests/test_find_option_with_delta.py
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
# Force import from source repo, not site-packages installed copy.
|
|
10
|
+
sys.path.insert(0, "/home/psharma/onedrive/code/tradingapi2")
|
|
11
|
+
|
|
12
|
+
from tradingapi import utils
|
|
13
|
+
|
|
14
|
+
assert utils.__file__.startswith("/home/psharma/onedrive/code/tradingapi2/"), (
|
|
15
|
+
f"Wrong tradingapi loaded: {utils.__file__}"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_chain(strikes, expiry="20260529", opt_type="CALL", underlying="NIFTY"):
|
|
20
|
+
# Symbol format expected by _strike() and calculate_delta: "<u>_<x>_<expiry>_<type>_<strike>_..."
|
|
21
|
+
return [f"{underlying}_X_{expiry}_{opt_type}_{int(s) if s == int(s) else s}_X" for s in strikes]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bs_like_delta(strike, spot, opt_type="CALL"):
|
|
25
|
+
"""Smooth monotone proxy for |delta| as a function of strike. Just needs to be monotone."""
|
|
26
|
+
# CALL delta ~ N(d1); approximate with logistic in (spot - strike)
|
|
27
|
+
x = (spot - strike) / (spot * 0.05)
|
|
28
|
+
sig = 1.0 / (1.0 + math.exp(-x))
|
|
29
|
+
return sig if opt_type == "CALL" else -(1.0 - sig)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_case(name, chain, price_f, target_delta, nan_strikes=(), return_lower=True, expect_index=True):
|
|
33
|
+
nan_set = set(nan_strikes)
|
|
34
|
+
call_log = []
|
|
35
|
+
|
|
36
|
+
def fake_calc_delta(brokers, sym, pf, *a, **kw):
|
|
37
|
+
strike = float(sym.split("_")[4])
|
|
38
|
+
call_log.append(strike)
|
|
39
|
+
if strike in nan_set:
|
|
40
|
+
return float("nan")
|
|
41
|
+
return bs_like_delta(strike, pf, opt_type=sym.split("_")[3])
|
|
42
|
+
|
|
43
|
+
with patch.object(utils, "calculate_delta", side_effect=fake_calc_delta):
|
|
44
|
+
idx = utils.find_option_with_delta(
|
|
45
|
+
brokers=[],
|
|
46
|
+
price_f=price_f,
|
|
47
|
+
option_chain=chain,
|
|
48
|
+
target_delta=target_delta,
|
|
49
|
+
return_lower_delta=return_lower,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
found = idx >= 0
|
|
53
|
+
status = "PASS" if found == expect_index else "FAIL"
|
|
54
|
+
chosen_strike = float(chain[idx].split("_")[4]) if found else None
|
|
55
|
+
chosen_delta = bs_like_delta(chosen_strike, price_f) if chosen_strike is not None else None
|
|
56
|
+
delta_str = f"{abs(chosen_delta):.4f}" if chosen_delta is not None else "n/a"
|
|
57
|
+
print(
|
|
58
|
+
f"[{status}] {name}: idx={idx} strike={chosen_strike} |delta|≈{delta_str} "
|
|
59
|
+
f"calls={len(call_log)} unique={len(set(call_log))} target={target_delta}"
|
|
60
|
+
)
|
|
61
|
+
return status == "PASS", idx, len(call_log)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
results = []
|
|
66
|
+
|
|
67
|
+
# Case 1 — clean chain, no NaN. Should land near target_delta=0.3 and be efficient.
|
|
68
|
+
strikes = [64900 + 100 * i for i in range(215)] # 64900..86300
|
|
69
|
+
chain = make_chain(strikes)
|
|
70
|
+
ok, idx, calls = run_case("clean chain, target=0.3", chain, 76956.7, 0.3)
|
|
71
|
+
results.append(ok)
|
|
72
|
+
assert calls < 30, f"clean chain should be cheap, got {calls} calls"
|
|
73
|
+
|
|
74
|
+
# Case 2 — exact repro of warning: NaN at deep ITM call strikes.
|
|
75
|
+
nan_strikes = [64900.0, 65100.0, 65400.0, 66100.0, 67500.0]
|
|
76
|
+
ok, idx, calls = run_case(
|
|
77
|
+
"warning repro: 5 NaN deep-ITM, target=0.3", chain, 76956.7, 0.3, nan_strikes=nan_strikes
|
|
78
|
+
)
|
|
79
|
+
results.append(ok)
|
|
80
|
+
|
|
81
|
+
# Case 3 — NaN cluster centered on mid (chain mid ~ index 107, strike ~75600).
|
|
82
|
+
mid_nans = [strikes[i] for i in range(100, 115)]
|
|
83
|
+
ok, _, _ = run_case("NaN cluster around mid", chain, 76956.7, 0.3, nan_strikes=mid_nans)
|
|
84
|
+
results.append(ok)
|
|
85
|
+
|
|
86
|
+
# Case 4 — NaN around the seeding fractions (0.1, 0.25, 0.5, 0.75, 0.9).
|
|
87
|
+
seed_idxs = [int(round(f * 214)) for f in [0.1, 0.25, 0.5, 0.75]] # leave 0.9 valid
|
|
88
|
+
seed_nans = [strikes[i] for i in seed_idxs]
|
|
89
|
+
ok, _, _ = run_case("NaN at most seed fractions", chain, 76956.7, 0.3, nan_strikes=seed_nans)
|
|
90
|
+
results.append(ok)
|
|
91
|
+
|
|
92
|
+
# Case 5 — all but two strikes NaN; should still find via linear fallback.
|
|
93
|
+
valid_strikes = {strikes[50], strikes[160]}
|
|
94
|
+
all_nan = [s for s in strikes if s not in valid_strikes]
|
|
95
|
+
ok, _, _ = run_case(
|
|
96
|
+
"only 2 valid strikes (linear fallback)", chain, 76956.7, 0.3, nan_strikes=all_nan
|
|
97
|
+
)
|
|
98
|
+
results.append(ok)
|
|
99
|
+
|
|
100
|
+
# Case 6 — entire chain NaN: must return -1 with warning, not crash.
|
|
101
|
+
ok, idx, _ = run_case(
|
|
102
|
+
"all NaN -> expect -1", chain, 76956.7, 0.3, nan_strikes=strikes, expect_index=False
|
|
103
|
+
)
|
|
104
|
+
results.append(ok)
|
|
105
|
+
|
|
106
|
+
# Case 7 — PUT chain (decreasing |delta| with strike).
|
|
107
|
+
put_chain = make_chain(strikes, opt_type="PUT")
|
|
108
|
+
ok, _, _ = run_case("PUT chain target=0.3", put_chain, 76956.7, 0.3)
|
|
109
|
+
results.append(ok)
|
|
110
|
+
|
|
111
|
+
# Case 8 — empty chain.
|
|
112
|
+
ok, idx, _ = run_case("empty chain -> -1", [], 76956.7, 0.3, expect_index=False)
|
|
113
|
+
results.append(ok)
|
|
114
|
+
|
|
115
|
+
passed = sum(results)
|
|
116
|
+
total = len(results)
|
|
117
|
+
print(f"\n{passed}/{total} passed")
|
|
118
|
+
sys.exit(0 if passed == total else 1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Allocation helpers for budget-based position sizing.
|
|
3
|
+
|
|
4
|
+
Strategies read their daily pct from ~/onedrive/.tradingapi/allocations/master_allocation_today.yaml,
|
|
5
|
+
then call these helpers at each entry decision to derive a live per-entry target margin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
_ALLOCATIONS_DIR = Path(os.path.expanduser("~/onedrive/.tradingapi/allocations"))
|
|
21
|
+
_TODAY_SYMLINK = _ALLOCATIONS_DIR / "master_allocation_today.yaml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_today_allocation(broker_name: str, strategy_name: str) -> float:
|
|
25
|
+
"""Return the pct (0.0–1.0) allocated to strategy_name on broker_name today.
|
|
26
|
+
|
|
27
|
+
Reads master_allocation_today.yaml (a symlink to today's dated file).
|
|
28
|
+
Raises FileNotFoundError if the symlink/file is missing.
|
|
29
|
+
Raises ValueError if the date in the file does not match today.
|
|
30
|
+
Returns 0.0 if the strategy is listed with pct == 0 (inactive today).
|
|
31
|
+
Raises KeyError if the broker/strategy is not found in the file at all.
|
|
32
|
+
"""
|
|
33
|
+
if not _TODAY_SYMLINK.exists():
|
|
34
|
+
raise FileNotFoundError(
|
|
35
|
+
f"Master allocation file not found: {_TODAY_SYMLINK}. "
|
|
36
|
+
"Run generate_master_allocation.py before starting strategies."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
with open(_TODAY_SYMLINK) as f:
|
|
40
|
+
data = yaml.safe_load(f)
|
|
41
|
+
|
|
42
|
+
file_date = str(data.get("date", "")).strip()
|
|
43
|
+
today_str = dt.date.today().isoformat()
|
|
44
|
+
if file_date != today_str:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Master allocation file is stale: file date={file_date!r}, today={today_str!r}. "
|
|
47
|
+
"Regenerate with generate_master_allocation.py."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
broker_key = broker_name.strip().upper()
|
|
51
|
+
brokers = data.get("brokers", {})
|
|
52
|
+
if broker_key not in brokers:
|
|
53
|
+
raise KeyError(
|
|
54
|
+
f"Broker {broker_key!r} not found in master allocation. "
|
|
55
|
+
f"Available brokers: {list(brokers.keys())}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
strategies = brokers[broker_key].get("strategies", {})
|
|
59
|
+
strategy_key = strategy_name.strip().upper()
|
|
60
|
+
if strategy_key not in strategies:
|
|
61
|
+
raise KeyError(
|
|
62
|
+
f"Strategy {strategy_key!r} not found under broker {broker_key!r} in master allocation. "
|
|
63
|
+
f"Available strategies: {list(strategies.keys())}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
entry = strategies[strategy_key]
|
|
67
|
+
return float(entry.get("pct", 0.0))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def broker_capital(broker) -> float:
|
|
71
|
+
"""Fetch live total capital from broker: cash + collateral."""
|
|
72
|
+
caps = broker.get_available_capital()
|
|
73
|
+
return float(caps.get("cash", 0.0)) + float(caps.get("collateral", 0.0))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def current_utilisation(broker, strategy_name: str) -> float:
|
|
77
|
+
"""Sum of margin currently consumed by open positions of this strategy.
|
|
78
|
+
|
|
79
|
+
Calls broker.get_margin_requirement() for each open position (net=False).
|
|
80
|
+
Exchange is read from Redis per position; defaults to NSE if missing.
|
|
81
|
+
"""
|
|
82
|
+
from tradingapi.utils import get_pnl_table, hget_with_default
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
pnl = get_pnl_table(broker, strategy_name, refresh_status=True)
|
|
86
|
+
except Exception:
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
if pnl.empty:
|
|
90
|
+
return 0.0
|
|
91
|
+
|
|
92
|
+
open_mask = (pnl["entry_quantity"].fillna(0) + pnl["exit_quantity"].fillna(0)) != 0
|
|
93
|
+
exit_empty = pnl["exit_time"].fillna("").astype(str).str.strip().isin(["", "0", "NaT"])
|
|
94
|
+
open_pnl = pnl.loc[open_mask | exit_empty]
|
|
95
|
+
|
|
96
|
+
total = 0.0
|
|
97
|
+
for _, row in open_pnl.iterrows():
|
|
98
|
+
combo_symbol = str(row.get("symbol", "")).strip()
|
|
99
|
+
if not combo_symbol:
|
|
100
|
+
continue
|
|
101
|
+
net_qty = float(row.get("entry_quantity", 0) or 0) + float(row.get("exit_quantity", 0) or 0)
|
|
102
|
+
if net_qty == 0:
|
|
103
|
+
continue
|
|
104
|
+
# Signed: positive = long, negative = short.
|
|
105
|
+
# parse_combo_symbol handles both simple and combo symbols uniformly.
|
|
106
|
+
order_size = int(net_qty)
|
|
107
|
+
|
|
108
|
+
entry_keys_str = str(row.get("entry_keys", "")).strip()
|
|
109
|
+
first_key = entry_keys_str.split()[0] if entry_keys_str else ""
|
|
110
|
+
exchange = hget_with_default(broker, first_key, "exchange", "NSE") if first_key else "NSE"
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
margin = broker.get_margin_requirement(combo_symbol, order_size, exchange)
|
|
114
|
+
if margin is not None:
|
|
115
|
+
total += float(margin)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
return total
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def remaining_budget(budget: float, utilised: float) -> float:
|
|
122
|
+
return max(0.0, budget - utilised)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _remaining_factor_sum(open_positions: int, max_positions: int, decay: float, min_frac: float) -> float:
|
|
126
|
+
"""Sum of decay fractions for future legs [open_positions, max_positions)."""
|
|
127
|
+
return float(sum(
|
|
128
|
+
max(min_frac, decay ** k)
|
|
129
|
+
for k in range(open_positions, max_positions)
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def scalping_entry_target(
|
|
134
|
+
remaining: float,
|
|
135
|
+
open_positions: int,
|
|
136
|
+
max_positions: int,
|
|
137
|
+
decay: float,
|
|
138
|
+
min_frac: float,
|
|
139
|
+
) -> float:
|
|
140
|
+
"""Target margin for the next scalping entry given remaining budget.
|
|
141
|
+
|
|
142
|
+
Entry 1 (0 open): remaining / sum([1.0, 0.75, 0.5625]) ≈ remaining / 2.3125
|
|
143
|
+
Entry 2 (1 open): remaining / sum([0.75, 0.5625]) ≈ remaining / 1.3125
|
|
144
|
+
Entry 3 (2 open): remaining / 0.5625 → clamped to remaining (last leg gets all that's left)
|
|
145
|
+
"""
|
|
146
|
+
if open_positions >= max_positions:
|
|
147
|
+
return 0.0
|
|
148
|
+
factor_sum = _remaining_factor_sum(open_positions, max_positions, decay, min_frac)
|
|
149
|
+
if factor_sum <= 0:
|
|
150
|
+
return remaining
|
|
151
|
+
raw = remaining / factor_sum
|
|
152
|
+
return min(raw, remaining)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def strangle_entry_target(budget_now: float, remaining: float, scheduled_entries: int) -> float:
|
|
156
|
+
"""Target margin for the next strangle entry.
|
|
157
|
+
|
|
158
|
+
Equal weighting across scheduled entries, clamped to what's actually left.
|
|
159
|
+
"""
|
|
160
|
+
if scheduled_entries <= 0:
|
|
161
|
+
return remaining
|
|
162
|
+
equal_share = budget_now / scheduled_entries
|
|
163
|
+
return min(equal_share, remaining)
|
|
@@ -1076,7 +1076,7 @@ class BrokerBase(ABC):
|
|
|
1076
1076
|
pass
|
|
1077
1077
|
|
|
1078
1078
|
def get_margin_requirement(
|
|
1079
|
-
self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds: Optional[str] = None
|
|
1079
|
+
self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds: Optional[str] = None, net: bool = False
|
|
1080
1080
|
) -> Optional[float]:
|
|
1081
1081
|
"""Default broker margin hook. Brokers can override if supported."""
|
|
1082
1082
|
_get_trading_logger().log_warning(
|
|
@@ -541,6 +541,22 @@ def get_config() -> Config:
|
|
|
541
541
|
return cast(Config, _config_instance)
|
|
542
542
|
|
|
543
543
|
|
|
544
|
+
def get_fno_freeze_limit(broker_name: str, underlying: str) -> Optional[int]:
|
|
545
|
+
"""Return max contracts per FNO order for (broker, underlying).
|
|
546
|
+
|
|
547
|
+
Values in tradingapi.yaml are total contracts (= lots × lot_size).
|
|
548
|
+
Returns None if no limit is configured for the broker/underlying.
|
|
549
|
+
"""
|
|
550
|
+
try:
|
|
551
|
+
cfg = get_config()
|
|
552
|
+
freeze_cfg = cfg.get("freeze_limits") or {}
|
|
553
|
+
broker_map = (freeze_cfg.get("brokers") or {}).get(str(broker_name).strip().upper(), {})
|
|
554
|
+
val = broker_map.get(str(underlying).strip().upper())
|
|
555
|
+
return int(val) if val is not None else None
|
|
556
|
+
except Exception:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
|
|
544
560
|
# Example usage with enhanced error handling
|
|
545
561
|
if __name__ == "__main__":
|
|
546
562
|
try:
|
|
@@ -402,6 +402,10 @@ class Dhan(BrokerBase):
|
|
|
402
402
|
self._global_market_data_rate_limit_lock_key = "dhan:market_data:lock"
|
|
403
403
|
self._historical_audit_redis = None
|
|
404
404
|
self._historical_audit_redis_db = 1
|
|
405
|
+
self._audit_enabled_cache: bool = False
|
|
406
|
+
self._audit_enabled_cache_ts: float = 0.0
|
|
407
|
+
self._audit_pid: str = str(os.getpid())
|
|
408
|
+
self._audit_proc_name: str = os.path.basename(sys.argv[0]) if sys.argv and sys.argv[0] else ""
|
|
405
409
|
atexit.register(self._mark_process_shutting_down)
|
|
406
410
|
|
|
407
411
|
trading_logger.log_info(
|
|
@@ -778,7 +782,7 @@ class Dhan(BrokerBase):
|
|
|
778
782
|
combo_symbol=lambda x: isinstance(x, str) and len(x.strip()) > 0,
|
|
779
783
|
order_size=lambda x: isinstance(x, (int, float)) and int(x) != 0,
|
|
780
784
|
)
|
|
781
|
-
def get_margin_requirement(self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds=None) -> float:
|
|
785
|
+
def get_margin_requirement(self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds=None, net: bool = False) -> float:
|
|
782
786
|
"""Return basket margin required for a single-leg or multi-leg combo."""
|
|
783
787
|
try:
|
|
784
788
|
if self.api is None:
|
|
@@ -826,8 +830,8 @@ class Dhan(BrokerBase):
|
|
|
826
830
|
|
|
827
831
|
payload = {
|
|
828
832
|
"dhanClientId": self.api.client_id,
|
|
829
|
-
"includePosition":
|
|
830
|
-
"includeOrder":
|
|
833
|
+
"includePosition": net,
|
|
834
|
+
"includeOrder": net,
|
|
831
835
|
"includeOrders": False,
|
|
832
836
|
"scripList": scripts,
|
|
833
837
|
"scripts": scripts,
|
|
@@ -979,53 +983,51 @@ class Dhan(BrokerBase):
|
|
|
979
983
|
parsed = parsed.replace(hour=23, minute=59, second=59, microsecond=999000)
|
|
980
984
|
return parsed.isoformat(timespec="milliseconds")
|
|
981
985
|
|
|
982
|
-
def
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
986
|
+
def _audit_dhan_call(self, call_type: str, fields: dict) -> None:
|
|
987
|
+
"""Unified Redis stream audit for all Dhan API call types and errors.
|
|
988
|
+
|
|
989
|
+
call_type: "historical" | "quote" | "stream" | "error" | "stream_disconnect"
|
|
990
|
+
Toggle live (no restart): redis-cli -n 1 SET audit:dhan:enabled 1|0
|
|
991
|
+
"""
|
|
992
|
+
now = time.monotonic()
|
|
993
|
+
if now - self._audit_enabled_cache_ts < 5.0:
|
|
994
|
+
if not self._audit_enabled_cache:
|
|
995
|
+
return
|
|
996
|
+
else:
|
|
997
|
+
try:
|
|
998
|
+
val = self._get_historical_audit_redis().get("audit:dhan:enabled")
|
|
999
|
+
self._audit_enabled_cache = val in (b"1", "1")
|
|
1000
|
+
except Exception:
|
|
1001
|
+
self._audit_enabled_cache = False
|
|
1002
|
+
self._audit_enabled_cache_ts = now
|
|
1003
|
+
if not self._audit_enabled_cache:
|
|
1004
|
+
return
|
|
990
1005
|
try:
|
|
991
1006
|
audit_redis = self._get_historical_audit_redis()
|
|
992
1007
|
tz_ist = pytz.timezone("Asia/Kolkata")
|
|
993
1008
|
now_ist = dt.datetime.now(tz_ist)
|
|
994
1009
|
day_key = now_ist.strftime("%Y%m%d")
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1010
|
+
ttl = 60 * 60 * 24 * 45
|
|
1011
|
+
stream_key = f"audit:dhan:{call_type}:req:v1:{day_key}"
|
|
1012
|
+
combined_key = f"audit:dhan:all:req:v1:{day_key}"
|
|
1013
|
+
count_key = f"audit:dhan:{call_type}:req:count:day:{day_key}"
|
|
1014
|
+
entry = {
|
|
1015
|
+
"ts_ist": now_ist.isoformat(timespec="milliseconds"),
|
|
1016
|
+
"call_type": call_type,
|
|
1017
|
+
"pid": self._audit_pid,
|
|
1018
|
+
"proc": self._audit_proc_name,
|
|
1019
|
+
**{k: str(v) for k, v in fields.items()},
|
|
1020
|
+
}
|
|
1006
1021
|
pipe = audit_redis.pipeline()
|
|
1007
|
-
pipe.incr(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
pipe.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
"ts_ist": ts_ist,
|
|
1014
|
-
"symbol": symbol_str,
|
|
1015
|
-
"security_id": str(security_id),
|
|
1016
|
-
"interval": str(interval),
|
|
1017
|
-
"from_date": from_dt_ist,
|
|
1018
|
-
"to_date": to_dt_ist,
|
|
1019
|
-
},
|
|
1020
|
-
maxlen=500000,
|
|
1021
|
-
approximate=True,
|
|
1022
|
-
)
|
|
1023
|
-
pipe.expire(total_key, ttl_seconds)
|
|
1024
|
-
pipe.expire(per_symbol_key, ttl_seconds)
|
|
1025
|
-
pipe.expire(stream_key, ttl_seconds)
|
|
1022
|
+
pipe.incr(count_key)
|
|
1023
|
+
pipe.xadd(stream_key, entry, maxlen=500000, approximate=True)
|
|
1024
|
+
pipe.xadd(combined_key, entry, maxlen=1000000, approximate=True)
|
|
1025
|
+
pipe.expire(count_key, ttl)
|
|
1026
|
+
pipe.expire(stream_key, ttl)
|
|
1027
|
+
pipe.expire(combined_key, ttl)
|
|
1026
1028
|
pipe.execute()
|
|
1027
1029
|
except Exception as e:
|
|
1028
|
-
trading_logger.log_warning("Failed to audit Dhan
|
|
1030
|
+
trading_logger.log_warning(f"Failed to audit Dhan {call_type} call", {"error": str(e)})
|
|
1029
1031
|
|
|
1030
1032
|
def _fetch_dhan_historical(
|
|
1031
1033
|
self,
|
|
@@ -1060,13 +1062,13 @@ class Dhan(BrokerBase):
|
|
|
1060
1062
|
url = self.api.base_url + "/charts/intraday"
|
|
1061
1063
|
|
|
1062
1064
|
self._wait_for_historical_rate_limit()
|
|
1063
|
-
self.
|
|
1064
|
-
symbol
|
|
1065
|
-
security_id
|
|
1066
|
-
interval
|
|
1067
|
-
from_date
|
|
1068
|
-
to_date
|
|
1069
|
-
)
|
|
1065
|
+
self._audit_dhan_call("historical", {
|
|
1066
|
+
"symbol": symbol or "",
|
|
1067
|
+
"security_id": security_id,
|
|
1068
|
+
"interval": periodicity,
|
|
1069
|
+
"from_date": from_date,
|
|
1070
|
+
"to_date": to_date,
|
|
1071
|
+
})
|
|
1070
1072
|
response = self.api.session.post(url, headers=self.api.header, timeout=self.api.timeout, data=json.dumps(payload))
|
|
1071
1073
|
return self.api._parse_response(response)
|
|
1072
1074
|
|
|
@@ -1809,14 +1811,22 @@ class Dhan(BrokerBase):
|
|
|
1809
1811
|
continue
|
|
1810
1812
|
historical_data_list.sort(key=lambda x: x.date)
|
|
1811
1813
|
else:
|
|
1814
|
+
_hist_err = _extract_dhan_error_details(data)
|
|
1812
1815
|
trading_logger.log_error(
|
|
1813
1816
|
"No data from Dhan historical API",
|
|
1814
1817
|
None,
|
|
1815
1818
|
{
|
|
1816
1819
|
"symbol": long_symbol,
|
|
1817
|
-
**
|
|
1820
|
+
**_hist_err,
|
|
1818
1821
|
},
|
|
1819
1822
|
)
|
|
1823
|
+
self._audit_dhan_call("error", {
|
|
1824
|
+
"call_type": "historical",
|
|
1825
|
+
"symbol": long_symbol,
|
|
1826
|
+
"security_id": security_id,
|
|
1827
|
+
"exchange_segment": exchange_segment,
|
|
1828
|
+
**_hist_err,
|
|
1829
|
+
})
|
|
1820
1830
|
|
|
1821
1831
|
if periodicity in {"1d", "d"} and date_end_dt.date() == get_tradingapi_now().date():
|
|
1822
1832
|
# Refresh today's daily bar from 1m data (up-to-date partial OHLC), matching Shoonya-style behavior.
|
|
@@ -1949,6 +1959,11 @@ class Dhan(BrokerBase):
|
|
|
1949
1959
|
raise BrokerConnectionError("Dhan API not initialized")
|
|
1950
1960
|
|
|
1951
1961
|
self._wait_for_quote_rate_limit()
|
|
1962
|
+
self._audit_dhan_call("quote", {
|
|
1963
|
+
"symbol": long_symbol,
|
|
1964
|
+
"security_id": security_id,
|
|
1965
|
+
"exchange_segment": exchange_segment,
|
|
1966
|
+
})
|
|
1952
1967
|
out = self.api.quote_data(securities={exchange_segment: [int(security_id)]})
|
|
1953
1968
|
if out and out.get("status") == "success":
|
|
1954
1969
|
response_data = out.get("data", {}) or {}
|
|
@@ -1971,9 +1986,17 @@ class Dhan(BrokerBase):
|
|
|
1971
1986
|
market_feed.ask_volume = int(sell_qty[0].get("quantity", 0) or 0)
|
|
1972
1987
|
return market_feed
|
|
1973
1988
|
|
|
1989
|
+
_quote_err = _extract_dhan_error_details(out)
|
|
1990
|
+
self._audit_dhan_call("error", {
|
|
1991
|
+
"call_type": "quote",
|
|
1992
|
+
"symbol": long_symbol,
|
|
1993
|
+
"security_id": security_id,
|
|
1994
|
+
"exchange_segment": exchange_segment,
|
|
1995
|
+
**_quote_err,
|
|
1996
|
+
})
|
|
1974
1997
|
trading_logger.log_warning(
|
|
1975
1998
|
"Dhan quote_data unavailable",
|
|
1976
|
-
{"long_symbol": long_symbol, **
|
|
1999
|
+
{"long_symbol": long_symbol, **_quote_err},
|
|
1977
2000
|
)
|
|
1978
2001
|
|
|
1979
2002
|
return market_feed
|
|
@@ -2438,6 +2461,18 @@ class Dhan(BrokerBase):
|
|
|
2438
2461
|
# Call synchronously; avoids RuntimeError from asyncio.to_thread()
|
|
2439
2462
|
# when concurrent.futures thread pool is torn down by another component.
|
|
2440
2463
|
self._wait_for_stream_request_rate_limit()
|
|
2464
|
+
self._audit_dhan_call("stream", {
|
|
2465
|
+
"request_code": message.get("RequestCode", ""),
|
|
2466
|
+
"instrument_count": len(message.get("InstrumentList", [])),
|
|
2467
|
+
"operation": "subscribe",
|
|
2468
|
+
"symbols": ",".join(
|
|
2469
|
+
stream_symbol_map.get(
|
|
2470
|
+
(_DHAN_FEED_SEGMENT_MAP.get(instr.get("ExchangeSegment", ""), ("", -1))[1], int(instr.get("SecurityId", -1))),
|
|
2471
|
+
(instr.get("SecurityId", ""), ""),
|
|
2472
|
+
)[0]
|
|
2473
|
+
for instr in message.get("InstrumentList", [])
|
|
2474
|
+
),
|
|
2475
|
+
})
|
|
2441
2476
|
await ws.send(json.dumps(message))
|
|
2442
2477
|
while self._streaming:
|
|
2443
2478
|
raw = await ws.recv()
|
|
@@ -2462,6 +2497,10 @@ class Dhan(BrokerBase):
|
|
|
2462
2497
|
trading_logger.log_info("Dhan streaming thread closed cleanly")
|
|
2463
2498
|
else:
|
|
2464
2499
|
trading_logger.log_error("Dhan streaming thread error", e, {})
|
|
2500
|
+
self._audit_dhan_call("stream_disconnect", {
|
|
2501
|
+
"error": str(e),
|
|
2502
|
+
"error_type": type(e).__name__,
|
|
2503
|
+
})
|
|
2465
2504
|
finally:
|
|
2466
2505
|
try:
|
|
2467
2506
|
if loop is not None and not loop.is_closed():
|
|
@@ -2500,6 +2539,18 @@ class Dhan(BrokerBase):
|
|
|
2500
2539
|
async def _send_incremental_messages():
|
|
2501
2540
|
for message in delta_messages:
|
|
2502
2541
|
self._wait_for_stream_request_rate_limit()
|
|
2542
|
+
self._audit_dhan_call("stream", {
|
|
2543
|
+
"request_code": message.get("RequestCode", ""),
|
|
2544
|
+
"instrument_count": len(message.get("InstrumentList", [])),
|
|
2545
|
+
"operation": "subscribe_delta",
|
|
2546
|
+
"symbols": ",".join(
|
|
2547
|
+
stream_symbol_map.get(
|
|
2548
|
+
(_DHAN_FEED_SEGMENT_MAP.get(instr.get("ExchangeSegment", ""), ("", -1))[1], int(instr.get("SecurityId", -1))),
|
|
2549
|
+
(instr.get("SecurityId", ""), ""),
|
|
2550
|
+
)[0]
|
|
2551
|
+
for instr in message.get("InstrumentList", [])
|
|
2552
|
+
),
|
|
2553
|
+
})
|
|
2503
2554
|
await ws.send(json.dumps(message))
|
|
2504
2555
|
|
|
2505
2556
|
future = asyncio.run_coroutine_threadsafe(_send_incremental_messages(), loop)
|