tradingapi 0.3.4__tar.gz → 0.3.6__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.3.4 → tradingapi-0.3.6}/PKG-INFO +1 -1
- {tradingapi-0.3.4 → tradingapi-0.3.6}/pyproject.toml +1 -1
- tradingapi-0.3.6/tests/test_calculate_delta_realtime_quotes.py +29 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/allocation.py +18 -4
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/fivepaisa.py +8 -12
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/utils.py +98 -28
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/PKG-INFO +1 -1
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/SOURCES.txt +1 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/README.md +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/setup.cfg +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tests/test_find_option_with_delta.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/__init__.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/attribution.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/broker_base.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/config.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/dhan.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/flattrade.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/globals.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/shoonya.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi/span.py +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.3.4 → tradingapi-0.3.6}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import sys
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, "/home/psharma/onedrive/code/tradingapi2")
|
|
8
|
+
|
|
9
|
+
from tradingapi import utils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CalculateDeltaRealtimeQuotesTest(unittest.TestCase):
|
|
13
|
+
def test_calculate_delta_returns_nan_without_live_bid_ask(self):
|
|
14
|
+
ticker = SimpleNamespace(bid=0.0, ask=0.0, prior_close=123.45)
|
|
15
|
+
|
|
16
|
+
with patch.object(utils, "get_price", return_value=ticker):
|
|
17
|
+
delta = utils.calculate_delta(
|
|
18
|
+
brokers=[],
|
|
19
|
+
long_symbol="TCS_OPT_20260630_PUT_2160",
|
|
20
|
+
price_f=2325.1,
|
|
21
|
+
exchange="NFO",
|
|
22
|
+
mds="mds",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
self.assertTrue(math.isnan(delta))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
unittest.main()
|
|
@@ -21,13 +21,15 @@ _ALLOCATIONS_DIR = Path(os.path.expanduser("~/onedrive/.tradingapi/allocations")
|
|
|
21
21
|
_TODAY_SYMLINK = _ALLOCATIONS_DIR / "master_allocation_today.yaml"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
"""Return the
|
|
24
|
+
def load_today_allocation_entry(broker_name: str, strategy_name: str) -> dict:
|
|
25
|
+
"""Return the full allocation entry dict for strategy_name on broker_name today.
|
|
26
|
+
|
|
27
|
+
Example return value: {'pct': 0.225, 'redis_db': 8}
|
|
28
|
+
SCALPING strategies include a 'redis_db' key; others have only 'pct'.
|
|
26
29
|
|
|
27
30
|
Reads master_allocation_today.yaml (a symlink to today's dated file).
|
|
28
31
|
Raises FileNotFoundError if the symlink/file is missing.
|
|
29
32
|
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
33
|
Raises KeyError if the broker/strategy is not found in the file at all.
|
|
32
34
|
"""
|
|
33
35
|
if not _TODAY_SYMLINK.exists():
|
|
@@ -63,7 +65,19 @@ def load_today_allocation(broker_name: str, strategy_name: str) -> float:
|
|
|
63
65
|
f"Available strategies: {list(strategies.keys())}"
|
|
64
66
|
)
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
return dict(strategies[strategy_key])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_today_allocation(broker_name: str, strategy_name: str) -> float:
|
|
72
|
+
"""Return the pct (0.0–1.0) allocated to strategy_name on broker_name today.
|
|
73
|
+
|
|
74
|
+
Reads master_allocation_today.yaml (a symlink to today's dated file).
|
|
75
|
+
Raises FileNotFoundError if the symlink/file is missing.
|
|
76
|
+
Raises ValueError if the date in the file does not match today.
|
|
77
|
+
Returns 0.0 if the strategy is listed with pct == 0 (inactive today).
|
|
78
|
+
Raises KeyError if the broker/strategy is not found in the file at all.
|
|
79
|
+
"""
|
|
80
|
+
entry = load_today_allocation_entry(broker_name, strategy_name)
|
|
67
81
|
return float(entry.get("pct", 0.0))
|
|
68
82
|
|
|
69
83
|
|
|
@@ -3738,12 +3738,11 @@ class FivePaisa(BrokerBase):
|
|
|
3738
3738
|
raise BrokerConnectionError(
|
|
3739
3739
|
"FivePaisa stream session helpers not initialized; connect() must succeed before streaming"
|
|
3740
3740
|
)
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
fresh_fn(token_path)
|
|
3741
|
+
# Always do a full TOTP re-login for stream reconnects.
|
|
3742
|
+
# restore_fn succeeds (REST verifies OK) but FivePaisa's WS server silently
|
|
3743
|
+
# rejects streaming on a token-restored session — subscriptions are accepted
|
|
3744
|
+
# but no ticks are ever delivered. fresh_fn gets a genuine new session.
|
|
3745
|
+
fresh_fn(token_path)
|
|
3747
3746
|
req_list_full = expand_symbols_to_request(self.subscribed_symbols)
|
|
3748
3747
|
if not req_list_full:
|
|
3749
3748
|
context = create_error_context(operation=operation, symbols=symbols, exchange=exchange)
|
|
@@ -3860,12 +3859,9 @@ class FivePaisa(BrokerBase):
|
|
|
3860
3859
|
|
|
3861
3860
|
# Start the connection and receiving data in a separate thread
|
|
3862
3861
|
if not has_live_stream():
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
)
|
|
3866
|
-
self.subscribe_thread.start()
|
|
3867
|
-
time.sleep(2)
|
|
3868
|
-
trading_logger.log_info("Streaming thread started", {"thread_name": "MarketDataStreamer"})
|
|
3862
|
+
# WS is dead — full reconnect restoring all subscribed_symbols (already updated above).
|
|
3863
|
+
# Connecting with just the current op's req_data would lose all prior subscriptions.
|
|
3864
|
+
reconnect_stream()
|
|
3869
3865
|
else:
|
|
3870
3866
|
trading_logger.log_info(
|
|
3871
3867
|
"Requesting streaming for existing connection", {"req_data": json.dumps(req_data)}
|
|
@@ -93,7 +93,7 @@ empty_trades = pd.DataFrame(
|
|
|
93
93
|
"exit_time": pd.Series(dtype="string"),
|
|
94
94
|
"exit_quantity": pd.Series(dtype="float64"),
|
|
95
95
|
"exit_price": pd.Series(dtype="float64"),
|
|
96
|
-
"commission": pd.Series(dtype="
|
|
96
|
+
"commission": pd.Series(dtype="float64"),
|
|
97
97
|
"int_order_id": pd.Series(dtype="object"),
|
|
98
98
|
"entry_keys": pd.Series(dtype="object"),
|
|
99
99
|
"exit_keys": pd.Series(dtype="object"),
|
|
@@ -2000,10 +2000,14 @@ def update_order_status(
|
|
|
2000
2000
|
for attr in required_attributes:
|
|
2001
2001
|
if not hasattr(fills, attr) or getattr(fills, attr) in [None, "0"]:
|
|
2002
2002
|
attr_value = getattr(fills, attr, "<missing>")
|
|
2003
|
-
|
|
2003
|
+
msg = (
|
|
2004
2004
|
f"Missing or invalid attribute {attr} in order information for broker_order_id: {broker_order_id}. "
|
|
2005
2005
|
f"value={attr_value!r}, value_type={type(attr_value).__name__}"
|
|
2006
2006
|
)
|
|
2007
|
+
if attr == "exchange_order_id":
|
|
2008
|
+
trading_logger.log_info(msg)
|
|
2009
|
+
else:
|
|
2010
|
+
trading_logger.log_error(msg)
|
|
2007
2011
|
return fills
|
|
2008
2012
|
|
|
2009
2013
|
if broker.broker != fills.broker:
|
|
@@ -3175,8 +3179,10 @@ def calculate_delta(
|
|
|
3175
3179
|
):
|
|
3176
3180
|
delta = float("nan")
|
|
3177
3181
|
if as_of is None:
|
|
3178
|
-
ticker = get_price(brokers, long_symbol, checks=["bid", "ask"
|
|
3179
|
-
|
|
3182
|
+
ticker = get_price(brokers, long_symbol, checks=["bid", "ask"], exchange=exchange, mds=mds)
|
|
3183
|
+
if not (ticker.bid > 0 and ticker.ask > 0):
|
|
3184
|
+
return float("nan")
|
|
3185
|
+
price = (ticker.bid + ticker.ask) / 2
|
|
3180
3186
|
t = (
|
|
3181
3187
|
calc_fractional_business_days(
|
|
3182
3188
|
get_tradingapi_now(),
|
|
@@ -3284,30 +3290,14 @@ def find_option_with_delta(
|
|
|
3284
3290
|
)
|
|
3285
3291
|
return -1
|
|
3286
3292
|
|
|
3287
|
-
#
|
|
3288
|
-
|
|
3289
|
-
seed_indices = sorted({min(n - 1, max(0, int(round(f * (n - 1))))) for f in seed_fractions})
|
|
3290
|
-
seed_samples: list[tuple[int, float]] = [] # (idx, |delta|)
|
|
3291
|
-
for idx in seed_indices:
|
|
3292
|
-
d = _delta_at(idx)
|
|
3293
|
-
if not math.isnan(d):
|
|
3294
|
-
ad = abs(d)
|
|
3295
|
-
seed_samples.append((idx, ad))
|
|
3296
|
-
_consider(idx, ad)
|
|
3297
|
-
|
|
3298
|
-
if len(seed_samples) < 2:
|
|
3299
|
-
if best_index < 0:
|
|
3300
|
-
trading_logger.log_warning(
|
|
3301
|
-
f"find_option_with_delta: no valid option strike found (seed failure). "
|
|
3302
|
-
f"price_f={price_f} target_delta={target_delta} return_lower_delta={return_lower_delta} "
|
|
3303
|
-
f"chain_len={n} strike_range=[{strike_lo}, {strike_hi}] "
|
|
3304
|
-
f"valid_delta_count={valid_delta_count} nan_delta_count={nan_delta_count} "
|
|
3305
|
-
f"total_delta_checks={total_delta_checks} nan_delta_strikes={sorted(set(nan_delta_strikes))}"
|
|
3306
|
-
)
|
|
3307
|
-
return best_index
|
|
3293
|
+
# Monotonicity is deterministic from option type: calls lose delta as strike rises, puts gain it.
|
|
3294
|
+
increasing = "_PUT_" in option_chain[0]
|
|
3308
3295
|
|
|
3309
|
-
#
|
|
3310
|
-
|
|
3296
|
+
# Seed at midpoint (near ATM) — most liquid, least likely to have stale data.
|
|
3297
|
+
mid_seed = n // 2
|
|
3298
|
+
d = _delta_at(mid_seed)
|
|
3299
|
+
if not math.isnan(d):
|
|
3300
|
+
_consider(mid_seed, abs(d))
|
|
3311
3301
|
|
|
3312
3302
|
# --- Change 2: NaN at mid -> scan to nearest valid neighbor instead of shrinking range ---
|
|
3313
3303
|
NAN_SCAN_CAP = 5
|
|
@@ -3382,6 +3372,7 @@ def get_delta_strike(
|
|
|
3382
3372
|
exchange="NSE",
|
|
3383
3373
|
mds: Optional[str] = None,
|
|
3384
3374
|
as_of: Optional[Union[dt.datetime, dt.date, str, pd.Timestamp]] = None,
|
|
3375
|
+
max_moneyness: float = 0.07,
|
|
3385
3376
|
) -> Union[str, None]:
|
|
3386
3377
|
"""Get option strike price for a given delta with enhanced error handling.
|
|
3387
3378
|
|
|
@@ -3403,6 +3394,10 @@ def get_delta_strike(
|
|
|
3403
3394
|
as_of: Optional. When omitted (default), behavior matches pre-change realtime paths.
|
|
3404
3395
|
When set, underlying and option marks use historical OHLC at that time; T for IV/delta
|
|
3405
3396
|
uses ``as_of`` instead of now.
|
|
3397
|
+
max_moneyness: Maximum allowed deviation of the found strike from the underlying price,
|
|
3398
|
+
as a fraction (e.g. 0.07 = 7%). Applies symmetrically to ITM and OTM strikes.
|
|
3399
|
+
Rejects results where stale market data produced a plausible-looking but wrong delta.
|
|
3400
|
+
Defaults to 0.07.
|
|
3406
3401
|
|
|
3407
3402
|
Returns:
|
|
3408
3403
|
Union[str, None]: Option symbol or None if not found
|
|
@@ -3473,7 +3468,17 @@ def get_delta_strike(
|
|
|
3473
3468
|
as_of=as_of,
|
|
3474
3469
|
)
|
|
3475
3470
|
if index >= 0:
|
|
3476
|
-
|
|
3471
|
+
sym = option_chain[index]
|
|
3472
|
+
strike = float(sym.split("_")[4])
|
|
3473
|
+
deviation = abs(strike / price_f - 1.0)
|
|
3474
|
+
if deviation > max_moneyness:
|
|
3475
|
+
trading_logger.log_warning(
|
|
3476
|
+
f"get_delta_strike: moneyness sanity check failed. "
|
|
3477
|
+
f"strike={strike} price_f={price_f} deviation={deviation:.1%} max_moneyness={max_moneyness:.1%} "
|
|
3478
|
+
f"delta={delta} option_type={option_type} symbol={sym}"
|
|
3479
|
+
)
|
|
3480
|
+
return None
|
|
3481
|
+
return sym
|
|
3477
3482
|
else:
|
|
3478
3483
|
trading_logger.log_debug(f"get_delta_strike: no option found (index={index})")
|
|
3479
3484
|
return None
|
|
@@ -4597,3 +4602,68 @@ def register_strategy_capital(
|
|
|
4597
4602
|
except Exception as e:
|
|
4598
4603
|
trading_logger.log_error(f"Failed to register strategy capital: {e}", exc_info=True)
|
|
4599
4604
|
return False
|
|
4605
|
+
|
|
4606
|
+
|
|
4607
|
+
def close_all_positions(
|
|
4608
|
+
broker: BrokerBase,
|
|
4609
|
+
strategy: str,
|
|
4610
|
+
exchange: str,
|
|
4611
|
+
paper: bool,
|
|
4612
|
+
refresh_status: bool = True,
|
|
4613
|
+
price_types: list[str] = ["LMT"],
|
|
4614
|
+
exclude_today_expiry: bool = False,
|
|
4615
|
+
publish: bool = True,
|
|
4616
|
+
additional_infos: list[str] | None = None,
|
|
4617
|
+
) -> list[str]:
|
|
4618
|
+
"""Close all open positions for a strategy by placing exit orders.
|
|
4619
|
+
|
|
4620
|
+
Reads open positions from get_pnl_table (authoritative Redis state, not in-memory cache),
|
|
4621
|
+
places one exit order per open position, then publishes updated trades to Redis.
|
|
4622
|
+
|
|
4623
|
+
Args:
|
|
4624
|
+
broker: Broker instance used for order placement and Redis access.
|
|
4625
|
+
strategy: Strategy name.
|
|
4626
|
+
exchange: Exchange string (e.g. "NSE", "BSE") passed to place_combo_order.
|
|
4627
|
+
paper: Paper trading flag passed through to place_combo_order.
|
|
4628
|
+
price_types: Order price type(s), e.g. ["LMT"], ["MKT"], ["BID+0.05"].
|
|
4629
|
+
exclude_today_expiry: If True, skip symbols whose expiry date is today or earlier.
|
|
4630
|
+
publish: Whether to publish trades to Redis after placing orders.
|
|
4631
|
+
additional_infos: Per-order metadata list passed to place_combo_order (e.g. [json.dumps({"message": "..."})]).
|
|
4632
|
+
|
|
4633
|
+
Returns:
|
|
4634
|
+
List of symbols for which exit orders were successfully placed.
|
|
4635
|
+
"""
|
|
4636
|
+
closed_symbols: list[str] = []
|
|
4637
|
+
pnl = get_pnl_table(broker, strategy, refresh_status=refresh_status)
|
|
4638
|
+
if pnl.empty:
|
|
4639
|
+
return closed_symbols
|
|
4640
|
+
open_rows = pnl[(pnl["entry_quantity"].fillna(0) + pnl["exit_quantity"].fillna(0)) != 0]
|
|
4641
|
+
for _, row in open_rows.iterrows():
|
|
4642
|
+
symbol = str(row["symbol"])
|
|
4643
|
+
if exclude_today_expiry:
|
|
4644
|
+
parts = symbol.split("_")
|
|
4645
|
+
if len(parts) > 2 and parts[2] <= dt.datetime.today().strftime("%Y%m%d"):
|
|
4646
|
+
trading_logger.log_info(f"close_all_positions: skipping today-expiry symbol {symbol}")
|
|
4647
|
+
continue
|
|
4648
|
+
qty = int(float(row["entry_quantity"]) + float(row["exit_quantity"]))
|
|
4649
|
+
if qty == 0:
|
|
4650
|
+
continue
|
|
4651
|
+
try:
|
|
4652
|
+
place_combo_order(
|
|
4653
|
+
execution_broker=broker,
|
|
4654
|
+
strategy=strategy,
|
|
4655
|
+
symbols=[symbol],
|
|
4656
|
+
quantities=[-qty],
|
|
4657
|
+
entry=False,
|
|
4658
|
+
exchanges=[exchange],
|
|
4659
|
+
price_types=price_types,
|
|
4660
|
+
paper=paper,
|
|
4661
|
+
additional_infos=additional_infos if additional_infos is not None else [""],
|
|
4662
|
+
)
|
|
4663
|
+
trading_logger.log_info(f"close_all_positions: exit placed {symbol} qty={qty}")
|
|
4664
|
+
closed_symbols.append(symbol)
|
|
4665
|
+
except Exception as e:
|
|
4666
|
+
trading_logger.log_error(f"close_all_positions: failed to place exit for {symbol} qty={qty}: {e}")
|
|
4667
|
+
if publish:
|
|
4668
|
+
publish_trades_to_redis(broker, strategy, publish=not paper)
|
|
4669
|
+
return closed_symbols
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|