From e64ee769a02be5ad663c3cf07599aa7d35c0516d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 23:49:31 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20741=20Pure=20Macro=20Matrix=20?= =?UTF-8?q?=E2=80=94=20regime=20gate=20on=20all=20BUY=20orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New blueprint: core/api/macro_bp.py • Computes EMAs [30,60,90,120,741] on 791 daily bars from Tradier • Classifies PERFECT_BULLISH_REGIME / PERFECT_BEARISH_REGIME / CONSOLIDATION_CHOP • 1-hour per-symbol cache (daily EMAs are intraday-stable) • GET /api/macro and GET /api/macro/ - Registered in core/app.py at /api prefix - iam_executor.py: _get_macro_regime() + gate before Tradier/Discord execution • PERFECT_BEARISH_REGIME blocks BUY; SELL/exits always proceed • UNKNOWN / INSUFFICIENT_DATA fail open (never block on data unavailability) - tools/robinhood_executor_sml.py v3.3 • _get_macro_regime() + 1-hour local cache • Macro gate in _execute() after god_stacked check, BUY side only • Version bump v3.2 → v3.3 Data source: Tradier daily OHLCV — DEVELOPER_MANIFESTO §3 compliant (no yfinance) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019CTP9AwncFUZQvZxpXb6Np --- core/api/macro_bp.py | 110 ++++++++++++++++++++++++++++++++ core/app.py | 2 + iam_executor.py | 39 +++++++++++ tools/robinhood_executor_sml.py | 35 +++++++++- 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 core/api/macro_bp.py diff --git a/core/api/macro_bp.py b/core/api/macro_bp.py new file mode 100644 index 0000000..607712f --- /dev/null +++ b/core/api/macro_bp.py @@ -0,0 +1,110 @@ +""" +741 Pure Macro Matrix — Regime Scanner +======================================= +Computes EMAs [30, 60, 90, 120, 741] on daily bars to classify macro regime. + + PERFECT_BULLISH_REGIME — full stack stacked bullish; institutional highway + PERFECT_BEARISH_REGIME — full stack stacked bearish; avoid longs + CONSOLIDATION_CHOP — mixed/neutral; watch for coil + breakout + +Data source: Tradier daily OHLCV — DEVELOPER_MANIFESTO §3 compliant. +Cache: 1 hour per symbol (daily EMAs are intraday-stable). + +Endpoints: + GET /api/macro → batch scan of live universe + GET /api/macro/ → single-symbol regime check +""" +from __future__ import annotations + +import logging +import time +from datetime import datetime, timezone +from typing import Any, Dict + +from flask import Blueprint, jsonify + +logger = logging.getLogger("macro-matrix") + +macro_bp = Blueprint("macro", __name__) + +MACRO_PERIODS = [30, 60, 90, 120, 741] +_REQUIRED_BARS = 791 # 741 + 50 warmup buffer +_CACHE_TTL = 3600 # 1 hour — daily EMAs don't shift intraday +_cache: Dict[str, Dict[str, Any]] = {} + + +def _compute_regime(symbol: str) -> Dict[str, Any]: + """Fetch daily bars from Tradier and compute 741 Pure Macro regime.""" + 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) + emas = {p: float(close.ewm(span=p, adjust=False).mean().iloc[-1]) + for p in MACRO_PERIODS} + + e30, e60, e90, e120, e741 = (emas[p] for p in MACRO_PERIODS) + price = float(close.iloc[-1]) + spread = round((e30 - e741) / e741 * 100, 2) if e741 else 0.0 + + if e30 > e60 > e90 > e120 > e741: + regime = "PERFECT_BULLISH_REGIME" + elif e30 < e60 < e90 < e120 < e741: + 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"EMA_{p}": round(emas[p], 2) for p in MACRO_PERIODS}, + "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 + + +@macro_bp.route("/macro", methods=["GET"]) +@macro_bp.route("/macro/", methods=["GET"]) +def macro_batch(): + from core.state import state + from core.oracle_engine import ORACLE_SYMBOLS + symbols = list(state.quotes.keys()) if state.quotes else ORACLE_SYMBOLS + regimes = {sym: _compute_regime(sym) for sym in symbols} + summary = { + "PERFECT_BULLISH_REGIME": [s for s, r in regimes.items() if r["regime"] == "PERFECT_BULLISH_REGIME"], + "PERFECT_BEARISH_REGIME": [s for s, r in regimes.items() if r["regime"] == "PERFECT_BEARISH_REGIME"], + "CONSOLIDATION_CHOP": [s for s, r in regimes.items() if r["regime"] == "CONSOLIDATION_CHOP"], + } + return jsonify({ + "status": "success", + "universe_size": len(symbols), + "summary": summary, + "regimes": regimes, + "timestamp": datetime.now(timezone.utc).isoformat(), + }) + + +@macro_bp.route("/macro/", methods=["GET"]) +def macro_single(symbol: str): + 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..adb847f 100644 --- a/iam_executor.py +++ b/iam_executor.py @@ -39,6 +39,35 @@ logger = logging.getLogger("IAM-EXEC") +_macro_cache_iam: dict = {} +_MACRO_CACHE_TTL_IAM = 3600 + +def _get_macro_regime(symbol: str) -> str: + """ + Query 741 Pure Macro Matrix for the daily regime. + Fails open — UNKNOWN / INSUFFICIENT_DATA never block a trade. + Only PERFECT_BEARISH_REGIME blocks BUY orders. + """ + import time as _time + import urllib.request as _urlreq + import json as _json + now = _time.time() + cached = _macro_cache_iam.get(symbol) + if cached and now - cached["ts"] < _MACRO_CACHE_TTL_IAM: + return cached["regime"] + base = os.getenv("SQUEEZEOS_BASE_URL", "https://squeezeos-api.onrender.com").rstrip("/") + try: + req = _urlreq.Request(f"{base}/api/macro/{symbol}", + headers={"User-Agent": "SqueezeOS-IAM-Executor/1.0"}) + with _urlreq.urlopen(req, timeout=10) as resp: + data = _json.loads(resp.read()) + regime = data.get("regime", "UNKNOWN") + except Exception as e: + logger.warning(f"[IAM-EXEC] macro regime fetch failed for {symbol}: {e} — failing open") + regime = "UNKNOWN" + _macro_cache_iam[symbol] = {"regime": regime, "ts": now} + return regime + # ── Config ───────────────────────────────────────────────────────────────────── def _env_bool(key: str, default: bool) -> bool: return os.environ.get(key, str(default)).strip().lower() in ("true", "1", "yes") @@ -379,6 +408,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/tools/robinhood_executor_sml.py b/tools/robinhood_executor_sml.py index 3f4ac81..60549d3 100644 --- a/tools/robinhood_executor_sml.py +++ b/tools/robinhood_executor_sml.py @@ -53,6 +53,31 @@ # ── 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 + +def _get_macro_regime(symbol: str) -> str: + """ + Query 741 Pure Macro Matrix on SqueezeOS server. + Returns PERFECT_BULLISH_REGIME | PERFECT_BEARISH_REGIME | CONSOLIDATION_CHOP | UNKNOWN. + Fails open — UNKNOWN / INSUFFICIENT_DATA never block a trade. + """ + 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"}) + 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 +313,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}" @@ -683,7 +716,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.3 — 741 Macro Regime Gate (PERFECT_BEARISH blocks BUY)") 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)") From 7952e19590950160c2bd0b772c38414f7350b883 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 23:58:25 +0000 Subject: [PATCH 2/4] feat: dynamic options routing + mandatory AMC/GME/IWM anchors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iam_scanner.py: - _MANDATORY_ANCHORS = {AMC, GME, IWM} always prepended to every scan pass - 100% FETCH preserved — anchors are deduplicated, never duplicated from live universe - Anchors come first so they're never dropped by the TOP_N cap iam_executor.py: - Remove hardcoded ("IWM","SPY","QQQ") filter from auto options routing - auto mode now tries options (calls/puts) on ANY symbol from the live universe - Options chain availability is the natural gate — no chain → logged error, no crash - BUY signal → calls, SELL signal → puts, extended hours → equity fallback unchanged tools/robinhood_executor_sml.py v3.4: - _MANDATORY_ANCHORS = {AMC, GME, IWM} — always injected into oracle poll - After batch + history scan, fetch /api/oracle/{sym} for each missing anchor - Wide open dynamic universe + guaranteed AMC/GME/IWM coverage every cycle Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019CTP9AwncFUZQvZxpXb6Np --- iam_executor.py | 4 +++- iam_scanner.py | 24 +++++++++++++++--------- tools/robinhood_executor_sml.py | 19 ++++++++++++++++++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/iam_executor.py b/iam_executor.py index adb847f..37ff768 100644 --- a/iam_executor.py +++ b/iam_executor.py @@ -227,7 +227,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) 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 60549d3..f1d3502 100644 --- a/tools/robinhood_executor_sml.py +++ b/tools/robinhood_executor_sml.py @@ -57,6 +57,9 @@ _macro_cache: dict = {} _MACRO_CACHE_TTL = 3600 # matches server-side 1-hour TTL +# 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 741 Pure Macro Matrix on SqueezeOS server. @@ -665,6 +668,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 @@ -716,7 +733,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.3 — 741 Macro Regime Gate (PERFECT_BEARISH blocks BUY)") + 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)") From 7a720b897ecb7b9c8b1fe0bad13e8646c7570ddb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 00:09:08 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20lock=20down=20/api/macro=20=E2=80=94?= =?UTF-8?q?=20not=20a=20free=20product?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/741macro is the paid pitch-deck product (x402, 0.04 RLUSD). /api/macro was accidentally public — now secret-gated. core/api/macro_bp.py: - Kill public batch route (/api/macro) entirely - /api/macro/ now requires X-Macro-Secret header → 403 without it - MACRO_GATE_SECRET env var controls the secret iam_executor.py: - Drop HTTP call entirely — import _compute_regime() directly (same process) - Zero network exposure, uses server-side 1-hour cache tools/robinhood_executor_sml.py: - _get_macro_regime() now sends X-Macro-Secret header from MACRO_GATE_SECRET env var - No secret configured → fails open (UNKNOWN), never blocks trades Add MACRO_GATE_SECRET to Render env + executor.env to activate the gate. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019CTP9AwncFUZQvZxpXb6Np --- core/api/macro_bp.py | 49 ++++++++++++--------------------- iam_executor.py | 29 +++++-------------- tools/robinhood_executor_sml.py | 13 ++++++--- 3 files changed, 34 insertions(+), 57 deletions(-) diff --git a/core/api/macro_bp.py b/core/api/macro_bp.py index 607712f..47d8433 100644 --- a/core/api/macro_bp.py +++ b/core/api/macro_bp.py @@ -1,27 +1,25 @@ """ -741 Pure Macro Matrix — Regime Scanner -======================================= -Computes EMAs [30, 60, 90, 120, 741] on daily bars to classify macro regime. +741 Pure Macro Matrix — Internal Regime Engine +=============================================== +_compute_regime() is the primary interface — call it directly from server-side code. - PERFECT_BULLISH_REGIME — full stack stacked bullish; institutional highway - PERFECT_BEARISH_REGIME — full stack stacked bearish; avoid longs - CONSOLIDATION_CHOP — mixed/neutral; watch for coil + breakout +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 EMAs are intraday-stable). - -Endpoints: - GET /api/macro → batch scan of live universe - GET /api/macro/ → single-symbol regime check """ from __future__ import annotations +import os import logging import time from datetime import datetime, timezone from typing import Any, Dict -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, request logger = logging.getLogger("macro-matrix") @@ -34,7 +32,11 @@ def _compute_regime(symbol: str) -> Dict[str, Any]: - """Fetch daily bars from Tradier and compute 741 Pure Macro regime.""" + """ + Compute 741 Pure Macro regime for a symbol. + Importable directly by server-side code — no HTTP call needed. + Returns regime + all 5 EMA layers. + """ cached = _cache.get(symbol) if cached and time.time() - cached["ts"] < _CACHE_TTL: return cached["data"] @@ -83,28 +85,13 @@ def _compute_regime(symbol: str) -> Dict[str, Any]: return result -@macro_bp.route("/macro", methods=["GET"]) -@macro_bp.route("/macro/", methods=["GET"]) -def macro_batch(): - from core.state import state - from core.oracle_engine import ORACLE_SYMBOLS - symbols = list(state.quotes.keys()) if state.quotes else ORACLE_SYMBOLS - regimes = {sym: _compute_regime(sym) for sym in symbols} - summary = { - "PERFECT_BULLISH_REGIME": [s for s, r in regimes.items() if r["regime"] == "PERFECT_BULLISH_REGIME"], - "PERFECT_BEARISH_REGIME": [s for s, r in regimes.items() if r["regime"] == "PERFECT_BEARISH_REGIME"], - "CONSOLIDATION_CHOP": [s for s, r in regimes.items() if r["regime"] == "CONSOLIDATION_CHOP"], - } - return jsonify({ - "status": "success", - "universe_size": len(symbols), - "summary": summary, - "regimes": regimes, - "timestamp": datetime.now(timezone.utc).isoformat(), - }) +_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/iam_executor.py b/iam_executor.py index 37ff768..710c75b 100644 --- a/iam_executor.py +++ b/iam_executor.py @@ -39,34 +39,19 @@ logger = logging.getLogger("IAM-EXEC") -_macro_cache_iam: dict = {} -_MACRO_CACHE_TTL_IAM = 3600 - def _get_macro_regime(symbol: str) -> str: """ - Query 741 Pure Macro Matrix for the daily regime. - Fails open — UNKNOWN / INSUFFICIENT_DATA never block a trade. + 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. """ - import time as _time - import urllib.request as _urlreq - import json as _json - now = _time.time() - cached = _macro_cache_iam.get(symbol) - if cached and now - cached["ts"] < _MACRO_CACHE_TTL_IAM: - return cached["regime"] - base = os.getenv("SQUEEZEOS_BASE_URL", "https://squeezeos-api.onrender.com").rstrip("/") try: - req = _urlreq.Request(f"{base}/api/macro/{symbol}", - headers={"User-Agent": "SqueezeOS-IAM-Executor/1.0"}) - with _urlreq.urlopen(req, timeout=10) as resp: - data = _json.loads(resp.read()) - regime = data.get("regime", "UNKNOWN") + 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 fetch failed for {symbol}: {e} — failing open") - regime = "UNKNOWN" - _macro_cache_iam[symbol] = {"regime": regime, "ts": now} - return regime + 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: diff --git a/tools/robinhood_executor_sml.py b/tools/robinhood_executor_sml.py index f1d3502..cf39f54 100644 --- a/tools/robinhood_executor_sml.py +++ b/tools/robinhood_executor_sml.py @@ -56,23 +56,28 @@ _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 741 Pure Macro Matrix on SqueezeOS server. - Returns PERFECT_BULLISH_REGIME | PERFECT_BEARISH_REGIME | CONSOLIDATION_CHOP | UNKNOWN. - Fails open — UNKNOWN / INSUFFICIENT_DATA never block a trade. + 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"}) + 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") From d3d943c5d332133c565175d66028e073a435821c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 00:18:42 +0000 Subject: [PATCH 4/4] fix: scrub methodology from macro_bp source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stack config moved to MACRO_STACK_CSV env var (Render secret) — no periods in code - Response layers renamed L1-L5 (short→long) — opaque, no period values exposed - Internal variable names cleaned up — no period references visible in source - _REQUIRED_BARS computed from env config, not hardcoded - Fails gracefully with NOT_CONFIGURED if MACRO_STACK_CSV not set /api/741macro remains the paid public product. /api/macro/ is secret-gated internal only. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019CTP9AwncFUZQvZxpXb6Np --- core/api/macro_bp.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/core/api/macro_bp.py b/core/api/macro_bp.py index 47d8433..68ae77e 100644 --- a/core/api/macro_bp.py +++ b/core/api/macro_bp.py @@ -9,7 +9,7 @@ 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 EMAs are intraday-stable). +Cache: 1 hour per symbol (daily regime is intraday-stable). """ from __future__ import annotations @@ -17,7 +17,7 @@ import logging import time from datetime import datetime, timezone -from typing import Any, Dict +from typing import Any, Dict, List from flask import Blueprint, jsonify, request @@ -25,9 +25,13 @@ macro_bp = Blueprint("macro", __name__) -MACRO_PERIODS = [30, 60, 90, 120, 741] -_REQUIRED_BARS = 791 # 741 + 50 warmup buffer -_CACHE_TTL = 3600 # 1 hour — daily EMAs don't shift intraday +# 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]] = {} @@ -35,8 +39,12 @@ 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 + all 5 EMA layers. + 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"] @@ -57,16 +65,14 @@ def _compute_regime(symbol: str) -> Dict[str, Any]: } close = df["Close"].astype(float) - emas = {p: float(close.ewm(span=p, adjust=False).mean().iloc[-1]) - for p in MACRO_PERIODS} + ema_values = [float(close.ewm(span=p, adjust=False).mean().iloc[-1]) for p in _STACK] - e30, e60, e90, e120, e741 = (emas[p] for p in MACRO_PERIODS) price = float(close.iloc[-1]) - spread = round((e30 - e741) / e741 * 100, 2) if e741 else 0.0 + spread = round((ema_values[0] - ema_values[-1]) / ema_values[-1] * 100, 2) if ema_values[-1] else 0.0 - if e30 > e60 > e90 > e120 > e741: + if all(ema_values[i] > ema_values[i + 1] for i in range(len(ema_values) - 1)): regime = "PERFECT_BULLISH_REGIME" - elif e30 < e60 < e90 < e120 < e741: + 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" @@ -77,7 +83,7 @@ def _compute_regime(symbol: str) -> Dict[str, Any]: "regime": regime, "price": round(price, 2), "matrix_spread_pct": spread, - "layers": {f"EMA_{p}": round(emas[p], 2) for p in MACRO_PERIODS}, + "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}