tradingapi 0.1.7__tar.gz → 0.2.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.
Files changed (30) hide show
  1. {tradingapi-0.1.7 → tradingapi-0.2.1}/PKG-INFO +14 -1
  2. {tradingapi-0.1.7 → tradingapi-0.2.1}/README.md +13 -0
  3. {tradingapi-0.1.7 → tradingapi-0.2.1}/pyproject.toml +1 -1
  4. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/__init__.py +27 -5
  5. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/broker_base.py +58 -40
  6. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/config/config_sample.yaml +45 -0
  7. tradingapi-0.2.1/tradingapi/dhan.py +2578 -0
  8. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/fivepaisa.py +400 -139
  9. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/flattrade.py +349 -61
  10. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/icicidirect.py +905 -210
  11. tradingapi-0.2.1/tradingapi/market_data_exchanges.py +57 -0
  12. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/proxy_utils.py +9 -10
  13. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/shoonya.py +620 -121
  14. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/utils.py +590 -207
  15. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi.egg-info/PKG-INFO +14 -1
  16. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi.egg-info/SOURCES.txt +1 -1
  17. tradingapi-0.1.7/tests/test_shoonya_symbol_parser.py +0 -0
  18. tradingapi-0.1.7/tradingapi/dhan.py +0 -1686
  19. {tradingapi-0.1.7 → tradingapi-0.2.1}/setup.cfg +0 -0
  20. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/attribution.py +0 -0
  21. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/config/commissions_20241216.yaml +0 -0
  22. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/config.py +0 -0
  23. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/error_handling.py +0 -0
  24. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/exceptions.py +0 -0
  25. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/globals.py +0 -0
  26. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi/icicidirect_generate_session.py +0 -0
  27. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi.egg-info/dependency_links.txt +0 -0
  28. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi.egg-info/entry_points.txt +0 -0
  29. {tradingapi-0.1.7 → tradingapi-0.2.1}/tradingapi.egg-info/requires.txt +0 -0
  30. {tradingapi-0.1.7 → tradingapi-0.2.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.1.7
3
+ Version: 0.2.1
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.1.7"
31
+ version = "0.2.1"
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
@@ -180,13 +187,26 @@ class TradingAPILogger:
180
187
  pass
181
188
  return {}
182
189
 
190
+ def _sanitize_extra(self, context: Optional[dict] = None) -> dict:
191
+ """Rename reserved LogRecord keys in extra context to avoid logging failures."""
192
+ extra = dict(context or {})
193
+ reserved_keys = set(logging.makeLogRecord({}).__dict__.keys()) | {
194
+ "message",
195
+ "asctime",
196
+ }
197
+ sanitized = {}
198
+ for key, value in extra.items():
199
+ new_key = f"context_{key}" if key in reserved_keys else key
200
+ sanitized[new_key] = value
201
+ return sanitized
202
+
183
203
  def log_error(
184
204
  self, message: str, error: Optional[Exception] = None, context: Optional[dict] = None, exc_info: bool = True
185
205
  ):
186
206
  """Log an error with structured context. Extra values are sanitized to single-line so
187
207
  parse_log_errors.py can reliably match the ERROR line (same format as other tradingapi logs).
188
208
  """
189
- extra = context or {}
209
+ extra = self._sanitize_extra(context)
190
210
  if error:
191
211
  extra["error_type"] = type(error).__name__
192
212
  # Keep error_message single-line so the log line matches parse_log_errors structured format
@@ -206,7 +226,7 @@ class TradingAPILogger:
206
226
 
207
227
  def log_warning(self, message: str, context: Optional[dict] = None):
208
228
  """Log a warning with structured context."""
209
- extra = context or {}
229
+ extra = self._sanitize_extra(context)
210
230
 
211
231
  # Add caller information
212
232
  caller_info = self._get_caller_info()
@@ -216,7 +236,7 @@ class TradingAPILogger:
216
236
 
217
237
  def log_info(self, message: str, context: Optional[dict] = None):
218
238
  """Log an info message with structured context."""
219
- extra = context or {}
239
+ extra = self._sanitize_extra(context)
220
240
 
221
241
  # Add caller information
222
242
  caller_info = self._get_caller_info()
@@ -226,7 +246,7 @@ class TradingAPILogger:
226
246
 
227
247
  def log_debug(self, message: str, context: Optional[dict] = None):
228
248
  """Log a debug message with structured context."""
229
- extra = context or {}
249
+ extra = self._sanitize_extra(context)
230
250
 
231
251
  # Add caller information
232
252
  caller_info = self._get_caller_info()
@@ -262,7 +282,9 @@ def configure_logging(
262
282
  backup_count: Number of log files to keep.
263
283
  format_string: Custom format string for log messages.
264
284
  enable_structured_logging: Enable structured logging with additional context.
265
- 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.
266
288
  """
267
289
  trading_logger.configure(
268
290
  level=level,
@@ -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,10 +10,6 @@ import pandas as pd
11
10
  import pytz
12
11
  import redis
13
12
 
14
- from chameli.dateutils import parse_datetime
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
13
  from .exceptions import (
19
14
  TradingAPIError,
20
15
  BrokerConnectionError,
@@ -26,8 +21,12 @@ from .exceptions import (
26
21
  create_error_context,
27
22
  )
28
23
  from .error_handling import retry_on_error, safe_execute, log_execution_time, handle_broker_errors, validate_inputs
24
+ from chameli.dateutils import parse_datetime
29
25
 
30
- # 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
31
30
 
32
31
  NEXT_DAY_TIMESTAMP = int((get_tradingapi_now() + dt.timedelta(days=1)).timestamp())
33
32
 
@@ -292,9 +291,9 @@ class Order:
292
291
  try:
293
292
  # Handle None values
294
293
  if value is None:
295
- logger.warning(
294
+ _get_trading_logger().log_warning(
296
295
  "Received None value, returning 0",
297
- 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},
298
297
  )
299
298
  return 0
300
299
 
@@ -311,9 +310,9 @@ class Order:
311
310
  # Strip whitespace and handle empty strings
312
311
  value = value.strip()
313
312
  if not value:
314
- logger.warning(
313
+ _get_trading_logger().log_warning(
315
314
  "Empty string value, returning 0",
316
- extra={
315
+ context={
317
316
  "value": repr(value),
318
317
  "default": 0,
319
318
  "method": "_convert_to_int",
@@ -326,9 +325,9 @@ class Order:
326
325
  # Try to convert string to float first, then to int
327
326
  return int(float(value))
328
327
  except ValueError:
329
- logger.warning(
328
+ _get_trading_logger().log_warning(
330
329
  "Failed to convert string to int",
331
- extra={
330
+ context={
332
331
  "value": repr(value),
333
332
  "default": 0,
334
333
  "method": "_convert_to_int",
@@ -343,9 +342,9 @@ class Order:
343
342
  # Convert object to string and then to number
344
343
  str_value = str(value).strip()
345
344
  if not str_value:
346
- logger.warning(
345
+ _get_trading_logger().log_warning(
347
346
  "Object converted to empty string, returning 0",
348
- extra={
347
+ context={
349
348
  "value": repr(value),
350
349
  "value_type": type(value).__name__,
351
350
  "default": 0,
@@ -357,9 +356,9 @@ class Order:
357
356
 
358
357
  return int(float(str_value))
359
358
  except (ValueError, TypeError):
360
- logger.warning(
359
+ _get_trading_logger().log_warning(
361
360
  "Failed to convert object to int",
362
- extra={
361
+ context={
363
362
  "value": repr(value),
364
363
  "value_type": type(value).__name__,
365
364
  "default": 0,
@@ -370,10 +369,10 @@ class Order:
370
369
  return 0
371
370
 
372
371
  except Exception as e:
373
- logger.error(
372
+ _get_trading_logger().log_error(
374
373
  "Error converting value to int",
375
- exc_info=True,
376
- extra={
374
+ e,
375
+ context={
377
376
  "value": repr(value),
378
377
  "value_type": type(value).__name__,
379
378
  "method": "_convert_to_int",
@@ -390,9 +389,9 @@ class Order:
390
389
  try:
391
390
  # Handle None values
392
391
  if value is None:
393
- logger.warning(
392
+ _get_trading_logger().log_warning(
394
393
  "Received None value, returning NaN",
395
- extra={
394
+ context={
396
395
  "value": value,
397
396
  "default": float("nan"),
398
397
  "method": "_convert_to_float",
@@ -410,9 +409,9 @@ class Order:
410
409
  # Strip whitespace and handle empty strings
411
410
  value = value.strip()
412
411
  if not value:
413
- logger.warning(
412
+ _get_trading_logger().log_warning(
414
413
  "Empty string value, returning NaN",
415
- extra={
414
+ context={
416
415
  "value": repr(value),
417
416
  "default": float("nan"),
418
417
  "method": "_convert_to_float",
@@ -424,9 +423,9 @@ class Order:
424
423
  try:
425
424
  return float(value)
426
425
  except ValueError:
427
- logger.warning(
426
+ _get_trading_logger().log_warning(
428
427
  "Failed to convert string to float",
429
- extra={
428
+ context={
430
429
  "value": repr(value),
431
430
  "default": float("nan"),
432
431
  "method": "_convert_to_float",
@@ -441,9 +440,9 @@ class Order:
441
440
  # Convert object to string and then to number
442
441
  str_value = str(value).strip()
443
442
  if not str_value:
444
- logger.warning(
443
+ _get_trading_logger().log_warning(
445
444
  "Object converted to empty string, returning NaN",
446
- extra={
445
+ context={
447
446
  "value": repr(value),
448
447
  "value_type": type(value).__name__,
449
448
  "default": float("nan"),
@@ -455,9 +454,9 @@ class Order:
455
454
 
456
455
  return float(str_value)
457
456
  except (ValueError, TypeError):
458
- logger.warning(
457
+ _get_trading_logger().log_warning(
459
458
  "Failed to convert object to float",
460
- extra={
459
+ context={
461
460
  "value": repr(value),
462
461
  "value_type": type(value).__name__,
463
462
  "default": float("nan"),
@@ -468,10 +467,10 @@ class Order:
468
467
  return float("nan")
469
468
 
470
469
  except Exception as e:
471
- logger.error(
470
+ _get_trading_logger().log_error(
472
471
  "Error converting value to float",
473
- exc_info=True,
474
- extra={
472
+ e,
473
+ context={
475
474
  "value": repr(value),
476
475
  "value_type": type(value).__name__,
477
476
  "method": "_convert_to_float",
@@ -494,10 +493,10 @@ class Order:
494
493
  else:
495
494
  return False
496
495
  except Exception as e:
497
- logger.error(
496
+ _get_trading_logger().log_error(
498
497
  "Error converting value to bool",
499
- exc_info=True,
500
- extra={
498
+ e,
499
+ context={
501
500
  "value": value,
502
501
  "value_type": type(value).__name__,
503
502
  "method": "_convert_to_bool",
@@ -722,9 +721,9 @@ class BrokerBase(ABC):
722
721
  try:
723
722
  self._validate_config(kwargs)
724
723
  self._initialize_broker(kwargs)
725
- logger.info(
724
+ _get_trading_logger().log_info(
726
725
  "Broker initialized successfully",
727
- extra={"broker_type": self.__class__.__name__, "config_keys": list(kwargs.keys())},
726
+ context={"broker_type": self.__class__.__name__, "config_keys": list(kwargs.keys())},
728
727
  )
729
728
  except Exception as e:
730
729
  context = create_error_context(
@@ -738,9 +737,9 @@ class BrokerBase(ABC):
738
737
  raise ValidationError("Configuration must be a dictionary")
739
738
 
740
739
  # Add specific validation logic for each broker type
741
- logger.debug(
740
+ _get_trading_logger().log_debug(
742
741
  "Validating broker configuration",
743
- extra={"broker_type": self.__class__.__name__, "config_keys": list(config.keys())},
742
+ context={"broker_type": self.__class__.__name__, "config_keys": list(config.keys())},
744
743
  )
745
744
 
746
745
  def _initialize_broker(self, config: dict):
@@ -835,7 +834,11 @@ class BrokerBase(ABC):
835
834
  Modify an existing order.
836
835
 
837
836
  Args:
838
- **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.
839
842
 
840
843
  Returns:
841
844
  Order: Updated order object
@@ -1071,3 +1074,18 @@ class BrokerBase(ABC):
1071
1074
  MarketDataError: If balance retrieval fails
1072
1075
  """
1073
1076
  pass
1077
+
1078
+ def get_margin_requirement(
1079
+ self, combo_symbol: str, order_size: int, exchange: str = "NSE", mds: Optional[str] = None
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