tradingapi 0.1.7__tar.gz → 0.2.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.1.7 → tradingapi-0.2.0}/PKG-INFO +1 -1
- {tradingapi-0.1.7 → tradingapi-0.2.0}/pyproject.toml +1 -1
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/__init__.py +17 -4
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/broker_base.py +1 -2
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/dhan.py +5 -5
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/fivepaisa.py +114 -34
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/flattrade.py +10 -1
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/shoonya.py +21 -1
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/utils.py +54 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/PKG-INFO +1 -1
- {tradingapi-0.1.7 → tradingapi-0.2.0}/README.md +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/setup.cfg +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tests/test_shoonya_symbol_parser.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/attribution.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/config.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/globals.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/SOURCES.txt +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.1.7 → tradingapi-0.2.0}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -180,13 +180,26 @@ class TradingAPILogger:
|
|
|
180
180
|
pass
|
|
181
181
|
return {}
|
|
182
182
|
|
|
183
|
+
def _sanitize_extra(self, context: Optional[dict] = None) -> dict:
|
|
184
|
+
"""Rename reserved LogRecord keys in extra context to avoid logging failures."""
|
|
185
|
+
extra = dict(context or {})
|
|
186
|
+
reserved_keys = set(logging.makeLogRecord({}).__dict__.keys()) | {
|
|
187
|
+
"message",
|
|
188
|
+
"asctime",
|
|
189
|
+
}
|
|
190
|
+
sanitized = {}
|
|
191
|
+
for key, value in extra.items():
|
|
192
|
+
new_key = f"context_{key}" if key in reserved_keys else key
|
|
193
|
+
sanitized[new_key] = value
|
|
194
|
+
return sanitized
|
|
195
|
+
|
|
183
196
|
def log_error(
|
|
184
197
|
self, message: str, error: Optional[Exception] = None, context: Optional[dict] = None, exc_info: bool = True
|
|
185
198
|
):
|
|
186
199
|
"""Log an error with structured context. Extra values are sanitized to single-line so
|
|
187
200
|
parse_log_errors.py can reliably match the ERROR line (same format as other tradingapi logs).
|
|
188
201
|
"""
|
|
189
|
-
extra = context
|
|
202
|
+
extra = self._sanitize_extra(context)
|
|
190
203
|
if error:
|
|
191
204
|
extra["error_type"] = type(error).__name__
|
|
192
205
|
# Keep error_message single-line so the log line matches parse_log_errors structured format
|
|
@@ -206,7 +219,7 @@ class TradingAPILogger:
|
|
|
206
219
|
|
|
207
220
|
def log_warning(self, message: str, context: Optional[dict] = None):
|
|
208
221
|
"""Log a warning with structured context."""
|
|
209
|
-
extra = context
|
|
222
|
+
extra = self._sanitize_extra(context)
|
|
210
223
|
|
|
211
224
|
# Add caller information
|
|
212
225
|
caller_info = self._get_caller_info()
|
|
@@ -216,7 +229,7 @@ class TradingAPILogger:
|
|
|
216
229
|
|
|
217
230
|
def log_info(self, message: str, context: Optional[dict] = None):
|
|
218
231
|
"""Log an info message with structured context."""
|
|
219
|
-
extra = context
|
|
232
|
+
extra = self._sanitize_extra(context)
|
|
220
233
|
|
|
221
234
|
# Add caller information
|
|
222
235
|
caller_info = self._get_caller_info()
|
|
@@ -226,7 +239,7 @@ class TradingAPILogger:
|
|
|
226
239
|
|
|
227
240
|
def log_debug(self, message: str, context: Optional[dict] = None):
|
|
228
241
|
"""Log a debug message with structured context."""
|
|
229
|
-
extra = context
|
|
242
|
+
extra = self._sanitize_extra(context)
|
|
230
243
|
|
|
231
244
|
# Add caller information
|
|
232
245
|
caller_info = self._get_caller_info()
|
|
@@ -11,8 +11,6 @@ import pandas as pd
|
|
|
11
11
|
import pytz
|
|
12
12
|
import redis
|
|
13
13
|
|
|
14
|
-
from chameli.dateutils import parse_datetime
|
|
15
|
-
|
|
16
14
|
logger = logging.getLogger(__name__)
|
|
17
15
|
|
|
18
16
|
from .exceptions import (
|
|
@@ -26,6 +24,7 @@ from .exceptions import (
|
|
|
26
24
|
create_error_context,
|
|
27
25
|
)
|
|
28
26
|
from .error_handling import retry_on_error, safe_execute, log_execution_time, handle_broker_errors, validate_inputs
|
|
27
|
+
from chameli.dateutils import parse_datetime
|
|
29
28
|
|
|
30
29
|
# Removed trading_logger import to avoid circular import issues
|
|
31
30
|
|
|
@@ -14,8 +14,6 @@ import requests
|
|
|
14
14
|
import redis
|
|
15
15
|
import pytz
|
|
16
16
|
|
|
17
|
-
from chameli.dateutils import parse_datetime
|
|
18
|
-
|
|
19
17
|
from dhanhq import dhanhq as dhanhq_sdk
|
|
20
18
|
try:
|
|
21
19
|
from dhanhq import DhanContext, MarketFeed
|
|
@@ -36,6 +34,7 @@ from .broker_base import (
|
|
|
36
34
|
_normalize_as_of_date,
|
|
37
35
|
)
|
|
38
36
|
from .config import get_config
|
|
37
|
+
from chameli.dateutils import parse_datetime
|
|
39
38
|
from .utils import (
|
|
40
39
|
_set_long_symbol_if_not_combo,
|
|
41
40
|
delete_broker_order_id,
|
|
@@ -517,7 +516,7 @@ class Dhan(BrokerBase):
|
|
|
517
516
|
@retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
|
|
518
517
|
def get_available_capital(self) -> Dict[str, float]:
|
|
519
518
|
"""
|
|
520
|
-
|
|
519
|
+
Returns dict with 'cash', 'collateral', and 'used' keys.
|
|
521
520
|
|
|
522
521
|
Dhan's get_fund_limits() response data fields:
|
|
523
522
|
availabelBalance, collateralAmount, utilisedAmount, …
|
|
@@ -533,9 +532,10 @@ class Dhan(BrokerBase):
|
|
|
533
532
|
data = result.get("data", {})
|
|
534
533
|
cash = float(data.get("availabelBalance", 0) or 0)
|
|
535
534
|
collateral = float(data.get("collateralAmount", 0) or 0)
|
|
535
|
+
used = float(data.get("utilisedAmount", 0) or 0)
|
|
536
536
|
|
|
537
|
-
trading_logger.log_debug("Available capital retrieved", {"cash": cash, "collateral": collateral})
|
|
538
|
-
return {"cash": cash, "collateral": collateral}
|
|
537
|
+
trading_logger.log_debug("Available capital retrieved", {"cash": cash, "collateral": collateral, "used": used})
|
|
538
|
+
return {"cash": cash, "collateral": collateral, "used": used}
|
|
539
539
|
|
|
540
540
|
except (BrokerConnectionError, MarketDataError):
|
|
541
541
|
raise
|
|
@@ -875,6 +875,26 @@ class FivePaisa(BrokerBase):
|
|
|
875
875
|
except (ValueError, TypeError):
|
|
876
876
|
continue
|
|
877
877
|
|
|
878
|
+
used = 0.0
|
|
879
|
+
used_fields = [
|
|
880
|
+
"UtilizedMargin",
|
|
881
|
+
"UsedMargin",
|
|
882
|
+
"MarginUsed",
|
|
883
|
+
"BookedMargin",
|
|
884
|
+
"TotalMargin",
|
|
885
|
+
"utilizedmargin",
|
|
886
|
+
"usedmargin",
|
|
887
|
+
"marginused",
|
|
888
|
+
"MarginUtilized",
|
|
889
|
+
]
|
|
890
|
+
for field in used_fields:
|
|
891
|
+
if field in margin_data[0]:
|
|
892
|
+
try:
|
|
893
|
+
used = float(margin_data[0][field])
|
|
894
|
+
break
|
|
895
|
+
except (ValueError, TypeError):
|
|
896
|
+
continue
|
|
897
|
+
|
|
878
898
|
trading_logger.log_debug(
|
|
879
899
|
"Available capital retrieved",
|
|
880
900
|
{
|
|
@@ -882,10 +902,11 @@ class FivePaisa(BrokerBase):
|
|
|
882
902
|
"funds_payln": funds_payln,
|
|
883
903
|
"cash": cash,
|
|
884
904
|
"collateral": collateral,
|
|
905
|
+
"used": used,
|
|
885
906
|
"total_capital": cash + collateral,
|
|
886
907
|
},
|
|
887
908
|
)
|
|
888
|
-
return {"cash": cash, "collateral": collateral}
|
|
909
|
+
return {"cash": cash, "collateral": collateral, "used": used}
|
|
889
910
|
|
|
890
911
|
except (BrokerConnectionError, MarketDataError):
|
|
891
912
|
raise
|
|
@@ -916,6 +937,7 @@ class FivePaisa(BrokerBase):
|
|
|
916
937
|
if self.api:
|
|
917
938
|
self.api = None
|
|
918
939
|
trading_logger.log_info("API reference cleared", {"broker_type": self.broker.name})
|
|
940
|
+
self.subscribe_thread = None
|
|
919
941
|
|
|
920
942
|
trading_logger.log_info("Successfully disconnected from FivePaisa", {"broker_type": self.broker.name})
|
|
921
943
|
return True
|
|
@@ -3206,6 +3228,32 @@ class FivePaisa(BrokerBase):
|
|
|
3206
3228
|
context = create_error_context(symbols=symbols, exchange=exchange, error=str(e))
|
|
3207
3229
|
raise MarketDataError(f"Failed to map exchange: {str(e)}", context)
|
|
3208
3230
|
|
|
3231
|
+
def resolve_symbol_and_exchange(json_data):
|
|
3232
|
+
"""Resolve a token to the correct symbol/exchange even when the tick exchange key is incomplete."""
|
|
3233
|
+
token = json_data.get("Token")
|
|
3234
|
+
exchange_candidates = []
|
|
3235
|
+
tick_exchange = json_data.get("Exch")
|
|
3236
|
+
|
|
3237
|
+
if tick_exchange in self.exchange_mappings:
|
|
3238
|
+
exchange_candidates.append(tick_exchange)
|
|
3239
|
+
|
|
3240
|
+
if isinstance(tick_exchange, str) and tick_exchange:
|
|
3241
|
+
short_exchange = tick_exchange[0]
|
|
3242
|
+
if short_exchange in self.exchange_mappings and short_exchange not in exchange_candidates:
|
|
3243
|
+
exchange_candidates.append(short_exchange)
|
|
3244
|
+
|
|
3245
|
+
for exchange_key in exchange_candidates:
|
|
3246
|
+
symbol = self.exchange_mappings[exchange_key]["symbol_map_reversed"].get(token)
|
|
3247
|
+
if symbol is not None:
|
|
3248
|
+
return symbol, exchange_key
|
|
3249
|
+
|
|
3250
|
+
for exchange_key, exchange_mapping in self.exchange_mappings.items():
|
|
3251
|
+
symbol = exchange_mapping["symbol_map_reversed"].get(token)
|
|
3252
|
+
if symbol is not None:
|
|
3253
|
+
return symbol, exchange_key
|
|
3254
|
+
|
|
3255
|
+
return None, tick_exchange
|
|
3256
|
+
|
|
3209
3257
|
def map_to_price(json_data):
|
|
3210
3258
|
"""Map JSON data to Price object."""
|
|
3211
3259
|
try:
|
|
@@ -3220,10 +3268,18 @@ class FivePaisa(BrokerBase):
|
|
|
3220
3268
|
price.high = json_data.get("High", float("nan"))
|
|
3221
3269
|
price.low = json_data.get("Low", float("nan"))
|
|
3222
3270
|
price.volume = json_data.get("TotalQty", float("nan"))
|
|
3223
|
-
price.symbol =
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3271
|
+
price.symbol, resolved_exchange = resolve_symbol_and_exchange(json_data)
|
|
3272
|
+
if price.symbol is None:
|
|
3273
|
+
trading_logger.log_warning(
|
|
3274
|
+
"Skipping price data with unmapped token",
|
|
3275
|
+
{
|
|
3276
|
+
"token": json_data.get("Token"),
|
|
3277
|
+
"exchange": json_data.get("Exch"),
|
|
3278
|
+
"exchange_type": json_data.get("ExchType"),
|
|
3279
|
+
},
|
|
3280
|
+
)
|
|
3281
|
+
return None
|
|
3282
|
+
price.exchange = self.map_exchange_for_db(price.symbol, resolved_exchange)
|
|
3227
3283
|
price.timestamp = self.convert_to_ist(json_data["TickDt"])
|
|
3228
3284
|
return price
|
|
3229
3285
|
except Exception as e:
|
|
@@ -3237,10 +3293,10 @@ class FivePaisa(BrokerBase):
|
|
|
3237
3293
|
json_data = json.loads(data_str)
|
|
3238
3294
|
if len(json_data) == 1:
|
|
3239
3295
|
price = map_to_price(json_data[0])
|
|
3240
|
-
if ext_callback:
|
|
3296
|
+
if price is not None and ext_callback:
|
|
3241
3297
|
ext_callback(price)
|
|
3242
3298
|
except Exception as e:
|
|
3243
|
-
trading_logger.log_error("Error processing WebSocket message", e, {"
|
|
3299
|
+
trading_logger.log_error("Error processing WebSocket message", e, {"ws_message": message})
|
|
3244
3300
|
|
|
3245
3301
|
def error_data(ws, err):
|
|
3246
3302
|
"""Handle WebSocket errors."""
|
|
@@ -3281,6 +3337,52 @@ class FivePaisa(BrokerBase):
|
|
|
3281
3337
|
trading_logger.log_error("Error in connect_and_receive", e, {"req_data": req_data})
|
|
3282
3338
|
raise
|
|
3283
3339
|
|
|
3340
|
+
def has_live_stream():
|
|
3341
|
+
"""Return True only when the streaming thread and websocket are both usable."""
|
|
3342
|
+
return (
|
|
3343
|
+
self.subscribe_thread is not None
|
|
3344
|
+
and self.subscribe_thread.is_alive()
|
|
3345
|
+
and self.api is not None
|
|
3346
|
+
and getattr(self.api, "ws", None) is not None
|
|
3347
|
+
and callable(getattr(self.api.ws, "send", None))
|
|
3348
|
+
)
|
|
3349
|
+
|
|
3350
|
+
def reconnect_stream():
|
|
3351
|
+
"""Start a fresh websocket and restore all subscriptions."""
|
|
3352
|
+
try:
|
|
3353
|
+
if self.api is not None and getattr(self.api, "ws", None) is not None:
|
|
3354
|
+
self.api.close_data()
|
|
3355
|
+
except Exception:
|
|
3356
|
+
pass
|
|
3357
|
+
self.subscribe_thread = None
|
|
3358
|
+
req_list_full = expand_symbols_to_request(self.subscribed_symbols)
|
|
3359
|
+
if not req_list_full:
|
|
3360
|
+
context = create_error_context(operation=operation, symbols=symbols, exchange=exchange)
|
|
3361
|
+
raise MarketDataError("No symbols to reconnect after socket closure", context)
|
|
3362
|
+
if self.api is None:
|
|
3363
|
+
raise BrokerConnectionError("API client not initialized")
|
|
3364
|
+
req_data_full = self.api.Request_Feed("mf", "s", req_list_full)
|
|
3365
|
+
self.subscribe_thread = threading.Thread(
|
|
3366
|
+
target=connect_and_receive,
|
|
3367
|
+
args=(req_data_full,),
|
|
3368
|
+
name="MarketDataStreamer",
|
|
3369
|
+
)
|
|
3370
|
+
self.subscribe_thread.start()
|
|
3371
|
+
time.sleep(2)
|
|
3372
|
+
trading_logger.log_info(
|
|
3373
|
+
"Reconnected after socket closure",
|
|
3374
|
+
{"subscribed_count": len(self.subscribed_symbols)},
|
|
3375
|
+
)
|
|
3376
|
+
|
|
3377
|
+
def send_stream_request(req_data):
|
|
3378
|
+
"""Send an incremental subscribe/unsubscribe over an existing websocket."""
|
|
3379
|
+
if self.api is None:
|
|
3380
|
+
raise BrokerConnectionError("API client not initialized")
|
|
3381
|
+
ws = getattr(self.api, "ws", None)
|
|
3382
|
+
if ws is None or not callable(getattr(ws, "send", None)):
|
|
3383
|
+
raise AttributeError("WebSocket client does not support send")
|
|
3384
|
+
ws.send(json.dumps(req_data))
|
|
3385
|
+
|
|
3284
3386
|
def resolve_exchange_from_symbology(long_symbol: str):
|
|
3285
3387
|
"""Resolve API exchange code for a symbol from symbology (which exchange's symbol_map contains it)."""
|
|
3286
3388
|
for exch in self.exchange_mappings:
|
|
@@ -3367,7 +3469,7 @@ class FivePaisa(BrokerBase):
|
|
|
3367
3469
|
req_data = self.api.Request_Feed("mf", operation, req_list)
|
|
3368
3470
|
|
|
3369
3471
|
# Start the connection and receiving data in a separate thread
|
|
3370
|
-
if
|
|
3472
|
+
if not has_live_stream():
|
|
3371
3473
|
self.subscribe_thread = threading.Thread(
|
|
3372
3474
|
target=connect_and_receive, args=(req_data,), name="MarketDataStreamer"
|
|
3373
3475
|
)
|
|
@@ -3379,36 +3481,13 @@ class FivePaisa(BrokerBase):
|
|
|
3379
3481
|
"Requesting streaming for existing connection", {"req_data": json.dumps(req_data)}
|
|
3380
3482
|
)
|
|
3381
3483
|
try:
|
|
3382
|
-
|
|
3383
|
-
except WebSocketConnectionClosedException:
|
|
3484
|
+
send_stream_request(req_data)
|
|
3485
|
+
except (AttributeError, WebSocketConnectionClosedException):
|
|
3384
3486
|
trading_logger.log_info(
|
|
3385
3487
|
"WebSocket closed, reconnecting...",
|
|
3386
3488
|
{"operation": operation, "symbols_count": len(symbols), "exchange": exchange},
|
|
3387
3489
|
)
|
|
3388
|
-
|
|
3389
|
-
if self.api is not None:
|
|
3390
|
-
self.api.close_data()
|
|
3391
|
-
except Exception:
|
|
3392
|
-
pass
|
|
3393
|
-
self.subscribe_thread = None
|
|
3394
|
-
req_list_full = expand_symbols_to_request(self.subscribed_symbols)
|
|
3395
|
-
if not req_list_full:
|
|
3396
|
-
context = create_error_context(operation=operation, symbols=symbols, exchange=exchange)
|
|
3397
|
-
raise MarketDataError("No symbols to reconnect after socket closure", context)
|
|
3398
|
-
if self.api is None:
|
|
3399
|
-
raise BrokerConnectionError("API client not initialized")
|
|
3400
|
-
req_data_full = self.api.Request_Feed("mf", "s", req_list_full)
|
|
3401
|
-
self.subscribe_thread = threading.Thread(
|
|
3402
|
-
target=connect_and_receive,
|
|
3403
|
-
args=(req_data_full,),
|
|
3404
|
-
name="MarketDataStreamer",
|
|
3405
|
-
)
|
|
3406
|
-
self.subscribe_thread.start()
|
|
3407
|
-
time.sleep(2)
|
|
3408
|
-
trading_logger.log_info(
|
|
3409
|
-
"Reconnected after socket closure",
|
|
3410
|
-
{"subscribed_count": len(self.subscribed_symbols)},
|
|
3411
|
-
)
|
|
3490
|
+
reconnect_stream()
|
|
3412
3491
|
# New connection already has full subscription; no need to send again
|
|
3413
3492
|
else:
|
|
3414
3493
|
trading_logger.log_warning(
|
|
@@ -3440,6 +3519,7 @@ class FivePaisa(BrokerBase):
|
|
|
3440
3519
|
try:
|
|
3441
3520
|
if self.api is not None:
|
|
3442
3521
|
self.api.close_data()
|
|
3522
|
+
self.subscribe_thread = None
|
|
3443
3523
|
trading_logger.log_info("Streaming stopped successfully")
|
|
3444
3524
|
except Exception as e:
|
|
3445
3525
|
context = create_error_context(error=str(e))
|
|
@@ -902,16 +902,25 @@ class FlatTrade(BrokerBase):
|
|
|
902
902
|
|
|
903
903
|
cash_float = float(cash)
|
|
904
904
|
collateral = float(limits.get("collateral", 0))
|
|
905
|
+
used = 0.0
|
|
906
|
+
for field in ("usedmargin", "UsedMargin", "utilisedamount", "UtilisedAmount", "marginused", "MarginUsed"):
|
|
907
|
+
if field in limits:
|
|
908
|
+
try:
|
|
909
|
+
used = float(limits[field])
|
|
910
|
+
break
|
|
911
|
+
except (ValueError, TypeError):
|
|
912
|
+
continue
|
|
905
913
|
|
|
906
914
|
trading_logger.log_debug(
|
|
907
915
|
"Available capital retrieved",
|
|
908
916
|
{
|
|
909
917
|
"cash": cash_float,
|
|
910
918
|
"collateral": collateral,
|
|
919
|
+
"used": used,
|
|
911
920
|
"total_capital": cash_float + collateral,
|
|
912
921
|
},
|
|
913
922
|
)
|
|
914
|
-
return {"cash": cash_float, "collateral": collateral}
|
|
923
|
+
return {"cash": cash_float, "collateral": collateral, "used": used}
|
|
915
924
|
|
|
916
925
|
except (BrokerConnectionError, MarketDataError):
|
|
917
926
|
raise
|
|
@@ -796,16 +796,36 @@ class Shoonya(BrokerBase):
|
|
|
796
796
|
except (ValueError, TypeError):
|
|
797
797
|
continue
|
|
798
798
|
|
|
799
|
+
used = 0.0
|
|
800
|
+
used_fields = [
|
|
801
|
+
"usedmargin",
|
|
802
|
+
"UsedMargin",
|
|
803
|
+
"utilisedamount",
|
|
804
|
+
"UtilisedAmount",
|
|
805
|
+
"marginused",
|
|
806
|
+
"MarginUsed",
|
|
807
|
+
"spanused",
|
|
808
|
+
"SpanUsed",
|
|
809
|
+
]
|
|
810
|
+
for field in used_fields:
|
|
811
|
+
if field in limits:
|
|
812
|
+
try:
|
|
813
|
+
used = float(limits[field])
|
|
814
|
+
break
|
|
815
|
+
except (ValueError, TypeError):
|
|
816
|
+
continue
|
|
817
|
+
|
|
799
818
|
trading_logger.log_debug(
|
|
800
819
|
"Available capital retrieved",
|
|
801
820
|
{
|
|
802
821
|
"cash": cash_float,
|
|
803
822
|
"collateral": collateral,
|
|
823
|
+
"used": used,
|
|
804
824
|
"total_capital": cash_float + collateral,
|
|
805
825
|
"broker": self.broker.name,
|
|
806
826
|
},
|
|
807
827
|
)
|
|
808
|
-
return {"cash": cash_float, "collateral": collateral}
|
|
828
|
+
return {"cash": cash_float, "collateral": collateral, "used": used}
|
|
809
829
|
|
|
810
830
|
except (BrokerConnectionError, MarketDataError):
|
|
811
831
|
raise
|
|
@@ -368,6 +368,9 @@ def _safe_parse_json_or_dict(json_str):
|
|
|
368
368
|
}
|
|
369
369
|
return eval(json_str, safe_globals, {})
|
|
370
370
|
except Exception as eval_error:
|
|
371
|
+
parsed_loose_info = _parse_loose_additional_info(json_str)
|
|
372
|
+
if parsed_loose_info is not None:
|
|
373
|
+
return parsed_loose_info
|
|
371
374
|
raise DataError(
|
|
372
375
|
f"Cannot parse as JSON or Python dict: {str(e)}",
|
|
373
376
|
{
|
|
@@ -379,6 +382,57 @@ def _safe_parse_json_or_dict(json_str):
|
|
|
379
382
|
)
|
|
380
383
|
|
|
381
384
|
|
|
385
|
+
def _parse_loose_additional_info(info_str):
|
|
386
|
+
info_str = str(info_str).strip()
|
|
387
|
+
if not info_str:
|
|
388
|
+
return {}
|
|
389
|
+
|
|
390
|
+
import shlex
|
|
391
|
+
|
|
392
|
+
def _coerce_value(value):
|
|
393
|
+
lower_value = value.lower()
|
|
394
|
+
if lower_value == "true":
|
|
395
|
+
return True
|
|
396
|
+
if lower_value == "false":
|
|
397
|
+
return False
|
|
398
|
+
if lower_value == "none":
|
|
399
|
+
return None
|
|
400
|
+
try:
|
|
401
|
+
if any(ch in value for ch in [".", "e", "E"]):
|
|
402
|
+
return float(value)
|
|
403
|
+
return int(value)
|
|
404
|
+
except ValueError:
|
|
405
|
+
return value
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
tokens = shlex.split(info_str)
|
|
409
|
+
except ValueError:
|
|
410
|
+
tokens = info_str.split()
|
|
411
|
+
|
|
412
|
+
parsed = {}
|
|
413
|
+
message_tokens = []
|
|
414
|
+
for token in tokens:
|
|
415
|
+
token_parts = [part for part in token.split(",") if part]
|
|
416
|
+
consumed_key_value = False
|
|
417
|
+
for part in token_parts:
|
|
418
|
+
if "=" not in part:
|
|
419
|
+
if not consumed_key_value:
|
|
420
|
+
message_tokens.append(part)
|
|
421
|
+
continue
|
|
422
|
+
key, value = part.split("=", 1)
|
|
423
|
+
if not key:
|
|
424
|
+
continue
|
|
425
|
+
parsed[key] = _coerce_value(value)
|
|
426
|
+
consumed_key_value = True
|
|
427
|
+
if "=" not in token and not token_parts:
|
|
428
|
+
message_tokens.append(token)
|
|
429
|
+
|
|
430
|
+
if message_tokens:
|
|
431
|
+
parsed["message"] = " ".join(message_tokens)
|
|
432
|
+
|
|
433
|
+
return parsed or {"message": info_str}
|
|
434
|
+
|
|
435
|
+
|
|
382
436
|
@log_execution_time
|
|
383
437
|
def _merge_additional_info(old_info, new_info):
|
|
384
438
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|