Skill: diagnostikon/polymarket-central-bank-trader (version 0.0.3)

--- name: polymarket-central-bank-trader description: Trades Polymarket prediction markets on central bank decisions, interest rates, inflation prints, and Fed/ECB/Riksbank policy moves. Exploits three compounding structural edges — question tractability (near-term rate decisions are near-certain from CME FedWatch; emergency cuts are once-per-decade events retail perpetually overprices), FOMC communication cycle (maximum conviction post-meeting/dot-plot; minimum during blackout), and data calendar timing (CPI/PCE/NFP release weeks boost signal quality for inflation and rate markets). metadata: author: Diagnostikon version: "1.0" displayName: Central Bank & Monetary Policy Trader difficulty: advanced --- # Central Bank & Monetary Policy Trader > **This is a template.** > The default signal is keyword-based market discovery combined with conviction-based sizing and `central_bank_bias()` — three compounding structural edges, no external API required. > The skill handles all the plumbing (market discovery, trade execution, safeguards). Your agent provides the alpha. ## Strategy Overview Central bank prediction markets are the most data-rich category on Polymarket — and among the worst-priced relative to the available data. The Fed publishes its own rate projections (dot plot), futures markets price every remaining meeting of the year, and inflation data is measured to two decimal places by multiple independent agencies. Three structural edges compound: 1. **Question tractability** — The Fed's "no surprise" doctrine means near-term rate decisions are, by design, nearly pre-determined from public data weeks before the meeting. CME FedWatch futures have ~95% accuracy when pricing ≥85% probability for a near-term decision. Polymarket retail prices the same meeting at 70–80%. Emergency cuts happened three times in 25 years. Retail prices them at 15–30% in every crisis. 2. **FOMC communication cycle** — The Fed operates on a strict 6-week cycle with a documented blackout period (~10 days before each meeting) when no official can speak publicly. Signal quality is highest in the 5 days post-meeting (fresh guidance, quarterly dot plot) and in the Fedspeak window (11–20 days before meeting). During blackout, no new guidance can arrive — markets are at maximum staleness. This cycle is computable from the published FOMC calendar. 3. **Data calendar timing** — BLS CPI (10th–17th of each month), BEA PCE (25th+), and BLS NFP (1st–7th) all have fixed release schedules. The window immediately around each release is when inflation and rate-path markets are most actionable: the Cleveland Fed CPI Nowcast is within 0.1pp of the final print, but retail anchors to the prior month's number and never checks the nowcast. ## Signal Logic ### Default Signal: Conviction-Based Sizing with Central Bank Bias 1. Discover active monetary policy markets on Polymarket 2. Compute base conviction from distance to threshold (0% at boundary → 100% at p=0/p=1) 3. Apply `central_bank_bias()` — question tractability × FOMC cycle position × data calendar 4. Size = `max(MIN_TRADE, conviction × bias × MAX_POSITION)` — capped at MAX_POSITION 5. Skip markets with spread > MAX_SPREAD or fewer than MIN_DAYS to resolution ### Central Bank Bias (built-in, no API required) **Factor 1 — Question Type Tractability** | Question type | Multiplier | The structural reality | |---|---|---| | Next-meeting rate decision (1–2 meetings out) | **1.25x** | "No surprise" doctrine; CME FedWatch ~95% accurate when ≥85% priced; retail still prices Polymarket as uncertain — 17-point structural gap | | CPI / PCE inflation print at specific level | **1.20x** | Measured to two decimal places; Cleveland Fed CPI Nowcast within 0.1pp of final print updated daily; retail anchors to prior month's number | | Year-end rate target / annual cut count | **1.15x** | Dot plot gives Fed's own median projection; CME futures curve gives market-implied path through every remaining meeting; both public, rarely read | | Yield curve inversion / un-inversion | **1.10x** | FRED publishes 10Y-2Y and 10Y-3M daily; direction predictable from Fed rate path; retail treats yield curve as abstract not trackable | | Recession declaration / GDP contraction | **0.80x** | NBER is backward-looking (6–18 month lag); Sahm Rule (real-time, near-perfect) unknown to retail; resolution criteria and signal are measuring different things | | Chair fired / resigned / succession | **0.75x** | Fed independence norm strong (one removal-equivalent in modern history) but political risk real; resolution timing entirely in executive hands | | Emergency / inter-meeting rate action | **0.70x** | Three times in 25 years (9/11, 2008, COVID); retail prices at 15–30% in every VIX spike; lowest-edge category in monetary policy | **The "No Surprise" Doctrine** — The Fed designs its communication precisely to eliminate decision uncertainty before the meeting. When FedWatch prices a hold at 92%, that is not the market's opinion — it is the Fed's advance communication, expressed in futures. Polymarket retail prices 75% for the same event. The 17-point gap is not an opportunity; it is a structural feature of how the Fed operates, which replenishes every meeting cycle. **The Emergency Cut Trap** — Three inter-meeting rate actions in 25 years (2001, 2008, 2020). Each was precipitated by a specific systemic shock (9/11, Lehman, COVID lockdown). Retail anchors to all three during every market stress episode and prices "will there be an emergency cut?" at 15–30% when the base rate in any given quarter is near zero. Dampen to 0.70x — the Stuxnet Rule of monetary policy. **The NBER Lag Problem** — "Will there be a recession by Q3 2026?" resolves based on NBER's official declaration, which typically comes 6–18 months *after* the actual peak. The Sahm Rule (triggers when 3-month average unemployment rises 0.5pp above prior 12-month low) is a near-real-time recession indicator with a near-perfect historical track record — but NBER's formal definition lags it by over a year. This mismatch between the predictive signal and the resolution criteria makes recession timing markets structurally noisier than they appear. Trade at 0.80x. **Factor 2 — FOMC Communication Cycle** The FOMC calendar is published by the Federal Reserve Board at least one year in advance. The bias function reads it exactly — every run computes the current position in the cycle: | Phase | Days relative to decision date | Multiplier | Why | |---|---|---|---| | Post-meeting | 0–5 days after | **1.20x (+0.05 in SEP months)** | Statement public, press conference indexed, dot plot fresh (Mar/Jun/Sep/Dec) | | Fedspeak window | 11–20 days before | **1.15x** | Officials actively telegraph upcoming decision; forward guidance sharpest | | Mid-cycle | All other periods | **1.00x** | Normal inter-meeting baseline | | Blackout | 1–10 days before | **0.85x** | No Fed communication permitted; last guidance already priced; no new signal can arrive | The **SEP boost** (+0.05x) applies in March, June, September, and December — the four meetings where the Fed publishes a fresh Summary of Economic Projections including the dot plot. The median dot is the Fed's own official rate forecast for the next 3 years. In these months, post-meeting clarity is at its maximum. **Factor 3 — Data Calendar Overlay** | Condition | Overlay mult | Release schedule | |---|---|---| | Inflation question + CPI release week (10th–17th) | **1.20x** | BLS CPI at 8:30 AM ET; Cleveland Fed nowcast closes in | | Inflation question + PCE release week (25th+) | **1.15x** | BEA PCE at 8:30 AM ET; Fed's preferred measure | | Labor market question + NFP week (1st–7th) | **1.15x** | BLS NFP first Friday; labor market shapes rate path | | Rate decision question + any data release week | **1.10x** | Fresh macro data recalibrates rate path conviction | ### Combined Examples | Market | Type | Cycle | Data | Final bias | |---|---|---|---|---| | "Will Fed cut 25bp at next meeting?" — post-SEP-FOMC + CPI week | 1.25x | 1.25x (post-SEP) | 1.20x | **1.40x cap** | | "Will CPI fall below 3% this month?" — CPI week | 1.20x | 1.00x (mid-cycle) | 1.20x | **1.40x cap** | | "Will Fed cut 3 times in 2026?" — mid-cycle | 1.15x | 1.00x | 1.00x | **1.15x** | | "Will there be an emergency rate cut?" — any blackout | 0.70x | 0.85x | 1.00x | **0.60x → MIN_TRADE** | | "Will Powell be fired?" — any | 0.75x | 1.00x | 1.00x | **0.75x** | | "Will there be a recession by Q3 2026?" | 0.80x | 1.00x | 1.00x | **0.80x** | | "Will Fed cut at next meeting?" — during blackout | 1.25x | 0.85x | 1.00x | **1.06x** — staleness penalty applies | ### Keywords Monitored ``` Federal Reserve, Fed rate, FOMC, interest rate, rate cut, rate hike, basis points, Fed funds rate, ECB, Bank of England, Riksbank, BOJ, Bank of Japan, RBA, Norges Bank, inflation, CPI, PCE, core CPI, Jerome Powell, Christine Lagarde, dot plot, forward guidance, quantitative easing, QE, quantitative tightening, QT, yield curve, inverted yield curve, recession, soft landing, stagflation, non-farm payroll, NFP, unemployment rate, Sahm Rule, pivot, emergency rate cut, inter-meeting, Jackson Hole ``` ### Remix Signal Ideas - **CME FedWatch**: When FedWatch prices ≥85% for a near-term decision and Polymarket prices <70%, the divergence is structural — wire FedWatch probability directly into `p` and the conviction formula becomes the arbitrage signal; the gap reliably closes within 15–45 minutes of Fed communication - **Cleveland Fed CPI Nowcast**: Updated daily using TIPS breakevens and commodity prices; consistently within 0.1pp of final CPI print — compare to Polymarket "will CPI be above/below X?" markets in the 72 hours before the BLS release - **FRED API**: 10Y-2Y yield spread, SOFR term structure, Sahm Rule indicator — all free, updated daily; yield curve and recession markets on Polymarket are almost never priced against the actual FRED data - **BLS / BEA Release Calendar**: CPI release schedule (bls.gov), PCE release schedule (bea.gov), NFP dates — the 30-minute window before each release is when rate-path markets are most mispriced relative to the incoming data ## Safety & Execution Mode **The skill defaults to paper trading (`venue="sim"`). Real trades only with `--live` flag.** | Scenario | Mode | Financial risk | |---|---|---| | `python trader.py` | Paper (sim) | None | | Cron / automaton | Paper (sim) | None | | `python trader.py --live` | Live (polymarket) | Real USDC | `autostart: false` and `cron: null` — nothing runs automatically until you configure it in Simmer UI. ## Required Credentials | Variable | Required | Notes | |---|---|---| | `SIMMER_API_KEY` | Yes | Trading authority. Treat as high-value credential. | ## Tunables (Risk Parameters) All declared as `tunables` in `clawhub.json` and adjustable from the Simmer UI. | Variable | Default | Purpose | |---|---|---| | `SIMMER_MAX_POSITION` | `35` | Max USDC per trade (reached at 100% conviction) | | `SIMMER_MIN_VOLUME` | `15000` | Min market volume filter (USD) — monetary policy markets are liquid | | `SIMMER_MAX_SPREAD` | `0.07` | Max bid-ask spread (7%) | | `SIMMER_MIN_DAYS` | `5` | Min days until resolution | | `SIMMER_MAX_POSITIONS` | `6` | Max concurrent open positions | | `SIMMER_YES_THRESHOLD` | `0.38` | Buy YES if market price ≤ this value | | `SIMMER_NO_THRESHOLD` | `0.62` | Sell NO if market price ≥ this value | | `SIMMER_MIN_TRADE` | `5` | Floor for any trade (min USDC regardless of conviction) | ## Dependency `simmer-sdk` by Simmer Markets (SpartanLabsXyz) - PyPI: https://pypi.org/project/simmer-sdk/ - GitHub: https://github.com/SpartanLabsXyz/simmer-sdk

