Spaces:
Running
Running
Deploy Aureon OS (deploy/selfhost stack: nginx edge + fusion API x2 + console + HNC organ + redis + postgres; paper mode) (part 4)
b41bee3 verified Download aureon/observer/wave_predictor.py from GaryLeckey/aureon-os: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/GaryLeckey/aureon-os/resolve/main/aureon/observer/wave_predictor.py
- Command line
-
hf download hf://spaces/GaryLeckey/aureon-os/aureon/observer/wave_predictor.py
-
curl -L -o wave_predictor.py https://huggingface.co/spaces/GaryLeckey/aureon-os/resolve/main/aureon/observer/wave_predictor.py
16.2 kB
| """``WavePredictor`` β a wave-based next-tick directional model over the | |
| live HNC field. | |
| Where the HarmonicObserver *describes* the field (rocks, regime, | |
| coherence_score), this predictor *forecasts* it: given the recent Ξ(t) | |
| trace, produce a near-term directional call (BULLISH / BEARISH / | |
| NEUTRAL) with a confidence and a predicted next Ξ. That signal plugs | |
| into PredictionBus alongside ``harmonic_observer`` so it contributes | |
| to ``get_consensus`` directly. | |
| The model is intentionally simple β pure Python, no numpy required: | |
| 1. Linear trend β OLS slope over the last N samples β primary | |
| directional driver, range-normalised. | |
| 2. Recent momentum β single-tick Ξ Ξ β short-horizon kick. | |
| 3. Wave continuation β if the observer reports a band rock at a | |
| specific dominant_hz, project the cycle | |
| forward by half a period from the current | |
| phase position to bias the call. | |
| 4. Coherence gating β final confidence is multiplied by the | |
| observer's coherence_score (when | |
| available); a chaotic field never produces | |
| a high-confidence wave call. | |
| A numpy fast-path replaces (1) with ``np.polyfit`` and (3) with a | |
| Hilbert-transform-based phase estimate when numpy/scipy are available; | |
| output shape is identical either way. | |
| The predictor self-registers against PredictionBus.__init__ via the | |
| same auto-wiring path as ``harmonic_observer`` (see Stage B). Default | |
| weight in get_consensus() is 1.0 β a fresh predictor with no validated | |
| track record. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import math | |
| import threading | |
| import time | |
| from collections import deque | |
| from dataclasses import dataclass, field | |
| from typing import Any, Callable, Deque, Dict, List, Optional, Tuple | |
| logger = logging.getLogger(__name__) | |
| # Optional fast-path imports. | |
| try: | |
| import numpy as _np | |
| _HAS_NUMPY = True | |
| except Exception: | |
| _HAS_NUMPY = False | |
| # βββ config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DEFAULT_HISTORY_LENGTH = 120 # ~10 minutes at 5 s cadence | |
| TREND_WINDOW = 24 # last N samples for OLS | |
| MOMENTUM_WINDOW = 4 # for kick term | |
| DIRECTION_THRESHOLD = 0.15 # |trend_score| above this β directional | |
| HALF_PERIOD_PROJECTION_FRAC = 0.5 # how far into the next half-cycle to project | |
| class WaveCall: | |
| """One forecast snapshot. Mirrors the predictor.UnifiedSignal shape | |
| closely enough to be adapted with no information loss.""" | |
| direction: str # "BULLISH" | "BEARISH" | "NEUTRAL" | |
| confidence: float # [0, 1] | |
| strength: float # [-1, 1] | |
| predicted_next_lambda: Optional[float] | |
| reason: str | |
| components: Dict[str, float] = field(default_factory=dict) | |
| ts: float = field(default_factory=time.time) | |
| samples_used: int = 0 | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "direction": self.direction, | |
| "confidence": float(self.confidence), | |
| "strength": float(self.strength), | |
| "predicted_next_lambda": ( | |
| None if self.predicted_next_lambda is None | |
| else float(self.predicted_next_lambda) | |
| ), | |
| "reason": self.reason, | |
| "components": dict(self.components), | |
| "ts": self.ts, | |
| "samples_used": self.samples_used, | |
| } | |
| # βββ pure-Python OLS slope (avoids the numpy dep) ββββββββββββββββββ | |
| def _ols_slope(values: List[float]) -> float: | |
| """Slope of a least-squares line fit through ``values`` (x = index). | |
| Pure-Python β no numpy. Returns 0.0 for fewer than 2 points. | |
| """ | |
| n = len(values) | |
| if n < 2: | |
| return 0.0 | |
| x_mean = (n - 1) / 2.0 | |
| y_mean = sum(values) / n | |
| num = 0.0 | |
| den = 0.0 | |
| for i, v in enumerate(values): | |
| dx = i - x_mean | |
| num += dx * (v - y_mean) | |
| den += dx * dx | |
| return num / den if den else 0.0 | |
| # βββ the predictor βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class WavePredictor: | |
| """Wave-based directional forecaster over the live Ξ trace. | |
| Thread-safe ``ingest()``; read accessors hold the same lock so a | |
| PredictionBus call doesn't see a half-updated history deque. | |
| """ | |
| def __init__( | |
| self, | |
| history_length: int = DEFAULT_HISTORY_LENGTH, | |
| observer=None, | |
| ): | |
| self._lock = threading.RLock() | |
| self._history: Deque[Tuple[float, float]] = deque(maxlen=history_length) | |
| self._observer = observer # optional β for coherence gating + rocks | |
| self._last_call: Optional[WaveCall] = None | |
| self._n_ingested = 0 | |
| # βββ public API βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ingest(self, ts: float, lambda_t: float) -> None: | |
| """Accept one (timestamp, Ξ_full) tick. Cheap; no work done here.""" | |
| try: | |
| t = float(ts) | |
| v = float(lambda_t) | |
| except (TypeError, ValueError): | |
| return | |
| if math.isnan(v) or math.isinf(v): | |
| return | |
| with self._lock: | |
| self._history.append((t, v)) | |
| self._n_ingested += 1 | |
| def ingest_state(self, state) -> None: | |
| """Accept a LambdaState-like object (matches observer.ingest_state). | |
| Useful when the predictor sits next to the daemon's compute | |
| loop β calling code can hand the same state to both. | |
| """ | |
| if state is None: | |
| return | |
| ts = getattr(state, "timestamp", None) or time.time() | |
| lt = getattr(state, "lambda_t", None) | |
| if lt is None: | |
| return | |
| self.ingest(ts, lt) | |
| def predict(self, symbol: str = "BTCUSD") -> WaveCall: | |
| """Compute a directional forecast from current history + observer.""" | |
| with self._lock: | |
| samples = [v for _, v in self._history] | |
| n = len(samples) | |
| if n < TREND_WINDOW: | |
| self._last_call = WaveCall( | |
| direction="NEUTRAL", confidence=0.0, strength=0.0, | |
| predicted_next_lambda=None, | |
| reason=f"insufficient_history({n}/{TREND_WINDOW})", | |
| samples_used=n, | |
| ) | |
| return self._last_call | |
| # Confine to the last TREND_WINDOW samples for the slope. | |
| recent = samples[-TREND_WINDOW:] | |
| slope = _ols_slope(recent) | |
| vmin = min(recent) | |
| vmax = max(recent) | |
| rng = (vmax - vmin) or 1e-9 | |
| normalised_slope = slope / rng # in samples-1 of normalised Ξ | |
| # Trend score in [-1, 1] β saturates the slope onto a | |
| # comparable directional unit. | |
| trend_score = max(-1.0, min(1.0, normalised_slope * 10.0)) | |
| # Momentum kick over the last MOMENTUM_WINDOW samples. | |
| if n >= MOMENTUM_WINDOW + 1: | |
| tail = samples[-MOMENTUM_WINDOW:] | |
| momentum = (tail[-1] - tail[0]) / rng | |
| momentum_score = max(-1.0, min(1.0, momentum * 5.0)) | |
| else: | |
| momentum_score = 0.0 | |
| # Wave continuation β if the observer has a band rock with a | |
| # dominant frequency, project the implied cycle a half-period | |
| # ahead and use the sign of the projected ΞΞ as a tiebreaker. | |
| wave_score = 0.0 | |
| if self._observer is not None: | |
| try: | |
| snap = self._observer.metrics_snapshot() | |
| rocks = (snap.get("rocks_fast") or [])[:1] # use the strongest | |
| if rocks: | |
| hz = float(rocks[0].get("dominant_hz") or 0.0) | |
| amp = float(rocks[0].get("amplitude") or 0.0) | |
| if hz > 0 and amp > 0: | |
| # Phase advances by 2Ο * hz * dt per unit time. | |
| dt = HALF_PERIOD_PROJECTION_FRAC / hz # half period | |
| # Compare predicted-next vs current. | |
| # Without true phase recovery we use sign of | |
| # slope as proxy for which side of the cycle | |
| # we're on, then flip the bias on the other half. | |
| wave_score = math.copysign(1.0, slope) * \ | |
| min(1.0, amp / (rng + 1e-9)) | |
| except Exception as exc: | |
| logger.debug("wave continuation read failed: %s", exc) | |
| # Combine β weights chosen so trend dominates, momentum & wave | |
| # adjust on the margin. | |
| combined = ( | |
| 0.55 * trend_score | |
| + 0.25 * momentum_score | |
| + 0.20 * wave_score | |
| ) | |
| combined = max(-1.0, min(1.0, combined)) | |
| # Direction. | |
| if combined > DIRECTION_THRESHOLD: | |
| direction = "BULLISH" | |
| elif combined < -DIRECTION_THRESHOLD: | |
| direction = "BEARISH" | |
| else: | |
| direction = "NEUTRAL" | |
| # Predicted next Ξ (one step ahead) β useful for paper trading. | |
| predicted_next = recent[-1] + slope | |
| # Confidence β built from: | |
| # (a) magnitude of the directional score | |
| # (b) recent-slope stability (low variance over a 3-window | |
| # rolling slope estimate) | |
| # (c) observer's coherence_score, when present | |
| if n >= TREND_WINDOW * 2: | |
| half = TREND_WINDOW | |
| slope_a = _ols_slope(samples[-half * 2 : -half]) | |
| slope_b = _ols_slope(samples[-half:]) | |
| stability = 1.0 - min(1.0, abs(slope_a - slope_b) / (abs(slope_a) + abs(slope_b) + 1e-9)) | |
| else: | |
| stability = 0.5 | |
| coherence_gate = 1.0 | |
| if self._observer is not None: | |
| try: | |
| coherence_gate = float(self._observer.coherence_score()) | |
| # Predictor still produces useful output without observer | |
| # rocks β clamp the floor so a numpy-less sandbox doesn't | |
| # zero everything out. | |
| coherence_gate = max(0.25, min(1.0, coherence_gate)) | |
| except Exception: | |
| pass | |
| confidence = abs(combined) * (0.5 + 0.5 * stability) * coherence_gate | |
| confidence = max(0.0, min(1.0, confidence)) | |
| call = WaveCall( | |
| direction=direction, | |
| confidence=confidence, | |
| strength=combined, | |
| predicted_next_lambda=predicted_next, | |
| reason=f"trend={trend_score:+.3f},mom={momentum_score:+.3f}," | |
| f"wave={wave_score:+.3f},stab={stability:.2f}," | |
| f"cohgate={coherence_gate:.2f}", | |
| components={ | |
| "trend_score": trend_score, | |
| "momentum_score": momentum_score, | |
| "wave_score": wave_score, | |
| "slope": slope, | |
| "stability": stability, | |
| "coherence_gate": coherence_gate, | |
| }, | |
| samples_used=n, | |
| ) | |
| with self._lock: | |
| self._last_call = call | |
| return call | |
| def last_call(self) -> Optional[WaveCall]: | |
| with self._lock: | |
| return self._last_call | |
| def n_ingested(self) -> int: | |
| with self._lock: | |
| return self._n_ingested | |
| # βββ singleton + auto-wire (mirrors the observer's pattern) ββββββββ | |
| _singleton: Optional[WavePredictor] = None | |
| _singleton_lock = threading.Lock() | |
| def get_wave_predictor() -> Optional[WavePredictor]: | |
| """Return the global WavePredictor or None if none constructed.""" | |
| return _singleton | |
| def set_wave_predictor(p: WavePredictor) -> None: | |
| global _singleton | |
| with _singleton_lock: | |
| _singleton = p | |
| _orig_init = WavePredictor.__init__ | |
| def _init_with_singleton(self, *args, **kwargs): | |
| _orig_init(self, *args, **kwargs) | |
| global _singleton | |
| with _singleton_lock: | |
| if _singleton is None: | |
| _singleton = self | |
| WavePredictor.__init__ = _init_with_singleton # type: ignore[assignment] | |
| # βββ PredictionBus adapter βββββββββββββββββββββββββββββββββββββββββ | |
| PREDICTOR_NAME = "wave_predictor" | |
| PREDICTOR_WEIGHT = 1.0 # default β same as a fresh, untracked predictor | |
| def _build_unified_signal(call: WaveCall, symbol: str): | |
| from aureon.autonomous.aureon_autonomy_hub import UnifiedSignal | |
| return UnifiedSignal( | |
| source=PREDICTOR_NAME, | |
| signal_type="prediction", | |
| symbol=symbol, | |
| direction=call.direction, | |
| confidence=float(call.confidence), | |
| strength=float(call.strength), | |
| payload={ | |
| "reason": call.reason, | |
| "predicted_next_lambda": call.predicted_next_lambda, | |
| "components": call.components, | |
| "samples_used": call.samples_used, | |
| }, | |
| timestamp=call.ts, | |
| ) | |
| def _neutral(symbol: str, reason: str): | |
| from aureon.autonomous.aureon_autonomy_hub import UnifiedSignal | |
| return UnifiedSignal( | |
| source=PREDICTOR_NAME, | |
| signal_type="prediction", | |
| symbol=symbol, | |
| direction="NEUTRAL", | |
| confidence=0.0, | |
| strength=0.0, | |
| payload={"reason": reason}, | |
| timestamp=time.time(), | |
| ) | |
| def make_predictor(predictor: WavePredictor) -> Callable: | |
| """Bind a WavePredictor instance into a predict_fn for PredictionBus.""" | |
| def _pf(_data_signals=None, symbol: str = "BTCUSD"): | |
| try: | |
| return _build_unified_signal(predictor.predict(symbol), symbol) | |
| except Exception as exc: | |
| logger.debug("wave_predictor predict failed: %s", exc) | |
| return _neutral(symbol, f"predict_error:{type(exc).__name__}") | |
| _pf.__name__ = "wave_predictor_predict" | |
| return _pf | |
| def wave_predictor_predict(_data_signals=None, symbol: str = "BTCUSD"): | |
| """Module-level callable consulting the singleton β used by auto-wire.""" | |
| p = get_wave_predictor() | |
| if p is None: | |
| return _neutral(symbol, "wave_predictor_not_running") | |
| try: | |
| return _build_unified_signal(p.predict(symbol), symbol) | |
| except Exception as exc: | |
| logger.debug("wave_predictor_predict failed: %s", exc) | |
| return _neutral(symbol, f"predict_error:{type(exc).__name__}") | |
| def wire_wave_predictor(prediction_bus, predictor: Optional[WavePredictor] = None, | |
| name: str = PREDICTOR_NAME) -> bool: | |
| if prediction_bus is None: | |
| return False | |
| try: | |
| if predictor is not None: | |
| prediction_bus.register_predictor(name, make_predictor(predictor)) | |
| else: | |
| prediction_bus.register_predictor(name, wave_predictor_predict) | |
| return True | |
| except Exception as exc: | |
| logger.debug("wire_wave_predictor failed: %s", exc) | |
| return False | |
| def auto_wire_prediction_bus(prediction_bus) -> bool: | |
| """Idempotent registration. Mirrors observer.wiring's flag-attr pattern | |
| so a single PredictionBus picks up both predictors with one __init__ | |
| call.""" | |
| if prediction_bus is None: | |
| return False | |
| if getattr(prediction_bus, "_wave_predictor_wired", False): | |
| return True | |
| if wire_wave_predictor(prediction_bus, predictor=None): | |
| setattr(prediction_bus, "_wave_predictor_wired", True) | |
| logger.info("auto-wired wave_predictor onto PredictionBus") | |
| return True | |
| return False | |