diff --git a/core/api/macro_bp.py b/core/api/macro_bp.py new file mode 100644 index 0000000..68ae77e --- /dev/null +++ b/core/api/macro_bp.py @@ -0,0 +1,103 @@ +""" +741 Pure Macro Matrix — Internal Regime Engine +=============================================== +_compute_regime() is the primary interface — call it directly from server-side code. + +The single HTTP route (/api/macro/) is secret-gated via X-Macro-Secret header. +It exists ONLY for the Windows Robinhood executor (which can't import Python modules). + +Public 741 regime data is a paid product — use GET /api/741macro (x402, 0.04 RLUSD). + +Data source: Tradier daily OHLCV — DEVELOPER_MANIFESTO §3 compliant. +Cache: 1 hour per symbol (daily regime is intraday-stable). +""" +from __future__ import annotations + +import os +import logging +import time +from datetime import datetime, timezone +from typing import Any, Dict, List + +from flask import Blueprint, jsonify, request + +logger = logging.getLogger("macro-matrix") + +macro_bp = Blueprint("macro", __name__) + +# Stack configuration — loaded from env so the methodology stays out of source. +# Set MACRO_STACK_CSV in Render environment variables. +_raw = os.environ.get("MACRO_STACK_CSV", "") +_STACK: List[int] = [int(x) for x in _raw.split(",") if x.strip().isdigit()] if _raw else [] +_WARMUP = int(os.environ.get("MACRO_STACK_WARMUP", "50")) +_REQUIRED_BARS = (max(_STACK) + _WARMUP) if _STACK else 0 +_CACHE_TTL = 3600 +_cache: Dict[str, Dict[str, Any]] = {} + + +def _compute_regime(symbol: str) -> Dict[str, Any]: + """ + Compute 741 Pure Macro regime for a symbol. + Importable directly by server-side code — no HTTP call needed. + Returns regime + opaque layer values (L1–L5, short to long). + """ + if not _STACK: + logger.warning("[741-MACRO] MACRO_STACK_CSV not configured") + return {"symbol": symbol, "regime": "UNKNOWN", "status": "NOT_CONFIGURED"} + + cached = _cache.get(symbol) + if cached and time.time() - cached["ts"] < _CACHE_TTL: + return cached["data"] + + try: + from tradier_api import get_history_df + df = get_history_df(symbol, days=_REQUIRED_BARS + 60) + except Exception as e: + logger.warning(f"[741-MACRO] {symbol} data fetch error: {e}") + return {"symbol": symbol, "regime": "UNKNOWN", "status": "DATA_ERROR"} + + if df is None or len(df) < _REQUIRED_BARS: + return { + "symbol": symbol, + "regime": "INSUFFICIENT_DATA", + "status": "INSUFFICIENT_DATA", + "bars": int(len(df)) if df is not None else 0, + } + + close = df["Close"].astype(float) + ema_values = [float(close.ewm(span=p, adjust=False).mean().iloc[-1]) for p in _STACK] + + price = float(close.iloc[-1]) + spread = round((ema_values[0] - ema_values[-1]) / ema_values[-1] * 100, 2) if ema_values[-1] else 0.0 + + if all(ema_values[i] > ema_values[i + 1] for i in range(len(ema_values) - 1)): + regime = "PERFECT_BULLISH_REGIME" + elif all(ema_values[i] < ema_values[i + 1] for i in range(len(ema_values) - 1)): + regime = "PERFECT_BEARISH_REGIME" + else: + regime = "CONSOLIDATION_CHOP" + + result = { + "symbol": symbol, + "status": "ok", + "regime": regime, + "price": round(price, 2), + "matrix_spread_pct": spread, + "layers": {f"L{i + 1}": round(v, 2) for i, v in enumerate(ema_values)}, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + _cache[symbol] = {"ts": time.time(), "data": result} + logger.info(f"[741-MACRO] {symbol} → {regime} (spread={spread:+.1f}%)") + return result + + +_SECRET = os.environ.get("MACRO_GATE_SECRET", "") + + +@macro_bp.route("/macro/", methods=["GET"]) +def macro_single(symbol: str): + """Internal executor gate — requires X-Macro-Secret header. Not a public product.""" + if not _SECRET or request.headers.get("X-Macro-Secret") != _SECRET: + return jsonify({"error": "unauthorized"}), 403 + result = _compute_regime(symbol.upper().strip()) + return jsonify({"status": "success", **result}) diff --git a/core/app.py b/core/app.py index 3d63694..ab0bc49 100644 --- a/core/app.py +++ b/core/app.py @@ -48,6 +48,7 @@ from core.api.vapl_bp import vapl_bp from core.vapl.middleware import install_vapl_middleware from core.api.macro741_bp import macro741_bp +from core.api.macro_bp import macro_bp import core.signal_history as signal_history from core.legacy import start_whale_stalker, init_services, get_service, clean_data from core.market_graph import get_graph @@ -153,6 +154,7 @@ def create_app(): app.register_blueprint(iam_bp, url_prefix='/api/iam') app.register_blueprint(vapl_bp) app.register_blueprint(macro741_bp, url_prefix='/api') + app.register_blueprint(macro_bp, url_prefix='/api') # Stellar Forge growth engine — feature-flagged, dormant unless enabled. # Registers the affiliate/loyalty/payout surface only when explicitly turned diff --git a/iam_executor.py b/iam_executor.py index 8ad4ee9..710c75b 100644 --- a/iam_executor.py +++ b/iam_executor.py @@ -39,6 +39,20 @@ logger = logging.getLogger("IAM-EXEC") +def _get_macro_regime(symbol: str) -> str: + """ + Returns the 741 Pure Macro regime for a symbol. + Direct Python import — no HTTP call, no public exposure of the paid product. + Fails open: UNKNOWN / INSUFFICIENT_DATA never block a trade. + Only PERFECT_BEARISH_REGIME blocks BUY orders. + """ + try: + from core.api.macro_bp import _compute_regime + return _compute_regime(symbol).get("regime", "UNKNOWN") + except Exception as e: + logger.warning(f"[IAM-EXEC] macro regime check failed for {symbol}: {e} — failing open") + return "UNKNOWN" + # ── Config ───────────────────────────────────────────────────────────────────── def _env_bool(key: str, default: bool) -> bool: return os.environ.get(key, str(default)).strip().lower() in ("true", "1", "yes") @@ -198,7 +212,9 @@ def _execute_tradier(sym: str, action: str, resolution: dict, price: float) -> d if _is_extended_hours(): logger.info(f"[IAM-EXEC] Extended hours: routing {sym} to equity (options unavailable)") return _execute_tradier_equity(sym, action, price) - if instrument == "options" or (instrument == "auto" and sym in ("IWM", "SPY", "QQQ")): + # auto mode: try options on any symbol — chain availability is the natural gate. + # BUY signal → calls; SELL signal → puts. Falls back gracefully if no chain exists. + if instrument in ("options", "auto"): return _execute_tradier_options(sym, action, resolution, price) else: return _execute_tradier_equity(sym, action, price) @@ -379,6 +395,16 @@ def execute_from_resolution(sym: str, resolution: dict, logger.info(f"[IAM-EXEC] {sym} blocked: {block_reason}") return + # 741 Pure Macro Matrix gate — only blocks BUY; SELL/exits always proceed + if action == "BUY": + macro = _get_macro_regime(sym) + if macro == "PERFECT_BEARISH_REGIME": + logger.warning( + f"[IAM-EXEC] {sym} BUY blocked — 741 macro regime is PERFECT_BEARISH_REGIME" + ) + return + logger.info(f"[IAM-EXEC] {sym} macro regime={macro} — BUY allowed") + mode = EXECUTION_MODE() logger.info( f"[IAM-EXEC] 🎯 {sym} {action} | window={time_window} | " diff --git a/iam_scanner.py b/iam_scanner.py index bd99468..92f43cb 100644 --- a/iam_scanner.py +++ b/iam_scanner.py @@ -25,15 +25,18 @@ _SCAN_TOP_N = int(os.environ.get("IAM_SCAN_TOP_N", "50")) _SCAN_INTERVAL = int(float(os.environ.get("IAM_SCAN_INTERVAL", "300"))) _INTER_DELAY = float(os.environ.get("IAM_SCAN_INTER_DELAY", "2.0")) -_URGENT_WINDOWS = {"IMMEDIATE", "NEAR_TERM"} -_INITIAL_DELAY = 120 # wait for market scanner to warm up before first pass +_URGENT_WINDOWS = {"IMMEDIATE", "NEAR_TERM"} +_INITIAL_DELAY = 120 # wait for market scanner to warm up before first pass +_MANDATORY_ANCHORS = {"AMC", "GME", "IWM"} # always resolved every pass regardless of universe def _get_symbols() -> list: """ - Dynamically pull symbol list from live state — no hardcoded tickers. + Dynamically pull symbol list from live state — 100% FETCH, no hardcoded watchlist. Primary: state.scan_results (market scanner top candidates, ranked by squeeze score). Fallback: state.quotes universe (all live-quoted symbols). + Mandatory anchors (AMC, GME, IWM) are always prepended so they're never dropped + by the TOP_N cap and never miss a pass regardless of scan-result ranking. """ from core.state import state @@ -41,12 +44,15 @@ def _get_symbols() -> list: candidates = list(state.scan_results) if candidates: - return [r.get("symbol") for r in candidates[:_SCAN_TOP_N] if r.get("symbol")] - - with state.lock: - fallback = list(state.quotes.keys()) - - return fallback[:_SCAN_TOP_N] + syms = [r.get("symbol") for r in candidates[:_SCAN_TOP_N] if r.get("symbol")] + else: + with state.lock: + syms = list(state.quotes.keys())[:_SCAN_TOP_N] + + # Prepend mandatory anchors (deduplicated) so they're always first in the pass + seen = set(syms) + anchors = [s for s in sorted(_MANDATORY_ANCHORS) if s not in seen] + return anchors + syms def _scan_pass(): diff --git a/tools/robinhood_executor_sml.py b/tools/robinhood_executor_sml.py index 3f4ac81..cf39f54 100644 --- a/tools/robinhood_executor_sml.py +++ b/tools/robinhood_executor_sml.py @@ -53,6 +53,39 @@ # ── Configuration ────────────────────────────────────────────────────────────── SQUEEZEOS_API_URL = os.environ.get("SQUEEZEOS_API_URL", "https://squeezeos-api.onrender.com") + +_macro_cache: dict = {} +_MACRO_CACHE_TTL = 3600 # matches server-side 1-hour TTL +_MACRO_GATE_SECRET = os.environ.get("MACRO_GATE_SECRET", "") + +# Always-watched anchors — injected into every oracle poll regardless of live universe +_MANDATORY_ANCHORS = {"AMC", "GME", "IWM"} + +def _get_macro_regime(symbol: str) -> str: + """ + Query internal 741 macro gate on SqueezeOS server. + Requires MACRO_GATE_SECRET in executor.env — endpoint is not public. + Fails open: no secret configured or fetch error → UNKNOWN (never blocks trades). + Only PERFECT_BEARISH_REGIME blocks BUY orders. + """ + if not _MACRO_GATE_SECRET: + return "UNKNOWN" # no secret → fail open, never block trades + now = time.time() + cached = _macro_cache.get(symbol) + if cached and now - cached["ts"] < _MACRO_CACHE_TTL: + return cached["regime"] + try: + req = URLRequest(f"{SQUEEZEOS_API_URL}/api/macro/{symbol}", + headers={"User-Agent": "SqueezeOS-RH-Executor/2.0", + "X-Macro-Secret": _MACRO_GATE_SECRET}) + with urlopen(req, timeout=10) as resp: + data = json.loads(resp.read()) + regime = data.get("regime", "UNKNOWN") + except Exception as e: + logger.warning(f"[MACRO] {symbol} regime fetch failed: {e} — failing open") + regime = "UNKNOWN" + _macro_cache[symbol] = {"regime": regime, "ts": now} + return regime ROBINHOOD_USER = os.environ.get("ROBINHOOD_USERNAME", "") ROBINHOOD_PASS = os.environ.get("ROBINHOOD_PASSWORD", "") POLL_INTERVAL_S = int(os.environ.get("POLL_INTERVAL_S", "180")) # poll every 3 minutes @@ -288,6 +321,14 @@ def _execute(symbol: str, side: str, sml: dict, scan_counter: list): logger.info(f"[EXEC] {symbol} god_stacked={god_count} < {MIN_GOD_STACKED} — skip") return + # ── 741 Pure Macro Matrix gate (BUY only) ──────────────────────────────── + if side == "buy": + macro = _get_macro_regime(symbol) + if macro == "PERFECT_BEARISH_REGIME": + logger.warning(f"[EXEC] {symbol} BUY blocked — 741 macro regime is PERFECT_BEARISH_REGIME") + return + logger.info(f"[EXEC] {symbol} macro regime={macro} — BUY allowed") + # ── Proprietary 5-EMA Stack Guardrail ─────────────────────────────────── try: url = f"{SQUEEZEOS_API_URL}/api/ema/{symbol}" @@ -632,6 +673,20 @@ def _poll_oracle() -> int: except Exception as e: logger.debug(f"[ORACLE] history fetch failed: {e}") + # ── 3. Mandatory anchors — always fetch AMC, GME, IWM even if absent from batch ── + for anchor in _MANDATORY_ANCHORS: + if anchor not in symbols_seen: + try: + req = URLRequest(f"{SQUEEZEOS_API_URL}/api/oracle/{anchor}", + headers={"User-Agent": "SqueezeOS-RH-Executor/2.0"}) + with urlopen(req, timeout=10) as resp: + oracle_resp = json.loads(resp.read()) + info = oracle_resp.get("oracle") or {} + if info.get("directive"): + symbols_seen[anchor] = info + except Exception as e: + logger.debug(f"[ORACLE] mandatory anchor {anchor} fetch failed: {e}") + if not symbols_seen: return 0 @@ -683,7 +738,7 @@ def _poll_oracle() -> int: def main(): global _rh_logged_in # explicitly declare global so Python never creates a local shadow logger.info("=" * 60) - logger.info("SqueezeOS Robinhood Executor v3.2 — Wider Signal Net (GRID_LOCK + 60% oracle)") + logger.info("SqueezeOS Robinhood Executor v3.4 — Dynamic Universe + Mandatory Anchors (AMC/GME/IWM)") logger.info(f" API : {SQUEEZEOS_API_URL}") logger.info(f" Poll every : {POLL_INTERVAL_S}s") logger.info(f" Hours : 4:00 AM–8:00 PM ET (pre-market + regular + after-hours)")