path: clawhub.json
content: {
  "emoji": "\ud83c\udfe6",
  "requires": {
    "env": [
      "SIMMER_API_KEY"
    ],
    "pip": [
      "simmer-sdk"
    ]
  },
  "cron": null,
  "autostart": false,
  "automaton": {
    "managed": true,
    "entrypoint": "trader.py"
  },
  "tunables": [
    {
      "env": "SIMMER_MAX_POSITION",
      "type": "number",
      "default": 35,
      "range": [
        1,
        200
      ],
      "step": 1,
      "label": "Max position size (USD)"
    },
    {
      "env": "SIMMER_MIN_VOLUME",
      "type": "number",
      "default": 15000,
      "range": [
        0,
        500000
      ],
      "step": 500,
      "label": "Min market volume (USD)"
    },
    {
      "env": "SIMMER_MAX_SPREAD",
      "type": "number",
      "default": 0.07,
      "range": [
        0.01,
        0.5
      ],
      "step": 0.01,
      "label": "Max bid-ask spread"
    },
    {
      "env": "SIMMER_MIN_DAYS",
      "type": "number",
      "default": 5,
      "range": [
        1,
        180
      ],
      "step": 1,
      "label": "Min days until resolution"
    },
    {
      "env": "SIMMER_MAX_POSITIONS",
      "type": "number",
      "default": 6,
      "range": [
        1,
        20
      ],
      "step": 1,
      "label": "Max open positions"
    }
  ],
  "version": "0.0.2"
}

