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

--- name: polymarket-biotech-trader description: Trades Polymarket prediction markets on FDA drug approvals, biotech IPOs, clinical trial outcomes, pharma M&A, and precision medicine milestones. Use when you want to capture alpha on high-impact binary biotech events using PDUFA dates, clinical trial registries, and FDA calendar signals. metadata: author: Diagnostikon version: '1.0' displayName: Biotech & FDA Trader difficulty: advanced --- # Biotech & FDA Trader > **This is a template.** > The default signal is keyword discovery + FDA PDUFA calendar alignment — remix it with ClinicalTrials.gov API for trial status, BioMedTracker historical approval rates by indication, or biotech earnings call sentiment analysis. > The skill handles all the plumbing (market discovery, trade execution, safeguards). Your agent provides the alpha. ## Strategy Overview The global biotech market is valued at $1.77 trillion (2025) and growing to $6.34 trillion by 2035 (13.6% CAGR). Biotech prediction markets have some of the highest information density — FDA decisions are binary, have clear dates (PDUFA), and are well-studied. This skill trades: - **FDA approvals** — Breakthrough therapy designations, CRISPR/oncology approvals, GLP-1 next-gen - **Clinical trials** — Phase 3 outcomes, trial enrollment milestones - **Biotech IPOs** — Class of 2026 IPO announcement and performance markets - **Pharma M&A** — Large-cap acquisition deals in oncology, AI drug discovery - **Precision medicine** — CAR-T, mRNA vaccine, liquid biopsy milestones Key insight: FDA approval rates by indication are well-documented. When Polymarket prices deviate significantly from historical base rates, there's tradeable edge. ## Signal Logic ### Default Signal: PDUFA Date Calendar + Base Rate Analysis 1. Discover active biotech/FDA markets on Polymarket 2. Match markets to FDA PDUFA calendar (public, updated monthly) 3. Look up historical approval rates for the specific drug class/indication 4. Compare base rate (e.g., oncology breakthrough therapy ~85% approval) vs market price 5. If market prices approval at <70% for a high-base-rate indication, buy YES 6. Use context check to avoid overtraded markets before major catalysts ### Remix Ideas - **ClinicalTrials.gov API**: Monitor trial status changes (recruiting → completed) as leading indicator - **BioMedTracker / Citeline**: Historical approval rate databases by indication - **FDA press releases**: Real-time monitoring for early approval/rejection signals - **Biotech Twitter**: KOL (Key Opinion Leader) sentiment before PDUFA dates - **Short interest data**: High short interest in a biotech before PDUFA = market expects rejection — price this in ## Market Categories Tracked ```python BIOTECH_KEYWORDS = [ "FDA", "approval", "CRISPR", "cancer", "oncology", "clinical trial", "phase 3", "drug", "therapy", "vaccine", "mRNA", "CAR-T", "Alzheimer", "GLP-1", "biotech", "pharma", "M&A", "IPO", "diagnostic", "liquid biopsy", "gene editing", "sickle cell", "precision medicine", "antibody" ] ``` ## Risk Parameters | Parameter | Default | Notes | |-----------|---------|-------| | Max position size | $35 USDC | Binary events warrant careful sizing | | Min market volume | $5,000 | FDA markets attract informed traders | | Max bid-ask spread | 10% | Allow wider for rare disease markets | | Min days to resolution | 7 | Don't enter days before PDUFA | | Max open positions | 6 | Biotech events are correlated | ## FDA Approval Base Rates (Reference) | Indication | Historical Approval Rate | |-----------|--------------------------| | Oncology (breakthrough designation) | ~85% | | Rare disease (orphan drug) | ~70% | | CNS/Neurology | ~55% | | General approval (Phase 3 success) | ~65% | | CRISPR/gene therapy (novel) | ~45% | ## Key Data Sources - **FDA PDUFA Calendar**: https://www.fda.gov/patients/drug-development-process/step-4-fda-drug-review - **ClinicalTrials.gov API**: https://clinicaltrials.gov/api/ - **SEC EDGAR** (biotech S-1 filings): https://efts.sec.gov/LATEST/search-index?q=%22biotech%22&dateRange=custom ## Installation & Setup ```bash clawhub install polymarket-biotech-trader ``` Requires: `SIMMER_API_KEY` environment variable. ## Cron Schedule Runs every 20 minutes (`*/20 * * * *`). FDA decisions are infrequent but high-impact; moderate polling is sufficient. ## 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` | `35` | Max USDC per trade (reached at 100% conviction) | | `SIMMER_MIN_VOLUME` | `5000` | Min market volume filter (USD) | | `SIMMER_MAX_SPREAD` | `0.10` | Max bid-ask spread (0.10 = 10%) | | `SIMMER_MIN_DAYS` | `7` | Min days until market resolves | | `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` 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: clawhub.json
content: {
  "emoji": "\ud83e\uddec",
  "requires": {
    "env": [
      "SIMMER_API_KEY"
    ],
    "pip": [
      "simmer-sdk"
    ]
  },
  "cron": null,
  "autostart": false,
  "automaton": {
    "managed": true,
    "entrypoint": "trader.py"
  },
  "tunables": [
    {
      "env": "SIMMER_MAX_POSITION",
      "default": 35,
      "range": [
        1,
        200
      ],
      "step": 1,
      "label": "Max position size (USD)",
      "type": "number"
    },
    {
      "env": "SIMMER_MIN_VOLUME",
      "default": 1000,
      "range": [
        0,
        100000
      ],
      "step": 500,
      "label": "Min market volume (USD)",
      "type": "number"
    },
    {
      "env": "SIMMER_MAX_SPREAD",
      "default": 0.1,
      "range": [
        0.01,
        0.5
      ],
      "step": 0.01,
      "label": "Max bid-ask spread",
      "type": "number"
    },
    {
      "env": "SIMMER_MIN_DAYS",
      "default": 0,
      "range": [
        0,
        90
      ],
      "step": 1,
      "label": "Min days until resolution",
      "type": "number"
    },
    {
      "env": "SIMMER_MAX_POSITIONS",
      "default": 8,
      "range": [
        1,
        20
      ],
      "step": 1,
      "label": "Max open positions",
      "type": "number"
    },
    {
      "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"
}

path: trader.py
content: """
polymarket-biotech-trader
Trades biotech, FDA approval, clinical trial, and pharma M&A 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-biotech-trader"
SKILL_SLUG   = "polymarket-biotech-trader"

KEYWORDS = ['FDA', 'approval', 'CRISPR', 'cancer', 'oncology', 'clinical trial', 'phase 3', 'drug therapy', 'vaccine', 'mRNA', 'CAR-T', 'Alzheimer', 'GLP-1', 'biotech', 'pharma', 'acquisition']

# 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",  "35"))
MIN_VOLUME     = float(os.environ.get("SIMMER_MIN_VOLUME",    "1000"))
MAX_SPREAD     = float(os.environ.get("SIMMER_MAX_SPREAD",    "0.1"))
MIN_DAYS       = int(os.environ.get(  "SIMMER_MIN_DAYS",      "0"))
MAX_POSITIONS  = int(os.environ.get(  "SIMMER_MAX_POSITIONS", "6"))
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 ("fda", "approval", "crispr", "cancer", "vaccine", "drug", "clinical trial")):
                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 ("fda", "approval", "crispr", "cancer", "vaccine", "drug", "clinical trial")):
                seen.add(m.id)
                unique.append(m)
    except Exception:
        pass

    return unique


def compute_signal(market) -> tuple[str | None, float, str]:
    """
    Returns (side, size, reasoning) or (None, 0, skip_reason).
    Conviction-based sizing: larger edge → larger position, up to MAX_POSITION.
    FDA base rate calibration vs market price. Remix: ClinicalTrials.gov API, BioMedTracker, short interest.
    """
    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

    if p <= YES_THRESHOLD:
        conviction = (YES_THRESHOLD - p) / YES_THRESHOLD
        size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2))
        edge = YES_THRESHOLD - p
        return "yes", size, f"YES {p:.0%} edge={edge:.0%} size=${size} — {q[:70]}"

    if p >= NO_THRESHOLD:
        conviction = (p - NO_THRESHOLD) / (1 - NO_THRESHOLD)
        size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2))
        edge = p - NO_THRESHOLD
        return "no", size, f"NO YES={p:.0%} edge={edge:.0%} size=${size} — {q[:70]}"

    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-biotech-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-biotech-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-biotech-trader] done. {placed} orders placed.")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Trades biotech, FDA approval, clinical trial, and pharma M&A 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)


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