Download strata/loop.py from emylton/strata-net: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/emylton/strata-net/resolve/main/strata/loop.py
- Command line
-
hf download hf://emylton/strata-net/strata/loop.py
-
curl -L -o loop.py https://huggingface.co/emylton/strata-net/resolve/main/strata/loop.py
10.2 kB
| """ | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| # Adaptation config | |
| # --------------------------------------------------------------------------- | |
| LOOP_CONFIG = { | |
| "step": 0.005, # base nudge per update | |
| "min_step": 0.001, # minimum step after cooling | |
| "cooling": 0.995, # step multiplies by this each update | |
| "window": 50, # rolling window for outcome averaging | |
| "update_every": 10, # adapt weights every N outcomes | |
| "max_adapt": 0.30, # max allowed deviation from V2 seed per weight | |
| "min_weight": 0.01, # absolute minimum for any weight | |
| } | |
| # Which weights are adaptable and in which direction signal improvement | |
| # "+" = higher value → better performance, "-" = lower value → better | |
| ADAPTABLE_WEIGHTS = { | |
| "w_trend_bias": "+", | |
| "w_vol_uncertainty": "+", | |
| "w_break_momentum": "+", | |
| "w_fake_break_bias": "+", | |
| "w_trend_str_bias": "+", | |
| "alpha": None, # sum alpha+beta+gamma kept ~1.0, handled specially | |
| "beta": None, | |
| "gamma": None, | |
| "c_bias": "+", | |
| "c_momentum": "+", | |
| "c_trap": "-", # higher trap penalty = more conservative | |
| "decay_bias": None, # decay not adapted (structural, not performance-driven) | |
| "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] = [] | |
| # ----------------------------------------------------------------------- | |
| # Public API | |
| # ----------------------------------------------------------------------- | |
| 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:], | |
| } | |
| # ----------------------------------------------------------------------- | |
| # Internal adaptation logic | |
| # ----------------------------------------------------------------------- | |
| 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 breakdown of recent outcomes | |
| regime_pnl = {} | |
| for o in recent: | |
| r = o.get("regime", "TRANSITIONING") | |
| regime_pnl.setdefault(r, []).append(o.get("pnl_pct", 0.0)) | |
| # Adapt each eligible weight | |
| 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) | |
| # Keep alpha+beta+gamma summing to ~1.0 (renormalise) | |
| self._normalise_conflict_weights() | |
| # Cooling — step size shrinks over time | |
| 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"] | |
| # Compute per-weight relevance: how strongly did this weight | |
| # correlate with winning trades in the recent window? | |
| win_signal = self._weight_win_correlation(key, recent) | |
| # Direction: "+" means we want more of this weight when winning | |
| if direction == "+": | |
| nudge = +self._step if win_signal > 0 else -self._step | |
| else: # "-" | |
| nudge = -self._step if win_signal > 0 else +self._step | |
| # If overall performance is degrading, reverse the nudge | |
| if not improving: | |
| nudge = -nudge | |
| new_val = current + nudge | |
| # Bound: stay within max_adapt of seed, never go below min_weight | |
| 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. | |
| """ | |
| # Map weight key to the state/sense field it most influences | |
| 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) | |
| 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']}" | |
| ) | |