path: trader.py
content: """
polymarket-central-bank-trader
Trades Polymarket prediction markets on central bank decisions, interest rates, inflation prints, and Fed/ECB/Riksbank policy moves.

SAFE BY DEFAULT:
- No --live flag = paper trading (venue="sim"), zero financial risk.
- Cron/automaton runs default to paper mode.
- Real trades only with explicit --live flag.
"""
import os
import argparse
from datetime import datetime, timezone, date
from simmer_sdk import SimmerClient

TRADE_SOURCE = "sdk:polymarket-central-bank-trader"
SKILL_SLUG   = "polymarket-central-bank-trader"

KEYWORDS = [
    'Federal Reserve', 'Fed rate', 'FOMC', 'interest rate', 'rate cut', 'rate hike',
    'basis points', 'Fed funds rate', 'ECB', 'Bank of England', 'Riksbank', 'BOJ',
    'Bank of Japan', 'RBA', 'Norges Bank', 'inflation', 'CPI', 'PCE', 'core CPI',
    'Jerome Powell', 'Christine Lagarde', 'dot plot', 'forward guidance',
    'quantitative easing', 'QE', 'quantitative tightening', 'QT', 'yield curve',
    'inverted yield curve', 'recession', 'soft landing', 'stagflation',
    'non-farm payroll', 'NFP', 'unemployment rate', 'Sahm Rule', 'pivot',
    'emergency rate cut', 'inter-meeting', 'Jackson Hole',
]

