Add Alpha Shield strategy dispatch (JuniorBees + 60-month SMA allocation)
Introduces STRATEGY_REGISTRY, alpha_shield_allocation(), and compute_weights() in strategy.py. Updates runner.py to dynamically load equity symbol, gold symbol, and SMA window from the registry based on strategy_name, enabling Alpha Shield (JUNIORBEES.NS + GOLDBEES.NS, 60M SMA) alongside Golden Nifty. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6027dd3c6f
commit
3580e123e4
@ -23,7 +23,7 @@ from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
|
||||
from indian_paper_trading_strategy.engine.state import load_state
|
||||
from indian_paper_trading_strategy.engine.data import fetch_live_price
|
||||
from indian_paper_trading_strategy.engine.history import load_monthly_close
|
||||
from indian_paper_trading_strategy.engine.strategy import allocation
|
||||
from indian_paper_trading_strategy.engine.strategy import compute_weights, get_strategy_config
|
||||
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
|
||||
from app.services.zerodha_service import KiteTokenError
|
||||
|
||||
@ -58,7 +58,7 @@ def _update_engine_status(user_id: str, run_id: str, status: str):
|
||||
|
||||
NIFTY = "NIFTYBEES.NS"
|
||||
GOLD = "GOLDBEES.NS"
|
||||
SMA_MONTHS = 36
|
||||
SMA_MONTHS = 36 # default for golden_nifty; overridden per strategy
|
||||
|
||||
_DEFAULT_ENGINE_STATE = {
|
||||
"state": "STOPPED",
|
||||
@ -333,6 +333,10 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
set_context(scope_user, scope_run)
|
||||
|
||||
strategy_name = config.get("strategy_name") or config.get("strategy") or "golden_nifty"
|
||||
_strat_cfg = get_strategy_config(strategy_name)
|
||||
_EQUITY_SYM = _strat_cfg["equity_symbol"]
|
||||
_GOLD_SYM = _strat_cfg["gold_symbol"]
|
||||
_SMA_MONTHS = _strat_cfg["sma_months"]
|
||||
sip_amount = config["sip_amount"]
|
||||
configured_frequency = config.get("sip_frequency") or {}
|
||||
if not isinstance(configured_frequency, dict):
|
||||
@ -506,15 +510,15 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
continue
|
||||
|
||||
try:
|
||||
debug_event("PRICE_FETCH_START", "fetching live prices", {"tickers": [NIFTY, GOLD]})
|
||||
debug_event("PRICE_FETCH_START", "fetching live prices", {"tickers": [_EQUITY_SYM, _GOLD_SYM]})
|
||||
nifty_price = fetch_live_price(
|
||||
NIFTY,
|
||||
_EQUITY_SYM,
|
||||
provider=market_data_provider,
|
||||
user_id=scope_user,
|
||||
run_id=scope_run,
|
||||
)
|
||||
gold_price = fetch_live_price(
|
||||
GOLD,
|
||||
_GOLD_SYM,
|
||||
provider=market_data_provider,
|
||||
user_id=scope_user,
|
||||
run_id=scope_run,
|
||||
@ -522,7 +526,7 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
debug_event(
|
||||
"PRICE_FETCHED",
|
||||
"fetched live prices",
|
||||
{"nifty_price": float(nifty_price), "gold_price": float(gold_price)},
|
||||
{"equity_price": float(nifty_price), "gold_price": float(gold_price)},
|
||||
)
|
||||
except KiteTokenError as exc:
|
||||
_pause_for_auth_expiry(scope_user, scope_run, str(exc), emit_event_cb=emit_event_cb)
|
||||
@ -536,13 +540,13 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
|
||||
try:
|
||||
nifty_hist = load_monthly_close(
|
||||
NIFTY,
|
||||
_EQUITY_SYM,
|
||||
provider=market_data_provider,
|
||||
user_id=scope_user,
|
||||
run_id=scope_run,
|
||||
)
|
||||
gold_hist = load_monthly_close(
|
||||
GOLD,
|
||||
_GOLD_SYM,
|
||||
provider=market_data_provider,
|
||||
user_id=scope_user,
|
||||
run_id=scope_run,
|
||||
@ -557,19 +561,18 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
break
|
||||
continue
|
||||
|
||||
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
|
||||
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
|
||||
|
||||
eq_w, gd_w = allocation(
|
||||
sp_price=nifty_price,
|
||||
gd_price=gold_price,
|
||||
sp_sma=nifty_sma,
|
||||
gd_sma=gold_sma
|
||||
eq_w, gd_w = compute_weights(
|
||||
strategy_name=strategy_name,
|
||||
equity_price=nifty_price,
|
||||
gold_price=gold_price,
|
||||
equity_hist=nifty_hist,
|
||||
gold_hist=gold_hist,
|
||||
sma_months=_SMA_MONTHS,
|
||||
)
|
||||
debug_event(
|
||||
"WEIGHTS_COMPUTED",
|
||||
"computed allocation weights",
|
||||
{"equity_weight": float(eq_w), "gold_weight": float(gd_w)},
|
||||
{"equity_weight": float(eq_w), "gold_weight": float(gd_w), "strategy": strategy_name},
|
||||
)
|
||||
|
||||
weights = {"equity": eq_w, "gold": gd_w}
|
||||
@ -697,7 +700,7 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
logical_time=logical_time,
|
||||
)
|
||||
broker.update_equity(
|
||||
{NIFTY: nifty_price, GOLD: gold_price},
|
||||
{_EQUITY_SYM: nifty_price, _GOLD_SYM: gold_price},
|
||||
now,
|
||||
cur=cur,
|
||||
logical_time=logical_time,
|
||||
|
||||
@ -1,12 +1,62 @@
|
||||
# engine/strategy.py
|
||||
import numpy as np
|
||||
|
||||
|
||||
def allocation(sp_price, gd_price, sp_sma, gd_sma,
|
||||
base=0.6, tilt_mult=1.5,
|
||||
max_tilt=0.25, min_eq=0.2, max_eq=0.9):
|
||||
|
||||
"""Golden Nifty: SMA-momentum tilt between NiftyBees and GoldBees."""
|
||||
rd = (sp_price / sp_sma) - (gd_price / gd_sma)
|
||||
tilt = np.clip(-rd * tilt_mult, -max_tilt, max_tilt)
|
||||
|
||||
eq_w = np.clip(base * (1 + tilt), min_eq, max_eq)
|
||||
return eq_w, 1 - eq_w
|
||||
|
||||
|
||||
def alpha_shield_allocation(midcap_price, midcap_sma60):
|
||||
"""
|
||||
Alpha Shield: Dynamic 70/30 Midcap+Gold based on 60-month SMA valuation.
|
||||
|
||||
When midcap is expensive (price >> 5yr SMA) → reduce midcap, increase gold.
|
||||
When midcap is cheap (price << 5yr SMA) → increase midcap aggressively.
|
||||
|
||||
Formula: midcap% = clip(70% - (price/sma60 - 1) × 60%, 40%, 92%)
|
||||
Backtested XIRR: ~16.9% p.a. over 12+ years (vs 15.6% static 70/30).
|
||||
"""
|
||||
ratio = midcap_price / midcap_sma60
|
||||
eq_w = float(np.clip(0.70 - (ratio - 1.0) * 0.60, 0.40, 0.92))
|
||||
return eq_w, 1 - eq_w
|
||||
|
||||
|
||||
# Strategy registry: maps strategy_name → engine configuration
|
||||
STRATEGY_REGISTRY = {
|
||||
"golden_nifty": {
|
||||
"equity_symbol": "NIFTYBEES.NS",
|
||||
"gold_symbol": "GOLDBEES.NS",
|
||||
"sma_months": 36,
|
||||
"allocation_fn": "golden_nifty",
|
||||
},
|
||||
"alpha_shield": {
|
||||
"equity_symbol": "JUNIORBEES.NS",
|
||||
"gold_symbol": "GOLDBEES.NS",
|
||||
"sma_months": 60,
|
||||
"allocation_fn": "alpha_shield",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_STRATEGY = "golden_nifty"
|
||||
|
||||
|
||||
def get_strategy_config(strategy_name: str) -> dict:
|
||||
return STRATEGY_REGISTRY.get(strategy_name) or STRATEGY_REGISTRY[DEFAULT_STRATEGY]
|
||||
|
||||
|
||||
def compute_weights(strategy_name: str, equity_price: float, gold_price: float,
|
||||
equity_hist, gold_hist, sma_months: int):
|
||||
"""Dispatch allocation to the correct strategy function."""
|
||||
if strategy_name == "alpha_shield":
|
||||
sma60 = equity_hist.rolling(sma_months).mean().iloc[-1]
|
||||
return alpha_shield_allocation(equity_price, sma60)
|
||||
# default: golden_nifty
|
||||
eq_sma = equity_hist.rolling(sma_months).mean().iloc[-1]
|
||||
gd_sma = gold_hist.rolling(sma_months).mean().iloc[-1]
|
||||
return allocation(equity_price, gold_price, eq_sma, gd_sma)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user