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.
- {tradingapi-0.2.0 → tradingapi-0.3.0}/PKG-INFO +14 -1
- {tradingapi-0.2.0 → tradingapi-0.3.0}/README.md +13 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/pyproject.toml +1 -1
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/__init__.py +10 -1
- tradingapi-0.3.0/tradingapi/allocation.py +163 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/broker_base.py +57 -38
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config/config_sample.yaml +45 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config.py +16 -0
- tradingapi-0.3.0/tradingapi/dhan.py +2578 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/fivepaisa.py +469 -109
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/flattrade.py +421 -61
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/icicidirect.py +905 -210
- tradingapi-0.3.0/tradingapi/market_data_exchanges.py +57 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/proxy_utils.py +9 -10
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/shoonya.py +699 -136
- tradingapi-0.3.0/tradingapi/span.py +837 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/utils.py +750 -248
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/PKG-INFO +14 -1
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/SOURCES.txt +3 -1
- tradingapi-0.2.0/tests/test_shoonya_symbol_parser.py +0 -0
- tradingapi-0.2.0/tradingapi/dhan.py +0 -1686
- {tradingapi-0.2.0 → tradingapi-0.3.0}/setup.cfg +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/attribution.py +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/globals.py +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.2.0 → tradingapi-0.3.0}/tradingapi.egg-info/requires.txt +0 -0
- {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.
|
|
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)
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
_get_trading_logger().log_warning(
|
|
295
295
|
"Received None value, returning 0",
|
|
296
|
-
|
|
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
|
-
|
|
313
|
+
_get_trading_logger().log_warning(
|
|
314
314
|
"Empty string value, returning 0",
|
|
315
|
-
|
|
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
|
-
|
|
328
|
+
_get_trading_logger().log_warning(
|
|
329
329
|
"Failed to convert string to int",
|
|
330
|
-
|
|
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
|
-
|
|
345
|
+
_get_trading_logger().log_warning(
|
|
346
346
|
"Object converted to empty string, returning 0",
|
|
347
|
-
|
|
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
|
-
|
|
359
|
+
_get_trading_logger().log_warning(
|
|
360
360
|
"Failed to convert object to int",
|
|
361
|
-
|
|
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
|
-
|
|
372
|
+
_get_trading_logger().log_error(
|
|
373
373
|
"Error converting value to int",
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
392
|
+
_get_trading_logger().log_warning(
|
|
393
393
|
"Received None value, returning NaN",
|
|
394
|
-
|
|
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
|
-
|
|
412
|
+
_get_trading_logger().log_warning(
|
|
413
413
|
"Empty string value, returning NaN",
|
|
414
|
-
|
|
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
|
-
|
|
426
|
+
_get_trading_logger().log_warning(
|
|
427
427
|
"Failed to convert string to float",
|
|
428
|
-
|
|
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
|
-
|
|
443
|
+
_get_trading_logger().log_warning(
|
|
444
444
|
"Object converted to empty string, returning NaN",
|
|
445
|
-
|
|
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
|
-
|
|
457
|
+
_get_trading_logger().log_warning(
|
|
458
458
|
"Failed to convert object to float",
|
|
459
|
-
|
|
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
|
-
|
|
470
|
+
_get_trading_logger().log_error(
|
|
471
471
|
"Error converting value to float",
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
496
|
+
_get_trading_logger().log_error(
|
|
497
497
|
"Error converting value to bool",
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
724
|
+
_get_trading_logger().log_info(
|
|
725
725
|
"Broker initialized successfully",
|
|
726
|
-
|
|
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
|
-
|
|
740
|
+
_get_trading_logger().log_debug(
|
|
741
741
|
"Validating broker configuration",
|
|
742
|
-
|
|
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:
|
|
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:
|