# Risk parameters — declared as tunables in clawhub.json, adjustable from Simmer UI.
# Named SIMMER_* so apply_skill_config() can load automaton-managed overrides.
MAX_POSITION   = float(os.environ.get("SIMMER_MAX_POSITION",  "35"))
MIN_VOLUME     = float(os.environ.get("SIMMER_MIN_VOLUME",    "15000"))
MAX_SPREAD     = float(os.environ.get("SIMMER_MAX_SPREAD",    "0.07"))
MIN_DAYS       = int(os.environ.get(  "SIMMER_MIN_DAYS",      "5"))
MAX_POSITIONS  = int(os.environ.get(  "SIMMER_MAX_POSITIONS", "6"))
# Signal thresholds — buy YES below YES_THRESHOLD, sell NO above NO_THRESHOLD.
# Position size scales with conviction, adjusted by question tractability and
# position in the FOMC communication cycle (blackout vs. Fedspeak vs. post-meeting).
YES_THRESHOLD  = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.38"))
NO_THRESHOLD   = float(os.environ.get("SIMMER_NO_THRESHOLD",  "0.62"))
MIN_TRADE      = float(os.environ.get("SIMMER_MIN_TRADE",     "5"))

# FOMC decision dates (day the statement drops — second day of two-day meetings).
# Blackout starts 10 calendar days before each date.
# Published by the Federal Reserve Board at least one year in advance.
_FOMC_DECISION_DATES = [
    date(2026, 1, 29),
    date(2026, 3, 19),
    date(2026, 4, 30),
    date(2026, 6, 18),
    date(2026, 7, 30),
    date(2026, 9, 17),
    date(2026, 10, 29),
    date(2026, 12, 10),
    date(2027, 1, 29),
    date(2027, 3, 18),
    date(2027, 4, 29),
    date(2027, 6, 17),
    date(2027, 7, 29),
    date(2027, 9, 16),
    date(2027, 10, 28),
    date(2027, 12, 9),
]

# Quarterly SEP (dot plot) meetings: March, June, September, December.
# Fresh median dot = the Fed's own official rate path for the year.
_SEP_MONTHS = {3, 6, 9, 12}

_client: SimmerClient | None = None


def _fomc_cycle() -> tuple[str, float]:
    """
    Return (label, cycle_mult) based on position in the FOMC communication cycle.

    The Fed's communication cycle has three structurally distinct phases:

    POST-MEETING (0–5 days after decision):
      Maximum information density. The statement is public, the press conference
      transcript is indexed, and — in SEP months (Mar/Jun/Sep/Dec) — the dot plot
      gives the Fed's own median rate projection for the next 3 years. This is the
      window where CME FedWatch futures and Polymarket converge fastest. Trade with
      maximum conviction → 1.20x.

    FEDSPEAK WINDOW (11–20 days before next meeting):
      Fed officials speak freely and deliberately telegraph the upcoming decision.
      The Fed's 'no surprise' doctrine means this window is when forward guidance
      is sharpest — officials explicitly say 'the bar for a cut is high' or
      'conditions support further adjustment.' Retail hears the same speech but
      cannot translate Fedspeak into probability shifts → 1.15x.

    BLACKOUT (1–10 days before decision):
      The Fed imposes a strict communication blackout ~10 days before each meeting.
      No official can comment on monetary policy. The last forward guidance is
      already priced; no new signal can arrive. Rate decision markets under blackout
      are at maximum staleness — the signal quality drops sharply → 0.85x.

    MID-CYCLE (everything else):
      Normal inter-meeting period. Occasional Fedspeak but no concentrated signal
      density. Trade at base conviction → 1.00x.
    """
    today = datetime.now(timezone.utc).date()
    for fomc_date in sorted(_FOMC_DECISION_DATES):
        days_to = (fomc_date - today).days
        if days_to < -5:
            continue  # past meeting; skip to next
        if days_to <= 0:
            # 0-5 days after meeting (days_to is 0 or -1 to -5)
            label = f"post-FOMC({fomc_date.strftime('%b %d')})"
            # Extra boost if this is a SEP (dot plot) meeting
            dot_boost = 0.05 if fomc_date.month in _SEP_MONTHS else 0.0
            return label, 1.20 + dot_boost
        if days_to <= 10:
            return f"blackout({fomc_date.strftime('%b %d')})", 0.85
        if days_to <= 20:
            return f"fedspeak({fomc_date.strftime('%b %d')})", 1.15
        return f"mid-cycle({fomc_date.strftime('%b %d')})", 1.00
    return "mid-cycle", 1.00


def _data_calendar_overlay(question: str) -> float:
    """
    Secondary timing boost based on proximity to scheduled macro data releases.
    Compounds with FOMC cycle mult by returning an additional overlay multiplier.

    BLS CPI: released ~12th–15th of each month at 8:30 AM ET
    BEA PCE: released last week of month (25th+) at 8:30 AM ET
    BLS NFP: released first Friday (~1st–7th) at 8:30 AM ET
    """
    day = datetime.now(timezone.utc).day
    q = question.lower()

    is_inflation = any(w in q for w in ("cpi", "pce", "inflation", "core inflation",
                                          "price level", "consumer price", "deflation"))
    is_labor = any(w in q for w in ("unemployment", "nfp", "payroll", "jobs report",
                                      "labor market", "job gains", "sahm"))
    is_rate = any(w in q for w in ("rate cut", "rate hike", "rate hold", "basis points",
                                     "fed funds", "fomc", "rate decision"))

    in_cpi_window = 10 <= day <= 17   # BLS CPI release window
    in_pce_window = day >= 25          # BEA PCE release window
    in_nfp_window = 1 <= day <= 7     # BLS NFP release window

    if is_inflation and in_cpi_window:
        return 1.20  # CPI week: most market-moving data print; retail anchors to prior reading
    if is_inflation and in_pce_window:
        return 1.15  # PCE week: Fed's preferred measure; forward guidance recalibrates
    if is_labor and in_nfp_window:
        return 1.15  # NFP week: labor market signal shapes rate path for multiple meetings
    if is_rate and (in_cpi_window or in_pce_window or in_nfp_window):
        return 1.10  # Rate decision market during any major data week: signal freshest
    return 1.0


def central_bank_bias(question: str) -> float:
    """
    Returns a conviction multiplier (0.70–1.40) combining three structural edges
    unique to central bank and monetary policy prediction markets:

    1. QUESTION TYPE TRACTABILITY
       Monetary policy is the most heavily telegraphed policy in modern finance.
       The Fed's 'no surprise' doctrine — communicating intent weeks in advance
       to avoid market disruption — means near-term decisions are, by design,
       nearly pre-determined from public data. The tractability scale:

       Next-meeting rate decision (1–2 meetings out)              → 1.25x
         CME FedWatch federal funds futures have ~95% accuracy for
         decisions where futures price ≥85% probability. The Fed holds
         this track record deliberately — surprising markets destroys the
         credibility of forward guidance. Retail prices Polymarket at 75%
         when futures say 92%; that 17-point gap is structural edge.

       CPI / PCE inflation print hitting specific level            → 1.20x
         BLS CPI and BEA PCE are measured to two decimal places.
         Cleveland Fed CPI Nowcast gives a real-time estimate updated
         daily using bond market inflation expectations and commodity
         prices — consistently within 0.1pp of the final print. Retail
         anchors to the prior month's number and doesn't check nowcasts.

       Year-end Fed funds rate target / annual cut count           → 1.15x
         The dot plot (median projection) gives the Fed's own central
         tendency. The CME FedWatch futures curve gives market-implied
         path through every remaining meeting of the year. Both are
         public, precise, and almost never read by Polymarket traders.

       Yield curve inversion / un-inversion by date               → 1.10x
         FRED publishes 10Y-2Y and 10Y-3M spreads daily. Duration and
         direction predictable from Fed path; retail treats yield curve
         questions as abstract rather than trackable.

       Recession declaration / GDP contraction by date             → 0.80x
         NBER's official recession declaration takes 6–18 months after
         the actual peak — it is backward-looking by definition. The
         Sahm Rule (real-time recession indicator, triggers when 3-month
         unemployment average rises 0.5pp above prior 12-month low) is
         a far better predictor, but virtually unknown to retail.
         Markets for 'recession by X' are chronically mis-sized because
         the resolution criteria (NBER) lags the signal (Sahm) by a year.

       Chair fired / resign / succession                           → 0.75x
         Technically a political question once it reaches Polymarket.
         Fed independence norms are strong (only one removal in modern
         history — forced or otherwise) but political risk is real.
         Resolution timing is entirely in executive/legislative hands.

       Emergency / inter-meeting rate action                       → 0.70x
         The most reliably overpriced category in monetary policy markets.
         Emergency inter-meeting cuts happened in 2001 (9/11), 2008
         (financial crisis), and 2020 (COVID) — three events in 25 years.
         Retail overprices in every crisis because those three moments
         are vivid and salient. In any given 3-month window the base
         rate for an emergency cut is near zero even in stressed markets.

    2. FOMC COMMUNICATION CYCLE (computed from official meeting calendar)
       The Fed's communication cycle creates predictable windows of high and
       low information quality:
       - Post-meeting (0–5 days): 1.20x (+0.05 in SEP/dot-plot months)
       - Fedspeak window (11–20 days before): 1.15x
       - Blackout (1–10 days before): 0.85x
       - Mid-cycle: 1.00x

    3. DATA CALENDAR OVERLAY
       Inflation and labor market questions get an additional boost during
       the release windows for CPI (10th–17th), PCE (25th+), and NFP (1st–7th).
       Peak: CPI release week for inflation questions → 1.20x overlay.

    All three factors compound; capped at 1.40x.
    Next-meeting cut + post-SEP-FOMC + CPI week: 1.25 × 1.25 × 1.20 → 1.40x cap.
    Emergency cut any time: 0.70x — near MIN_TRADE floor.
    Blackout + recession: 0.80 × 0.85 = 0.68 → near floor.
    """
    q = question.lower()

    # Factor 1: question type tractability
    if any(w in q for w in ("rate cut", "rate hike", "rate hold", "basis points",
                              "fed funds", "fomc decision", "rate decision",
                              "cut rates", "hike rates", "pause", "skip",
                              "25bp", "50bp", "75bp", "hold rates")):
        type_mult = 1.25  # No-surprise doctrine; futures track record ~95% near-term

    elif any(w in q for w in ("cpi", "pce", "inflation", "consumer price",
                               "core inflation", "deflation", "price level",
                               "above 3%", "below 2%", "2% target")):
        type_mult = 1.20  # Measured precisely; Cleveland Fed nowcast within 0.1pp

    elif any(w in q for w in ("year-end rate", "fed funds rate by", "rate by december",
                               "rate cuts this year", "rate hikes this year",
                               "how many cuts", "how many hikes", "dot plot",
                               "end of year rate", "rate target by")):
        type_mult = 1.15  # Dot plot + futures curve both public and precise

    elif any(w in q for w in ("yield curve", "inverted", "uninvert", "10-year",
                               "2-year spread", "10y-2y", "10y-3m", "term premium",
                               "treasury spread")):
        type_mult = 1.10  # FRED daily; duration predictable from rate path

    elif any(w in q for w in ("recession", "gdp contraction", "two quarters",
                               "negative growth", "nber", "sahm", "downturn")):
        type_mult = 0.80  # NBER lags 6-18 months; Sahm Rule unknown to retail

    elif any(w in q for w in ("powell", "lagarde", "chair", "governor", "fired",
                               "resign", "reappoint", "nomination", "succession",
                               "replace the fed", "fed chair")):
        type_mult = 0.75  # Political question; norm protection strong but not certain

    elif any(w in q for w in ("emergency cut", "emergency rate", "inter-meeting",
                               "unscheduled", "surprise cut", "surprise hike",
                               "emergency meeting")):
        type_mult = 0.70  # Three times in 25 years; retail overprices in every crisis

    else:
        type_mult = 1.0

    # Factor 2: FOMC communication cycle
    _, cycle_mult = _fomc_cycle()
    cycle_mult = min(1.25, cycle_mult)  # clamp individual factor

    # Blackout dampens rate-decision markets most; less impact on inflation/recession questions
    is_rate_question = any(w in q for w in ("rate cut", "rate hike", "fomc", "basis points",
                                              "fed funds", "rate decision", "cut rates",
                                              "hike rates", "year-end rate", "how many cuts"))
    if not is_rate_question:
        # For non-rate questions, blackout matters less; mid-cycle toward baseline
        cycle_mult = max(cycle_mult, 1.0) if cycle_mult < 1.0 else cycle_mult

    # Factor 3: Data calendar overlay — additional boost during release windows
    data_mult = _data_calendar_overlay(question)

    raw = type_mult * cycle_mult * data_mult
    return min(1.40, raw)


def compute_signal(market) -> tuple[str | None, float, str]:
    """
    Returns (side, size, reasoning) or (None, 0, skip_reason).

    Conviction-based sizing with Fed communication cycle awareness:
    - Base conviction scales linearly with distance from threshold
    - central_bank_bias() applies three compounding structural edges:
      (1) question tractability — near-term rate decisions are near-certain from
          CME FedWatch futures; retail still prices them as uncertain
      (2) FOMC cycle position — maximum conviction post-meeting (fresh guidance,
          dot plot); minimum conviction during blackout (no new signal possible)
      (3) data calendar overlay — CPI/PCE/NFP release weeks boost inflation and
          rate-decision markets when data is freshest
    - Emergency cut / recession / chair questions dampened — rare events and
      backward-looking definitions structurally inflate retail pricing
    - Result capped at 1.0 so size never exceeds MAX_POSITION
    - MIN_TRADE floor prevents trivially small orders near the boundary

    Remix: wire CME FedWatch probability directly into p — when FedWatch says 92%
    and Polymarket says 75%, the 17-point arbitrage is structural, not random.
    """
    p = market.current_probability
    q = market.question

    # Spread gate
    if market.spread_cents is not None and market.spread_cents / 100 > MAX_SPREAD:
        return None, 0, f"Spread {market.spread_cents/100:.1%} > {MAX_SPREAD:.1%}"

    # Days-to-resolution gate
    if market.resolves_at:
        try:
            resolves = datetime.fromisoformat(market.resolves_at.replace("Z", "+00:00"))
            days = (resolves - datetime.now(timezone.utc)).days
            if days < MIN_DAYS:
                return None, 0, f"Only {days} days to resolve"
        except Exception:
            pass

    bias = central_bank_bias(q)

    if p <= YES_THRESHOLD:
        conviction = min(1.0, (YES_THRESHOLD - p) / YES_THRESHOLD * bias)
        size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2))
        edge = YES_THRESHOLD - p
        return "yes", size, f"YES {p:.0%} edge={edge:.0%} bias={bias:.2f}x size=${size} — {q[:65]}"

    if p >= NO_THRESHOLD:
        conviction = min(1.0, (p - NO_THRESHOLD) / (1 - NO_THRESHOLD) * bias)
        size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2))
        edge = p - NO_THRESHOLD
        return "no", size, f"NO YES={p:.0%} edge={edge:.0%} bias={bias:.2f}x size=${size} — {q[:65]}"

    return None, 0, f"Neutral at {p:.1%} (outside {YES_THRESHOLD:.0%}/{NO_THRESHOLD:.0%} bands)"


def context_ok(client: SimmerClient, market_id: str) -> tuple[bool, str]:
    """Check flip-flop and slippage safeguards."""
    try:
        ctx = client.get_market_context(market_id)
        if not ctx:
            return True, "no context"
        if ctx.get("discipline", {}).get("is_flip_flop"):
            reason = ctx["discipline"].get("flip_flop_reason", "recent reversal")
            return False, f"Flip-flop: {reason}"
        slip = ctx.get("slippage", {})
        if isinstance(slip, dict) and slip.get("slippage_pct", 0) > 0.15:
            return False, f"Slippage {slip['slippage_pct']:.1%}"
        for w in ctx.get("warnings", []):
            print(f"  [warn] {w}")
    except Exception as e:
        print(f"  [ctx] {market_id}: {e}")
    return True, "ok"


def get_client(live: bool = False) -> SimmerClient:
    """
    live=False → venue="sim"  (paper trades — safe default).
    live=True  → venue="polymarket" (real trades, only with --live flag).
    """
    global _client, MAX_POSITION, MIN_VOLUME, MAX_SPREAD, MIN_DAYS, MAX_POSITIONS, YES_THRESHOLD, NO_THRESHOLD, MIN_TRADE
    if _client is None:
        venue = "polymarket" if live else "sim"
        _client = SimmerClient(
            [REDACTED_SECRET]"SIMMER_API_KEY"],
            venue=venue,
        )
        if live:
            _client.live = True
        try:
            _client.apply_skill_config(SKILL_SLUG)
        except AttributeError:
            pass  # apply_skill_config only available in Simmer runtime
        MAX_POSITION   = float(os.environ.get("SIMMER_MAX_POSITION",  str(MAX_POSITION)))
        MIN_VOLUME     = float(os.environ.get("SIMMER_MIN_VOLUME",    str(MIN_VOLUME)))
        MAX_SPREAD     = float(os.environ.get("SIMMER_MAX_SPREAD",    str(MAX_SPREAD)))
        MIN_DAYS       = int(os.environ.get(  "SIMMER_MIN_DAYS",      str(MIN_DAYS)))
        MAX_POSITIONS  = int(os.environ.get(  "SIMMER_MAX_POSITIONS", str(MAX_POSITIONS)))
        YES_THRESHOLD  = float(os.environ.get("SIMMER_YES_THRESHOLD", str(YES_THRESHOLD)))
        NO_THRESHOLD   = float(os.environ.get("SIMMER_NO_THRESHOLD",  str(NO_THRESHOLD)))
        MIN_TRADE      = float(os.environ.get("SIMMER_MIN_TRADE",     str(MIN_TRADE)))
    return _client


def find_markets(client: SimmerClient) -> list:
    """Find active markets matching strategy keywords, deduplicated."""
    seen, unique = set(), []
    for kw in KEYWORDS:
        try:
            for m in client.find_markets(query=kw):
                if m.id not in seen:
                    seen.add(m.id)
                    unique.append(m)
        except Exception as e:
            print(f"[search] {kw!r}: {e}")
    return unique


def run(live: bool = False) -> None:
    mode = "LIVE" if live else "PAPER (sim)"
    fomc_label, _ = _fomc_cycle()
    sep_tag = " +dot-plot" if datetime.now(timezone.utc).month in _SEP_MONTHS else ""
    print(f"[polymarket-central-bank-trader] mode={mode} max_pos=${MAX_POSITION} min_vol=${MIN_VOLUME} fomc={fomc_label}{sep_tag}")

    client  = get_client(live=live)
    markets = find_markets(client)
    print(f"[polymarket-central-bank-trader] {len(markets)} candidate markets")

    placed = 0
    for m in markets:
        if placed >= MAX_POSITIONS:
            break

        side, size, reasoning = compute_signal(m)
        if not side:
            print(f"  [skip] {reasoning}")
            continue

        ok, why = context_ok(client, m.id)
        if not ok:
            print(f"  [skip] {why}")
            continue

        try:
            r = client.trade(
                market_id=m.id,
                side=side,
                amount=size,
                source=TRADE_SOURCE,
                skill_slug=SKILL_SLUG,
                reasoning=reasoning,
            )
            tag    = "(sim)" if r.simulated else "(live)"
            status = "OK" if r.success else f"FAIL:{r.error}"
            print(f"  [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:70]}")
            if r.success:
                placed += 1
        except Exception as e:
            print(f"  [error] {m.id}: {e}")

    print(f"[polymarket-central-bank-trader] done. {placed} orders placed.")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Trades Polymarket prediction markets on central bank decisions, interest rates, inflation prints, and Fed/ECB/Riksbank policy moves.")
    ap.add_argument("--live", action="store_true",
                    help="Real trades on Polymarket. Default is paper (sim) mode.")
    run(live=ap.parse_args().live)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)