Skill: diagnostikon/polymarket-climate-trader (version 0.0.3)

--- name: polymarket-climate-trader description: Trades Polymarket prediction markets on weather extremes, climate milestones, natural disasters, and agricultural outcomes. Use when you want to capture alpha on temperature records, hurricane seasons, flood events, and CO2 threshold markets using meteorological data signals. metadata: author: Diagnostikon version: '1.0' displayName: Climate & Weather Trader difficulty: intermediate --- # Climate & Weather Trader > **This is a template.** > The default signal is keyword discovery + NOAA/weather API data alignment — remix it with ForecastEx climate oracle feeds, satellite NDVI data for agriculture, or ensemble weather model outputs. > The skill handles all the plumbing (market discovery, trade execution, safeguards). Your agent provides the alpha. ## Strategy Overview Climate prediction markets are one of the fastest-growing underserved categories. Polymarket has 151+ active climate markets but most are basic. This skill captures alpha on: - **Temperature extremes** — record highs, heatwaves, frost events - **Natural catastrophes** — hurricane counts, earthquake magnitudes, wildfire acreage - **Climate milestones** — CO2 ppm thresholds, Arctic sea ice minimums - **Agricultural impacts** — wheat yields, drought-driven crop failures, water allocations Climate markets are uniquely suited for quantitative trading: the data sources are public, verifiable, and highly structured (NOAA, ECMWF, NASA). ## Signal Logic ### Default Signal: Conviction-Based Sizing with Seasonal Bias 1. Discover active climate/weather markets on Polymarket 2. Compute base conviction from distance to threshold (0% at boundary → 100% at p=0/p=1) 3. Apply `season_bias()` multiplier based on current month and event type in the question 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 ### Seasonal Bias (built-in, no API required) Climate events follow documented seasonal cycles. `season_bias()` boosts conviction when the current month aligns with peak season, and dampens it off-season: | Event type | Peak season | In-season multiplier | Off-season multiplier | |---|---|---|---| | Hurricane / cyclone | June–November | **1.4x** | 0.6x | | Sea ice / Arctic | July–September | **1.4x** | 0.7x | | El Niño / La Niña / ENSO | December–February | **1.3x** | 0.9x | | Wildfire / fire season | July–October | **1.3x** | 0.8x | | Heatwave / drought | June–September | **1.3x** | 0.8x | | Snowfall / blizzard | November–March | **1.3x** | 0.7x | Example: a hurricane market at 25% in October → conviction 34% × 1.4x bias = 48% → $12 position. Same market in January → 34% × 0.6x = 20% → $5 (floor). ### Remix Ideas - **ECMWF ensemble**: Replace `market.current_probability` with model consensus probability — trade the divergence between forecast and market - **NOAA ENSO index**: Feed ONI values directly to boost/reduce `season_bias()` for El Niño markets - **Insurance cat bond pricing**: Use ILS spreads as implied probability benchmarks for hurricane markets - **Copernicus climate data**: Real-time European climate services for local/regional temperature markets ## Market Categories Tracked ```python KEYWORDS = [ 'hurricane', 'tropical storm', 'cyclone', 'tornado', 'flood', 'drought', 'wildfire', 'earthquake', 'CO2', 'sea ice', 'Arctic', 'El Niño', 'La Niña', 'ENSO', 'snowfall', 'heatwave', 'heat wave', 'temperature record', 'crop yield', 'wheat', 'harvest', 'glacier', 'rainfall', 'water shortage', 'climate', 'emissions', 'carbon', ] ``` ## Risk Parameters | Parameter | Default | Notes | |-----------|---------|-------| | Max position size | $20 USDC | Per market | | Min market volume | $3,000 | Climate markets are less liquid | | Max bid-ask spread | 12% | Wider allowed for niche markets | | Min days to resolution | 14 | Weather requires sufficient lead time | | Max open positions | 8 | Diversify across events | ## Key Data Sources - **NOAA Climate Data Online**: https://www.ncdc.noaa.gov/cdo-web/ - **Open-Meteo API**: https://open-meteo.com/ (free, no key required) - **Copernicus C3S**: https://cds.climate.copernicus.eu/ - **ForecastEx**: https://forecastex.com/ ## Installation & Setup ```bash clawhub install polymarket-climate-trader ``` Requires: `SIMMER_API_KEY` environment variable. Optional: `OPENMETEO_API_KEY`. ## Cron Schedule Runs every 30 minutes (`*/30 * * * *`). Weather data updates every 1–6 hours; no need to poll faster. ## Safety & Execution Mode **The skill defaults to paper trading (`venue="sim"`). Real trades only execute when `--live` is passed explicitly.** | Scenario | Mode | Financial risk | |----------|------|----------------| | `python trader.py` | Paper (sim) | None | | Cron / automaton | Paper (sim) | None | | `python trader.py --live` | Live (polymarket) | Real USDC | The automaton cron is set to `null` — it does not run on a schedule until you configure it in the Simmer UI. `autostart: false` means it won't start automatically on install. ## Required Credentials | Variable | Required | Notes | |----------|----------|-------| | `SIMMER_API_KEY` | Yes | Trading authority — keep this credential private. Do not place a live-capable key in any environment where automated code could call `--live`. | ## Tunables (Risk Parameters) All risk parameters are declared in `clawhub.json` as `tunables` and adjustable from the Simmer UI without code changes. They use `SIMMER_`-prefixed env vars so `apply_skill_config()` can load them securely. | Variable | Default | Purpose | |----------|---------|---------| | `SIMMER_MAX_POSITION` | `25` | Max USDC per trade (reached at 100% conviction) | | `SIMMER_MIN_VOLUME` | `3000` | Min market volume filter (USD) | | `SIMMER_MAX_SPREAD` | `0.12` | Max bid-ask spread (0.12 = 12%) | | `SIMMER_MIN_DAYS` | `14` | Min days until market resolves | | `SIMMER_MAX_POSITIONS` | `8` | 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` is published on PyPI by Simmer Markets. - PyPI: https://pypi.org/project/simmer-sdk/ - GitHub: https://github.com/SpartanLabsXyz/simmer-sdk - Publisher: [REDACTED_SECRET] Review the source before providing live credentials if you require full auditability.

path: trader.py
content: """
polymarket-climate-trader
Trades climate, weather, natural disaster, and agricultural prediction markets on Polymarket.

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
from simmer_sdk import SimmerClient

TRADE_SOURCE = "sdk:polymarket-climate-trader"
SKILL_SLUG   = "polymarket-climate-trader"

KEYWORDS = [
    'hurricane', 'tropical storm', 'cyclone', 'tornado', 'flood',
    'drought', 'wildfire', 'earthquake', 'CO2', 'sea ice', 'Arctic',
    'El Niño', 'La Niña', 'ENSO', 'snowfall', 'heatwave', 'heat wave',
    'temperature record', 'crop yield', 'wheat', 'harvest', 'glacier',
    'rainfall', 'water shortage', 'climate', 'emissions', 'carbon',
]

# Risk parameters — declared as tunables in clawhub.json, tunable from Simmer UI.
# Named SIMMER_* so apply_skill_config() can load automaton-managed overrides.
MAX_POSITION   = float(os.environ.get("SIMMER_MAX_POSITION",  "25"))
MIN_VOLUME     = float(os.environ.get("SIMMER_MIN_VOLUME",    "1000"))
MAX_SPREAD     = float(os.environ.get("SIMMER_MAX_SPREAD",    "0.12"))
MIN_DAYS       = int(os.environ.get(  "SIMMER_MIN_DAYS",      "0"))
MAX_POSITIONS  = int(os.environ.get(  "SIMMER_MAX_POSITIONS", "8"))
# Signal thresholds — buy YES below YES_THRESHOLD, sell NO above NO_THRESHOLD.
# Position size scales with conviction, further boosted/dampened by seasonal alignment.
YES_THRESHOLD  = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.42"))
NO_THRESHOLD   = float(os.environ.get("SIMMER_NO_THRESHOLD",  "0.58"))
MIN_TRADE      = float(os.environ.get("SIMMER_MIN_TRADE",     "5"))

_client: SimmerClient | None = None


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,
        )
        # Load tunable overrides set via the Simmer UI (SIMMER_* vars only).
        if live:
            _client.live = True
        try:
            _client.apply_skill_config(SKILL_SLUG)
        except AttributeError:
            pass  # apply_skill_config only available in Simmer runtime
        # Re-read params in case apply_skill_config updated os.environ.
        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(), []

    # Fast markets (may not exist in older SDK)
    try:
        for m in client.get_fast_markets():
            q = getattr(m, "question", "").lower()
            if m.id not in seen and any(w in q for w in ("climate", "hurricane", "flood",
                    "tornado", "wildfire", "temperature", "weather", "drought", "carbon")):
                seen.add(m.id)
                unique.append(m)
    except (AttributeError, Exception):
        pass

    # Keyword search
    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}")

    # Bulk scan fallback
    try:
        for m in client.get_markets(limit=200):
            q = getattr(m, "question", "").lower()
            if m.id not in seen and any(w in q for w in ("climate", "hurricane", "flood",
                    "tornado", "wildfire", "temperature", "drought", "carbon", "emissions")):
                seen.add(m.id)
                unique.append(m)
    except Exception:
        pass

    return unique


def season_bias(question: str) -> float:
    """
    Returns a conviction multiplier (0.6–1.4) based on seasonal alignment.

    Climate events follow well-documented seasonal cycles. Trading with
    the season amplifies conviction; trading against it dampens it.
    No external API needed — just the current month.

    Examples (Northern Hemisphere):
      - Hurricane question in August  → 1.4x (peak season)
      - Hurricane question in January → 0.6x (off-season)
      - Wildfire question in September → 1.3x
      - El Niño question in January   → 1.3x (peak ENSO phase)
    """
    month = datetime.now(timezone.utc).month
    q = question.lower()

    # Atlantic hurricane season: June–November (peak Aug–Oct)
    if any(w in q for w in ("hurricane", "tropical storm", "cyclone", "typhoon")):
        return 1.4 if 6 <= month <= 11 else 0.6

    # Wildfire season: July–October
    if any(w in q for w in ("wildfire", "wildfire", "fire season", "burn area")):
        return 1.3 if 7 <= month <= 10 else 0.8

    # Arctic sea ice minimum: peaks in September
    if any(w in q for w in ("sea ice", "arctic", "ice extent", "ice area")):
        return 1.4 if 7 <= month <= 9 else 0.7

    # El Niño / La Niña: ENSO peaks December–February
    if any(w in q for w in ("el niño", "el nino", "la niña", "la nina", "enso")):
        return 1.3 if month in (12, 1, 2) else 0.9

    # Heatwave and drought: June–September
    if any(w in q for w in ("heatwave", "heat wave", "drought", "temperature record")):
        return 1.3 if 6 <= month <= 9 else 0.8

    # Winter storms and snowfall: November–March
    if any(w in q for w in ("snowfall", "blizzard", "frost", "freeze", "snowstorm")):
        return 1.3 if month in (11, 12, 1, 2, 3) else 0.7

    return 1.0  # no seasonal pattern detected — neutral multiplier


def compute_signal(market) -> tuple[str | None, float, str]:
    """
    Returns (side, size, reasoning) or (None, 0, skip_reason).

    Conviction-based sizing with seasonal boost/dampening:
    - Base conviction scales linearly with distance from threshold
    - season_bias() multiplies conviction up or down based on current month
    - Result is capped at 1.0 so size never exceeds MAX_POSITION
    - MIN_TRADE floor prevents trivially small orders near the boundary

    Remix: feed NOAA/Open-Meteo ensemble probabilities into p to replace
    the raw market price — trade the divergence between model and market.
    """
    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 = season_bias(q)

    if p <= YES_THRESHOLD:
        # conviction=0 at threshold boundary, conviction=1 at p=0 — scaled by seasonal bias
        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:.1f}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:.1f}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 run(live: bool = False) -> None:
    mode = "LIVE" if live else "PAPER (sim)"
    print(f"[polymarket-climate-trader] mode={mode} max_pos=${MAX_POSITION} min_vol=${MIN_VOLUME} max_spread={MAX_SPREAD:.0%} min_days={MIN_DAYS}")

    client  = get_client(live=live)
    markets = find_markets(client)
    print(f"[polymarket-climate-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-climate-trader] done. {placed} orders placed.")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Trades climate, weather, natural disaster, and agricultural prediction markets on Polymarket.")
    ap.add_argument("--live", action="store_true",
                    help="Real trades on Polymarket. Default is paper (sim) mode.")
    run(live=ap.parse_args().live)


path: clawhub.json
content: {
  "emoji": "\u2601\ufe0f",
  "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": 25,
      "range": [
        1,
        200
      ],
      "step": 1,
      "label": "Max position size (USD)"
    },
    {
      "env": "SIMMER_MIN_VOLUME",
      "type": "number",
      "default": 1000,
      "range": [
        0,
        100000
      ],
      "step": 500,
      "label": "Min market volume (USD)"
    },
    {
      "env": "SIMMER_MAX_SPREAD",
      "type": "number",
      "default": 0.12,
      "range": [
        0.01,
        0.5
      ],
      "step": 0.01,
      "label": "Max bid-ask spread"
    },
    {
      "env": "SIMMER_MIN_DAYS",
      "type": "number",
      "default": 0,
      "range": [
        0,
        90
      ],
      "step": 1,
      "label": "Min days until resolution"
    },
    {
      "env": "SIMMER_MAX_POSITIONS",
      "type": "number",
      "default": 8,
      "range": [
        1,
        20
      ],
      "step": 1,
      "label": "Max open positions"
    },
    {
      "env": "SIMMER_YES_THRESHOLD",
      "type": "number",
      "default": 0.42,
      "range": [
        0.01,
        0.49
      ],
      "step": 0.01,
      "label": "YES threshold"
    },
    {
      "env": "SIMMER_NO_THRESHOLD",
      "type": "number",
      "default": 0.58,
      "range": [
        0.51,
        0.99
      ],
      "step": 0.01,
      "label": "NO threshold"
    },
    {
      "env": "SIMMER_MIN_TRADE",
      "type": "number",
      "default": 5,
      "range": [
        1,
        100
      ],
      "step": 1,
      "label": "Min trade size (USD)"
    }
  ],
  "version": "0.0.2"
}

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