| """ |
| STRATA-LOOP: Closed-Loop Weight Adaptation |
| ------------------------------------------- |
| Receives trade outcomes (P&L signals) and adapts DEFAULT_WEIGHTS |
| incrementally using a gradient-free, sign-based update rule. |
| |
| Design principles: |
| - No backprop. No neural net. Fully auditable. |
| - Each weight nudged ±STEP based on which direction reduced error. |
| - Adaptation rate decays over time (cooling schedule). |
| - Weights stay within [MIN, MAX] bounds — never drift to degenerate values. |
| - All updates logged with reason for transparency. |
| |
| Integration: |
| loop = StrataLOOP() |
| # After each closed trade: |
| loop.record(outcome) # outcome: dict from StrataOUTCOME |
| # Periodically: |
| new_weights = loop.weights # inject into update_state(weights=...) |
| |
| Outcome dict schema: |
| { |
| "action": "LONG" | "SHORT" | "HOLD", |
| "pnl": float, # signed P&L in price units |
| "pnl_pct": float, # P&L as % of entry price |
| "regime": str, # regime label at entry |
| "confidence": float, # confidence at entry |
| "state_snap": dict, # full state at entry |
| "sense_snap": dict, # sense signals at entry |
| } |
| """ |
|
|
| import math |
| import copy |
| from typing import Dict, List, Optional |
|
|
| from .core import DEFAULT_WEIGHTS, WEIGHTS_V1 |
|
|
|
|
| |
| |
| |
| LOOP_CONFIG = { |
| "step": 0.005, |
| "min_step": 0.001, |
| "cooling": 0.995, |
| "window": 50, |
| "update_every": 10, |
| "max_adapt": 0.30, |
| "min_weight": 0.01, |
| } |
|
|
| |
| |
| ADAPTABLE_WEIGHTS = { |
| "w_trend_bias": "+", |
| "w_vol_uncertainty": "+", |
| "w_break_momentum": "+", |
| "w_fake_break_bias": "+", |
| "w_trend_str_bias": "+", |
| "alpha": None, |
| "beta": None, |
| "gamma": None, |
| "c_bias": "+", |
| "c_momentum": "+", |
| "c_trap": "-", |
| "decay_bias": None, |
| "decay_momentum": None, |
| "decay_trap_risk": None, |
| "decay_uncertainty": None, |
| "regime_momentum_mix": None, |
| } |
|
|
|
|
| class StrataLOOP: |
| """ |
| Closed-loop weight adaptation engine. |
| |
| Observes trade outcomes and nudges weights toward configurations |
| that historically produced better P&L in the current regime. |
| """ |
|
|
| def __init__(self, seed_weights: Optional[Dict] = None): |
| self.weights = copy.deepcopy(seed_weights or DEFAULT_WEIGHTS) |
| self._seed = copy.deepcopy(self.weights) |
| self._step = LOOP_CONFIG["step"] |
| self._outcomes: List[Dict] = [] |
| self._update_count = 0 |
| self._log: List[Dict] = [] |
|
|
| |
| |
| |
|
|
| def record(self, outcome: Dict) -> None: |
| """Record a trade outcome. Triggers adaptation every N outcomes.""" |
| self._outcomes.append(outcome) |
| window = LOOP_CONFIG["window"] |
| if len(self._outcomes) > window: |
| self._outcomes = self._outcomes[-window:] |
|
|
| if len(self._outcomes) % LOOP_CONFIG["update_every"] == 0: |
| self._adapt() |
|
|
| def snapshot(self) -> Dict: |
| """Return current weights for injection into update_state.""" |
| return copy.deepcopy(self.weights) |
|
|
| def reset(self) -> None: |
| """Reset weights back to seed (V2 defaults).""" |
| self.weights = copy.deepcopy(self._seed) |
| self._outcomes.clear() |
| self._step = LOOP_CONFIG["step"] |
| self._update_count = 0 |
|
|
| def summary(self) -> Dict: |
| """Return adaptation statistics.""" |
| diffs = { |
| k: round(self.weights[k] - self._seed[k], 5) |
| for k in self.weights |
| } |
| recent_pnl = [o["pnl_pct"] for o in self._outcomes if "pnl_pct" in o] |
| return { |
| "update_count": self._update_count, |
| "current_step": round(self._step, 6), |
| "outcomes_seen": len(self._outcomes), |
| "recent_win_rate": self._win_rate(self._outcomes), |
| "weight_deltas": diffs, |
| "log_tail": self._log[-5:], |
| } |
|
|
| |
| |
| |
|
|
| def _adapt(self) -> None: |
| """Core adaptation step — sign-based gradient-free update.""" |
| outcomes = self._outcomes |
| if len(outcomes) < LOOP_CONFIG["update_every"]: |
| return |
|
|
| recent = outcomes[-LOOP_CONFIG["update_every"]:] |
| prior = outcomes[:-LOOP_CONFIG["update_every"]] or outcomes |
|
|
| recent_wr = self._win_rate(recent) |
| prior_wr = self._win_rate(prior) |
| improving = recent_wr >= prior_wr |
|
|
| |
| regime_pnl = {} |
| for o in recent: |
| r = o.get("regime", "TRANSITIONING") |
| regime_pnl.setdefault(r, []).append(o.get("pnl_pct", 0.0)) |
|
|
| |
| for key, direction in ADAPTABLE_WEIGHTS.items(): |
| if direction is None or key not in self.weights: |
| continue |
| self._nudge_weight(key, direction, improving, recent, prior_wr) |
|
|
| |
| self._normalise_conflict_weights() |
|
|
| |
| self._step = max(LOOP_CONFIG["min_step"], self._step * LOOP_CONFIG["cooling"]) |
| self._update_count += 1 |
|
|
| self._log.append({ |
| "update": self._update_count, |
| "recent_wr": round(recent_wr, 4), |
| "prior_wr": round(prior_wr, 4), |
| "improving": improving, |
| "step": round(self._step, 6), |
| }) |
|
|
| def _nudge_weight( |
| self, |
| key: str, |
| direction: str, |
| improving: bool, |
| recent: List[Dict], |
| prior_wr: float, |
| ) -> None: |
| """Nudge a single weight based on performance signal.""" |
| current = self.weights[key] |
| seed = self._seed[key] |
| max_dev = LOOP_CONFIG["max_adapt"] |
|
|
| |
| |
| win_signal = self._weight_win_correlation(key, recent) |
|
|
| |
| if direction == "+": |
| nudge = +self._step if win_signal > 0 else -self._step |
| else: |
| nudge = -self._step if win_signal > 0 else +self._step |
|
|
| |
| if not improving: |
| nudge = -nudge |
|
|
| new_val = current + nudge |
|
|
| |
| new_val = max(LOOP_CONFIG["min_weight"], new_val) |
| new_val = max(seed - max_dev, min(seed + max_dev, new_val)) |
|
|
| self.weights[key] = round(new_val, 6) |
|
|
| def _normalise_conflict_weights(self) -> None: |
| """Keep alpha + beta + gamma = 1.0 after adaptation.""" |
| a = self.weights.get("alpha", 0.30) |
| b = self.weights.get("beta", 0.35) |
| g = self.weights.get("gamma", 0.35) |
| total = a + b + g |
| if abs(total - 1.0) > 0.001 and total > 0: |
| self.weights["alpha"] = round(a / total, 6) |
| self.weights["beta"] = round(b / total, 6) |
| self.weights["gamma"] = round(g / total, 6) |
|
|
| def _weight_win_correlation(self, key: str, outcomes: List[Dict]) -> float: |
| """ |
| Heuristic: compare average relevant state/sense value in winning |
| vs losing trades. Returns sign of correlation. |
| Positive → weight should be increased when winning. |
| """ |
| |
| weight_to_field = { |
| "w_trend_bias": ("sense_snap", "trend"), |
| "w_vol_uncertainty": ("sense_snap", "vol"), |
| "w_break_momentum": ("sense_snap", "break_structure"), |
| "w_fake_break_bias": ("sense_snap", "break_structure"), |
| "w_trend_str_bias": ("sense_snap", "trend"), |
| "c_bias": ("state_snap", "bias"), |
| "c_momentum": ("state_snap", "momentum"), |
| "c_trap": ("state_snap", "trap_risk"), |
| } |
|
|
| if key not in weight_to_field: |
| return 0.0 |
|
|
| snap_type, field = weight_to_field[key] |
|
|
| wins = [o for o in outcomes if o.get("pnl_pct", 0) > 0] |
| loses = [o for o in outcomes if o.get("pnl_pct", 0) <= 0] |
|
|
| def avg_field(group): |
| vals = [o.get(snap_type, {}).get(field, 0.0) for o in group] |
| return sum(vals) / len(vals) if vals else 0.0 |
|
|
| if not wins or not loses: |
| return 0.0 |
|
|
| return avg_field(wins) - avg_field(loses) |
|
|
| @staticmethod |
| def _win_rate(outcomes: List[Dict]) -> float: |
| if not outcomes: |
| return 0.0 |
| wins = sum(1 for o in outcomes if o.get("pnl_pct", 0) > 0) |
| return wins / len(outcomes) |
|
|
| def __repr__(self): |
| s = self.summary() |
| return ( |
| f"STRATA-LOOP | updates={s['update_count']}" |
| f" | recent_wr={s['recent_win_rate']:.2%}" |
| f" | step={s['current_step']:.5f}" |
| f" | outcomes={s['outcomes_seen']}" |
| ) |
|
|