tradingapi 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {tradingapi-0.2.0 → tradingapi-0.3.0}/PKG-INFO +14 -1
  2. {tradingapi-0.2.0 → tradingapi-0.3.0}/README.md +13 -0
  3. {tradingapi-0.2.0 → tradingapi-0.3.0}/pyproject.toml +1 -1
  4. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/__init__.py +10 -1
  5. tradingapi-0.3.0/tradingapi/allocation.py +163 -0
  6. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/broker_base.py +57 -38
  7. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config/config_sample.yaml +45 -0
  8. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config.py +16 -0
  9. tradingapi-0.3.0/tradingapi/dhan.py +2578 -0
  10. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/fivepaisa.py +469 -109
  11. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/flattrade.py +421 -61
  12. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/icicidirect.py +905 -210
  13. tradingapi-0.3.0/tradingapi/market_data_exchanges.py +57 -0
  14. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/proxy_utils.py +9 -10
  15. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/shoonya.py +699 -136
  16. tradingapi-0.3.0/tradingapi/span.py +837 -0
  17. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/utils.py +750 -248
  18. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/PKG-INFO +14 -1
  19. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/SOURCES.txt +3 -1
  20. tradingapi-0.2.0/tests/test_shoonya_symbol_parser.py +0 -0
  21. tradingapi-0.2.0/tradingapi/dhan.py +0 -1686
  22. {tradingapi-0.2.0 → tradingapi-0.3.0}/setup.cfg +0 -0
  23. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/attribution.py +0 -0
  24. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config/commissions_20241216.yaml +0 -0
  25. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/error_handling.py +0 -0
  26. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/exceptions.py +0 -0
  27. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/globals.py +0 -0
  28. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/icicidirect_generate_session.py +0 -0
  29. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/dependency_links.txt +0 -0
  30. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/entry_points.txt +0 -0
  31. {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/requires.txt +0 -0
  32. {tradingapi-0.2.0 → tradingapi-0.3.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.2.0
3
+ Version: 0.3.0
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -259,6 +259,19 @@ ICICIDIRECT:
259
259
  USERTOKEN_MAX_AGE_HOURS: 20
260
260
  AUTO_SESSION_TOKEN_CMD: "icicidirect-generate-session --api-key \"${ICICI_API_KEY}\" --user-id \"${ICICI_USER_ID}\" --password \"${ICICI_PASSWORD}\" --totp-token \"${ICICI_TOTP_TOKEN}\"" # Optional non-interactive token command
261
261
  SYMBOLCODES: "/path/to/icicidirect/symbols"
262
+
263
+ # Dhan Broker Configuration
264
+ DHAN:
265
+ CLIENT_ID: "your_client_id"
266
+ ACCESS_TOKEN: "your_access_token" # Optional fallback if TOTP/PIN flow is not used
267
+ TOTP_TOKEN: "your_totp_token" # Optional: used for auto token refresh
268
+ PIN: "your_pin" # Optional: used for auto token refresh
269
+ USERTOKEN: "/path/to/dhan_token.txt" # Optional token cache file
270
+ SYMBOLCODES: "/path/to/dhan/symbols"
271
+ QUOTE_RATE_LIMIT_REDIS_DB: 4 # Optional: dedicated Redis DB for global quote limiter
272
+ QUOTE_RATE_LIMIT_RPS: 1 # Optional: quote API limit in requests/second
273
+ HISTORICAL_RATE_LIMIT_RPS: 10 # Optional: historical API limit in requests/second
274
+ REQUEST_STREAMING_DATA_RATE_LIMIT_RPS: 10 # Optional: streaming subscription request rate in requests/second
262
275
  ```
263
276
 
264
277
  #### ICICIDirect fully automated session-token refresh (no copy/paste)
@@ -240,6 +240,19 @@ ICICIDIRECT:
240
240
  USERTOKEN_MAX_AGE_HOURS: 20
241
241
  AUTO_SESSION_TOKEN_CMD: "icicidirect-generate-session --api-key \"${ICICI_API_KEY}\" --user-id \"${ICICI_USER_ID}\" --password \"${ICICI_PASSWORD}\" --totp-token \"${ICICI_TOTP_TOKEN}\"" # Optional non-interactive token command
242
242
  SYMBOLCODES: "/path/to/icicidirect/symbols"
243
+
244
+ # Dhan Broker Configuration
245
+ DHAN:
246
+ CLIENT_ID: "your_client_id"
247
+ ACCESS_TOKEN: "your_access_token" # Optional fallback if TOTP/PIN flow is not used
248
+ TOTP_TOKEN: "your_totp_token" # Optional: used for auto token refresh
249
+ PIN: "your_pin" # Optional: used for auto token refresh
250
+ USERTOKEN: "/path/to/dhan_token.txt" # Optional token cache file
251
+ SYMBOLCODES: "/path/to/dhan/symbols"
252
+ QUOTE_RATE_LIMIT_REDIS_DB: 4 # Optional: dedicated Redis DB for global quote limiter
253
+ QUOTE_RATE_LIMIT_RPS: 1 # Optional: quote API limit in requests/second
254
+ HISTORICAL_RATE_LIMIT_RPS: 10 # Optional: historical API limit in requests/second
255
+ REQUEST_STREAMING_DATA_RATE_LIMIT_RPS: 10 # Optional: streaming subscription request rate in requests/second
243
256
  ```
244
257
 
245
258
  #### ICICIDirect fully automated session-token refresh (no copy/paste)
@@ -28,7 +28,7 @@ packages = ["tradingapi"]
28
28
 
29
29
  [project]
30
30
  name = "tradingapi"
31
- version = "0.2.0"
31
+ version = "0.3.0"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -144,6 +144,13 @@ class TradingAPILogger:
144
144
  for handler in handlers:
145
145
  self.logger.addHandler(handler)
146
146
 
147
+ # Child loggers (e.g. tradingapi.utils) propagate here; without this, the same
148
+ # record also reaches root and duplicates when root has its own handlers.
149
+ if handlers:
150
+ self.logger.propagate = False
151
+ else:
152
+ self.logger.propagate = True
153
+
147
154
  self._configured = True
148
155
 
149
156
  # Log configuration
@@ -275,7 +282,9 @@ def configure_logging(
275
282
  backup_count: Number of log files to keep.
276
283
  format_string: Custom format string for log messages.
277
284
  enable_structured_logging: Enable structured logging with additional context.
278
- configure_root_logger: Whether to configure the root logger (can cause duplicate logs if True).
285
+ configure_root_logger: If True, attach the same handlers to the root logger (for third-party
286
+ loggers). The ``tradingapi`` logger sets propagate=False when it has handlers, so package
287
+ messages are not written twice when root is also configured.
279
288
  """
280
289
  trading_logger.configure(
281
290
  level=level,
@@ -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)
@@ -1,6 +1,5 @@
1
1
  import datetime as dt
2
2
  import json
3
- import logging
4
3
  import math
5
4
  from abc import ABC, abstractmethod
6
5
  from dataclasses import asdict, dataclass
@@ -11,8 +10,6 @@ import pandas as pd
11
10
  import pytz
12
11
  import redis
13
12
 
14
- logger = logging.getLogger(__name__)
15
-
16
13
  from .exceptions import (
17
14
  TradingAPIError,
18
15
  BrokerConnectionError,
@@ -26,7 +23,10 @@ from .exceptions import (
26
23
  from .error_handling import retry_on_error, safe_execute, log_execution_time, handle_broker_errors, validate_inputs
27
24
  from chameli.dateutils import parse_datetime
28
25
 
29
- # Removed trading_logger import to avoid circular import issues
26
+
27
+ def _get_trading_logger():
28
+ from tradingapi import trading_logger
29
+ return trading_logger
30
30
 
31
31
  NEXT_DAY_TIMESTAMP = int((get_tradingapi_now() + dt.timedelta(days=1)).timestamp())
32
32
 
@@ -291,9 +291,9 @@ class Order:
291
291
  try:
292
292
  # Handle None values
293
293
  if value is None:
294
- logger.warning(
294
+ _get_trading_logger().log_warning(
295
295
  "Received None value, returning 0",
296
- extra={"value": value, "default": 0, "method": "_convert_to_int", "argument_name": argument_name},
296
+ context={"value": value, "default": 0, "method": "_convert_to_int", "argument_name": argument_name},
297
297
  )
298
298
  return 0
299
299
 
@@ -310,9 +310,9 @@ class Order:
310
310
  # Strip whitespace and handle empty strings
311
311
  value = value.strip()
312
312
  if not value:
313
- logger.warning(
313
+ _get_trading_logger().log_warning(
314
314
  "Empty string value, returning 0",
315
- extra={
315
+ context={
316
316
  "value": repr(value),
317
317
  "default": 0,
318
318
  "method": "_convert_to_int",
@@ -325,9 +325,9 @@ class Order:
325
325
  # Try to convert string to float first, then to int
326
326
  return int(float(value))
327
327
  except ValueError:
328
- logger.warning(
328
+ _get_trading_logger().log_warning(
329
329
  "Failed to convert string to int",
330
- extra={
330
+ context={
331
331
  "value": repr(value),
332
332
  "default": 0,
333
333
  "method": "_convert_to_int",
@@ -342,9 +342,9 @@ class Order:
342
342
  # Convert object to string and then to number
343
343
  str_value = str(value).strip()
344
344
  if not str_value:
345
- logger.warning(
345
+ _get_trading_logger().log_warning(
346
346
  "Object converted to empty string, returning 0",
347
- extra={
347
+ context={
348
348
  "value": repr(value),
349
349
  "value_type": type(value).__name__,
350
350
  "default": 0,
@@ -356,9 +356,9 @@ class Order:
356
356
 
357
357
  return int(float(str_value))
358
358
  except (ValueError, TypeError):
359
- logger.warning(
359
+ _get_trading_logger().log_warning(
360
360
  "Failed to convert object to int",
361
- extra={
361
+ context={
362
362
  "value": repr(value),
363
363
  "value_type": type(value).__name__,
364
364
  "default": 0,
@@ -369,10 +369,10 @@ class Order:
369
369
  return 0
370
370
 
371
371
  except Exception as e:
372
- logger.error(
372
+ _get_trading_logger().log_error(
373
373
  "Error converting value to int",
374
- exc_info=True,
375
- extra={
374
+ e,
375
+ context={
376
376
  "value": repr(value),
377
377
  "value_type": type(value).__name__,
378
378
  "method": "_convert_to_int",
@@ -389,9 +389,9 @@ class Order:
389
389
  try:
390
390
  # Handle None values
391
391
  if value is None:
392
- logger.warning(
392
+ _get_trading_logger().log_warning(
393
393
  "Received None value, returning NaN",
394
- extra={
394
+ context={
395
395
  "value": value,
396
396
  "default": float("nan"),
397
397
  "method": "_convert_to_float",
@@ -409,9 +409,9 @@ class Order:
409
409
  # Strip whitespace and handle empty strings
410
410
  value = value.strip()
411
411
  if not value:
412
- logger.warning(
412
+ _get_trading_logger().log_warning(
413
413
  "Empty string value, returning NaN",
414
- extra={
414
+ context={
415
415
  "value": repr(value),
416
416
  "default": float("nan"),
417
417
  "method": "_convert_to_float",
@@ -423,9 +423,9 @@ class Order:
423
423
  try:
424
424
  return float(value)
425
425
  except ValueError:
426
- logger.warning(
426
+ _get_trading_logger().log_warning(
427
427
  "Failed to convert string to float",
428
- extra={
428
+ context={
429
429
  "value": repr(value),
430
430
  "default": float("nan"),
431
431
  "method": "_convert_to_float",
@@ -440,9 +440,9 @@ class Order:
440
440
  # Convert object to string and then to number
441
441
  str_value = str(value).strip()
442
442
  if not str_value:
443
- logger.warning(
443
+ _get_trading_logger().log_warning(
444
444
  "Object converted to empty string, returning NaN",
445
- extra={
445
+ context={
446
446
  "value": repr(value),
447
447
  "value_type": type(value).__name__,
448
448
  "default": float("nan"),
@@ -454,9 +454,9 @@ class Order:
454
454
 
455
455
  return float(str_value)
456
456
  except (ValueError, TypeError):
457
- logger.warning(
457
+ _get_trading_logger().log_warning(
458
458
  "Failed to convert object to float",
459
- extra={
459
+ context={
460
460
  "value": repr(value),
461
461
  "value_type": type(value).__name__,
462
462
  "default": float("nan"),
@@ -467,10 +467,10 @@ class Order:
467
467
  return float("nan")
468
468
 
469
469
  except Exception as e:
470
- logger.error(
470
+ _get_trading_logger().log_error(
471
471
  "Error converting value to float",
472
- exc_info=True,
473
- extra={
472
+ e,
473
+ context={
474
474
  "value": repr(value),
475
475
  "value_type": type(value).__name__,
476
476
  "method": "_convert_to_float",
@@ -493,10 +493,10 @@ class Order:
493
493
  else:
494
494
  return False
495
495
  except Exception as e:
496
- logger.error(
496
+ _get_trading_logger().log_error(
497
497
  "Error converting value to bool",
498
- exc_info=True,
499
- extra={
498
+ e,
499
+ context={
500
500
  "value": value,
501
501
  "value_type": type(value).__name__,
502
502
  "method": "_convert_to_bool",
@@ -721,9 +721,9 @@ class BrokerBase(ABC):
721
721
  try:
722
722
  self._validate_config(kwargs)
723
723
  self._initialize_broker(kwargs)
724
- logger.info(
724
+ _get_trading_logger().log_info(
725
725
  "Broker initialized successfully",
726
- extra={"broker_type": self.__class__.__name__, "config_keys": list(kwargs.keys())},
726
+ context={"broker_type": self.__class__.__name__, "config_keys": list(kwargs.keys())},
727
727
  )
728
728
  except Exception as e:
729
729
  context = create_error_context(
@@ -737,9 +737,9 @@ class BrokerBase(ABC):
737
737
  raise ValidationError("Configuration must be a dictionary")
738
738
 
739
739
  # Add specific validation logic for each broker type
740
- logger.debug(
740
+ _get_trading_logger().log_debug(
741
741
  "Validating broker configuration",
742
- extra={"broker_type": self.__class__.__name__, "config_keys": list(config.keys())},
742
+ context={"broker_type": self.__class__.__name__, "config_keys": list(config.keys())},
743
743
  )
744
744
 
745
745
  def _initialize_broker(self, config: dict):
@@ -834,7 +834,11 @@ class BrokerBase(ABC):
834
834
  Modify an existing order.
835
835
 
836
836
  Args:
837
- **kwargs: Order modification parameters
837
+ **kwargs:
838
+ broker_order_id (str): Broker order ID to modify.
839
+ new_price (float): New limit price (0 for market).
840
+ new_quantity (int): New total quantity.
841
+ order (Order, optional): Order object to bootstrap Redis state if not cached.
838
842
 
839
843
  Returns:
840
844
  Order: Updated order object
@@ -1070,3 +1074,18 @@ class BrokerBase(ABC):
1070
1074
  MarketDataError: If balance retrieval fails
1071
1075
  """
1072
1076
  pass
1077
+
1078
+ def get_margin_requirement(
1079
+ self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds: Optional[str] = None, net: bool = False
1080
+ ) -> Optional[float]:
1081
+ """Default broker margin hook. Brokers can override if supported."""
1082
+ _get_trading_logger().log_warning(
1083
+ "get_margin_requirement not implemented for broker",
1084
+ context={
1085
+ "broker_type": self.__class__.__name__,
1086
+ "combo_symbol": combo_symbol,
1087
+ "order_size": order_size,
1088
+ "exchange": exchange,
1089
+ },
1090
+ )
1091
+ return None
@@ -20,6 +20,7 @@ commissions:
20
20
  file: "commissions_20241216.yaml"
21
21
 
22
22
  FIVEPAISA:
23
+ EXCHANGES: [NSE, BSE, MCX]
23
24
  USE_PROXY: false
24
25
  APP_NAME:
25
26
  APP_SOURCE:
@@ -33,7 +34,12 @@ FIVEPAISA:
33
34
  SYMBOLCODES:
34
35
  USERTOKEN:
35
36
 
37
+ # Additional FivePaisa accounts (optional). Pass as account= to FivePaisa().
38
+ # FIVEPAISA_ACCOUNT2:
39
+ # ... (same keys as FIVEPAISA above)
40
+
36
41
  SHOONYA:
42
+ EXCHANGES: [NSE, BSE, MCX]
37
43
  USE_PROXY: false
38
44
  USER:
39
45
  PWD:
@@ -43,7 +49,12 @@ SHOONYA:
43
49
  SYMBOLCODES: "/home/psharma/onedrive/rfiles/data/static/shoonya_symbols"
44
50
  USERTOKEN:
45
51
 
52
+ # Additional Shoonya accounts (optional). Pass as account= to Shoonya().
53
+ # SHOONYA_ACCOUNT2:
54
+ # ... (same keys as SHOONYA above)
55
+
46
56
  ICICIDIRECT:
57
+ EXCHANGES: [NSE, BSE, MCX]
47
58
  USE_PROXY: false
48
59
  API_KEY:
49
60
  API_SECRET:
@@ -72,3 +83,37 @@ ICICIDIRECT:
72
83
  # --totp-token "${ICICI_TOTP_TOKEN}"
73
84
  SYMBOL_MASTER_URL: # Optional: symbol master zip url
74
85
  SYMBOLCODES:
86
+
87
+ # Additional ICICIDirect accounts (optional). Pass as account= to IciciDirect().
88
+ # ICICIDIRECT_ACCOUNT2:
89
+ # ... (same keys as ICICIDIRECT above)
90
+
91
+ DHAN:
92
+ EXCHANGES: [NSE, BSE]
93
+ USE_PROXY: false
94
+ CLIENT_ID:
95
+ ACCESS_TOKEN: # Optional fallback if TOTP/PIN flow is not used
96
+ TOTP_TOKEN: # Optional: used for auto token refresh
97
+ PIN: # Optional: used for auto token refresh
98
+ USERTOKEN: # Optional: token cache file path
99
+ SYMBOLCODES:
100
+ QUOTE_RATE_LIMIT_REDIS_DB: 4 # Optional: dedicated Redis DB for global quote limiter
101
+ QUOTE_RATE_LIMIT_RPS: 1 # Optional: quote API limit in requests/second
102
+ HISTORICAL_RATE_LIMIT_RPS: 10 # Optional: historical API limit in requests/second
103
+ REQUEST_STREAMING_DATA_RATE_LIMIT_RPS: 10 # Optional: streaming subscription request rate in requests/second
104
+
105
+ # Additional DHAN accounts (optional). Name can be anything; pass as account= to Dhan().
106
+ # Example: dh2 = Dhan(account="DHAN_ACCOUNT2"); dh2.connect(redis_db=5)
107
+ DHAN_ACCOUNT2:
108
+ EXCHANGES: [NSE, BSE]
109
+ USE_PROXY: false
110
+ CLIENT_ID:
111
+ ACCESS_TOKEN:
112
+ TOTP_TOKEN:
113
+ PIN:
114
+ USERTOKEN:
115
+ SYMBOLCODES:
116
+ QUOTE_RATE_LIMIT_REDIS_DB: 5
117
+ QUOTE_RATE_LIMIT_RPS: 1
118
+ HISTORICAL_RATE_LIMIT_RPS: 10
119
+ REQUEST_STREAMING_DATA_RATE_LIMIT_RPS: 10
@@ -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: