{
  "schema_version": "1.0.0",
  "bot_id": "0.3",
  "bot_name": "EventCalendarMapper",
  "slug": "eventcalendarmapper",
  "layer": "Discovery",
  "layer_key": "disc",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only",
    "Recommend"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Discovery",
    "bot_class": "Signal Service",
    "authority": "Read-only, Recommend",
    "runs_before": "Strategy OrderIntent generation",
    "runs_after": "MarketScanner scan cycle",
    "applies_to": "All live Polymarket markets with a known resolution horizon",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Intelligence pod"
  },
  "purpose": "Map every Polymarket market to a known real-world calendar event (elections, sports fixtures, court dates, FOMC releases, earnings, debates) and emit a time-tagged ObservationReport so strategies can act on pre-event positioning logic.",
  "why_it_matters": [
    {
      "failure": "Markets not linked to calendar events",
      "consequence": "Strategies cannot distinguish between a stagnant market and one where a price-moving event is imminent, missing pre-event edge opportunities."
    },
    {
      "failure": "Stale calendar data",
      "consequence": "Rescheduled or cancelled events leave strategies positioned on a timeline that no longer exists."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market resolution date, question text, and linked event identifiers",
      "source": "Gamma API",
      "required": true,
      "use": "Primary source for matching markets to calendar events via resolution date and question NLP."
    },
    {
      "input": "Market condition_id and neg-risk flag",
      "source": "Gamma API",
      "required": true,
      "use": "Identify neg-risk bundles that share a common real-world event."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "risk.kill_switch",
      "required": true,
      "use": "Suppress emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "event_lookahead_h \u00b7 int",
    "enabled_calendars \u00b7 list",
    "timezone_default \u00b7 enum",
    "min_event_relevance \u00b7 0\u20131"
  ],
  "parameters": [
    {
      "name": "event_lookahead_h",
      "default": 72,
      "warning": 24,
      "hard": 6,
      "controls": "How many hours ahead the mapper looks for calendar events to associate with markets.",
      "why_default_matters": "72 hours gives strategies enough lead time to build or unwind pre-event positions without excessive false positives.",
      "threshold_logic": [
        {
          "condition": ">= 72h",
          "action": "Standard lookahead"
        },
        {
          "condition": "24\u201372h",
          "action": "Narrow window \u2014 WARN"
        },
        {
          "condition": "< 6h",
          "action": "Reject \u2014 too short for pre-event positioning"
        }
      ],
      "dev_check": "if (h < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Markets linked to events happening within the next few days are flagged for time-sensitive strategies."
    },
    {
      "name": "min_event_relevance",
      "default": 0.6,
      "warning": 0.4,
      "hard": 0.2,
      "controls": "Minimum NLP match score between market question and calendar event description for a mapping to be accepted.",
      "why_default_matters": "A relevance floor of 0.6 prevents spurious mappings while allowing reasonable headline variations.",
      "threshold_logic": [
        {
          "condition": ">= 0.6",
          "action": "Map accepted"
        },
        {
          "condition": "0.4\u20130.6",
          "action": "Map accepted with LOW_RELEVANCE flag"
        },
        {
          "condition": "< 0.2",
          "action": "Map rejected \u2014 CALENDAR_MISMATCH"
        }
      ],
      "dev_check": "if (score < params.hard) emit(HARD_REJECT, 'CALENDAR_MISMATCH');",
      "user_facing": "Markets are only linked to calendar events when there is a strong match between the market question and the event."
    }
  ],
  "default_config": {
    "bot_id": "disc.event_calendar_mapper",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "event_lookahead_h": 72,
      "enabled_calendars": [
        "elections",
        "sports",
        "macro",
        "earnings"
      ],
      "timezone_default": "UTC",
      "min_event_relevance": 0.6
    }
  },
  "implementation_flow": [
    "On each mapping cycle, fetch all active markets from Gamma API.",
    "For each market, extract resolution date and normalise to UTC.",
    "Filter to markets with resolution date within event_lookahead_h.",
    "For each candidate, run NLP title-match against each enabled calendar feed to find the best-matching event.",
    "Accept mappings where match_score >= min_event_relevance; reject below hard floor with CALENDAR_MISMATCH.",
    "Emit ObservationReport with market_id, event_id, calendar_source, match_score, event_time_utc, hours_to_event.",
    "Log cycle summary with total markets mapped, unmatched count, and top mapping by relevance."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 EventCalendarMapper emits ObservationReports, not approvals.",
    "reshape_required": "Not applicable \u2014 read-only mapping bot.",
    "reject": "Markets that cannot be matched to any calendar event above the hard floor receive CALENDAR_MISMATCH and are not forwarded.",
    "warning_only": "Markets matched with relevance between warning and default receive LOW_RELEVANCE annotation."
  },
  "decision_output_schema": "ObservationReport",
  "decision_output_example": {
    "report_id": "0xccdd2233445566778899001122334455ccdd2233445566778899001122334455",
    "bot_id": "disc.event_calendar_mapper",
    "market_id": "0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
    "event_id": "us-election-2026-nov-03",
    "calendar_source": "elections",
    "match_score": 0.87,
    "event_time_utc": "2026-11-03T23:00:00Z",
    "hours_to_event": 48.5,
    "warnings": [],
    "mapped_at_ms": 1746789000000
  },
  "developer_log": {
    "bot_id": "disc.event_calendar_mapper",
    "cycle": 11,
    "markets_evaluated": 312,
    "markets_mapped": 24,
    "markets_unmatched": 288,
    "top_mapping": "0x8a9b... \u2192 us-election-2026-nov-03 (score 0.87)",
    "killswitch_active": false,
    "mapped_at": "2026-05-09T11:30:00Z"
  },
  "user_explanations": [
    {
      "situation": "Market shown with an upcoming event tag",
      "message": "This market is linked to a real-world event happening soon. Strategies that are time-sensitive will consider it for pre-event positioning."
    },
    {
      "situation": "Market not linked to any event",
      "message": "No matching calendar event was found for this market within the current lookahead window."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "NLP title-match produces a false positive mapping linking a market to the wrong event, causing strategies to apply incorrect pre-event timing logic.",
    "false_positive_risk": "A market with a generic title (e.g. 'Will X win?') may incorrectly match multiple calendar events, causing the bot to emit duplicate conflicting mappings.",
    "false_negative_risk": "Markets with non-standard question phrasing may not match any calendar event above threshold, missing genuine pre-event signals.",
    "safe_fallback": "If Gamma API is unavailable, halt the mapping cycle with STALE_MARKET_DATA rather than emitting stale mappings.",
    "required_dependencies": [
      "Gamma API live market list",
      "Enabled calendar feed data",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Mapping below hard floor emits CALENDAR_MISMATCH",
        "setup": "match_score=0.15, hard=0.2",
        "expected": "Market excluded from mapping output with CALENDAR_MISMATCH reason"
      },
      {
        "test": "Mapping in warning band gets LOW_RELEVANCE flag",
        "setup": "match_score=0.5, warning=0.4, default=0.6",
        "expected": "ObservationReport emitted with warnings=['LOW_RELEVANCE']"
      },
      {
        "test": "KillSwitch suppresses emissions",
        "setup": "killswitch.active=true",
        "expected": "No ObservationReports emitted"
      }
    ],
    "integration": [
      {
        "test": "Election market correctly maps to election calendar event",
        "expected": "ObservationReport with calendar_source='elections' and hours_to_event > 0"
      },
      {
        "test": "Gamma API outage halts cycle with STALE_MARKET_DATA",
        "expected": "No reports emitted; resumes next cycle"
      }
    ],
    "property": [
      {
        "property": "hours_to_event always > 0 at time of emission",
        "required": "Always true \u2014 past events are not mapped"
      },
      {
        "property": "No report emitted when KillSwitch active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Map every market to a known calendar (elections, sports fixtures, court dates, CPI/Fed releases, earnings, debates) for pre-event positioning.",
  "legacy_pm_signals": [
    "Market resolution date + timezone",
    "Condition-id metadata",
    "Linked-event identifiers from Gamma API"
  ],
  "legacy_external_feeds": [
    "Sports schedule APIs",
    "FRED macro releases",
    "Election commission feeds",
    "Court calendars"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Gamma API enableNegRisk flag used to group neg-risk bundles under a shared event_id. All timing uses UTC resolution dates from Gamma V2 market metadata."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION mappingCycle():\n  ks = FETCH internal.killswitch.status\n  IF ks.active: RETURN\n\n  markets = FETCH gamma.GET('/markets?active=true&closed=false')\n  IF markets IS NULL:\n    LOG ERROR 'Gamma API unavailable \u2014 halting mapping cycle'\n    RETURN\n\n  now_utc = current_time_utc()\n  horizon_cutoff = now_utc + params.event_lookahead_h * 3600\n\n  FOR market IN markets:\n    res_time = parse_utc(market.resolution_date)\n    IF res_time > horizon_cutoff: CONTINUE  // outside lookahead\n\n    best_match = NULL; best_score = 0.0\n    FOR calendar IN params.enabled_calendars:\n      events = calendarFeed(calendar, res_time)\n      FOR event IN events:\n        score = nlpMatch(market.question, event.description)\n        IF score > best_score:\n          best_match = event; best_score = score\n\n    IF best_score < params.min_event_relevance.hard:\n      LOG reason=CALENDAR_MISMATCH; CONTINUE\n\n    warnings = []\n    IF best_score < params.min_event_relevance.default:\n      warnings.append('LOW_RELEVANCE')\n\n    hours_to_event = (parse_utc(best_match.time) - now_utc) / 3600\n    EMIT ObservationReport(market_id, best_match.id, best_match.calendar,\n                           best_score, hours_to_event, warnings)\n\n  LOG cycle summary",
    "sdk_calls": [
      "gamma.GET('/markets?active=true&closed=false')",
      "calendarFeed('elections', resolution_date)",
      "nlpMatch(market.question, event.description)"
    ],
    "complexity": "O(M \u00d7 C \u00d7 E) where M=markets, C=calendars, E=events per calendar window"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Gamma market with election resolution date",
        "source": "gamma_api",
        "payload": {
          "condition_id": "0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
          "question": "Will Candidate A win the 2026 midterm election?",
          "active": true,
          "resolution_date": "2026-11-04T00:00:00Z",
          "neg_risk": false
        }
      }
    ],
    "output": [
      {
        "label": "ObservationReport \u2014 election market mapped",
        "payload": {
          "report_id": "0xccdd2233445566778899001122334455ccdd2233445566778899001122334455",
          "bot_id": "disc.event_calendar_mapper",
          "market_id": "0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
          "event_id": "us-election-2026-nov-03",
          "calendar_source": "elections",
          "match_score": 0.87,
          "event_time_utc": "2026-11-03T23:00:00Z",
          "hours_to_event": 48.5,
          "warnings": [],
          "mapped_at_ms": 1746789000000
        }
      }
    ],
    "curl": "curl 'https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=100'"
  },
  "reason_codes": [
    {
      "code": "CALENDAR_MISMATCH",
      "severity": "HARD_REJECT",
      "meaning": "No calendar event matched above the hard relevance floor; market not mapped.",
      "action": "Exclude from mapping output; log market_id and best attempt score.",
      "user_message": "This market could not be linked to any known upcoming event."
    },
    {
      "code": "LOW_RELEVANCE",
      "severity": "WARN",
      "meaning": "Best calendar match is between warning and default threshold.",
      "action": "Include mapping with LOW_RELEVANCE warning annotation.",
      "user_message": "This market is tentatively linked to an event but the match is not high-confidence."
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Gamma API unavailable; mapping cycle halted.",
      "action": "Halt cycle; retry on next interval.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch is active; all emissions suppressed.",
      "action": "Return immediately; do not emit any ObservationReports.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_disc_eventcalendarmapper_markets_evaluated_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "cycle"
        ],
        "meaning": "Total markets evaluated per mapping cycle."
      },
      {
        "name": "polytraders_disc_eventcalendarmapper_mappings_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "calendar_source"
        ],
        "meaning": "Successful calendar mappings emitted, broken down by calendar source."
      },
      {
        "name": "polytraders_disc_eventcalendarmapper_match_score",
        "type": "histogram",
        "unit": "ratio",
        "labels": [],
        "meaning": "Distribution of NLP match scores for accepted mappings."
      }
    ],
    "alerts": [
      {
        "name": "EventCalendarMapperNoMappings",
        "condition": "rate(polytraders_disc_eventcalendarmapper_mappings_emitted_total[30m]) == 0",
        "severity": "P2",
        "runbook": "#runbook-eventcalendarmapper-no-mappings"
      },
      {
        "name": "EventCalendarMapperGammaAPIDown",
        "condition": "rate(polytraders_disc_eventcalendarmapper_markets_evaluated_total[5m]) == 0",
        "severity": "P1",
        "runbook": "#runbook-eventcalendarmapper-gamma-api"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Discovery / EventCalendarMapper mapping coverage"
    ],
    "log_levels": {
      "DEBUG": "Per-market best match event, score, and calendar source.",
      "INFO": "Cycle summary: markets_evaluated, mappings_emitted.",
      "WARN": "Low mapping rate (<5%); calendar feed returning empty.",
      "ERROR": "Gamma API unavailable; all calendar feeds unreachable."
    }
  },
  "state": {
    "store": "in-memory event cache per calendar source",
    "shape": "{ event_id -> { description: str, time_utc: ts, calendar: str } }",
    "ttl": "1h; refreshed from calendar feeds each cycle",
    "recovery": "On cold start, calendar feeds are re-fetched on first cycle.",
    "size_estimate": "~1 KB per event; ~200 events \u2192 ~200 KB"
  },
  "concurrency": {
    "execution_model": "single-threaded async loop",
    "max_in_flight": 1,
    "idempotency_key": "mapping_cycle_id",
    "timeout_ms": 10000,
    "backpressure": "drop newest",
    "locking": "none"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate suppresses emissions.",
        "contract": "If active, mapping runs but no reports emitted."
      }
    ],
    "emits_to": [
      {
        "bot_id": "disc.opportunityqueue",
        "why": "OpportunityQueue uses event proximity to prioritise opportunities.",
        "contract": "ObservationReport includes hours_to_event and event_id."
      }
    ],
    "sibling": [
      "disc.marketscanner"
    ],
    "external": [
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "Halt cycle; retry next interval."
      },
      {
        "service": "Calendar feed APIs",
        "endpoint": "Various (elections, sports, FRED, earnings)",
        "sla": "best-effort",
        "failure_mode": "Skip affected calendar source; log warning."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "External calendar feed returning crafted event data to create false market mappings"
    ],
    "mitigations": [
      "NLP match score threshold prevents low-confidence mappings from propagating",
      "All outputs are ObservationReports (read-only recommendations); downstream bots independently validate"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com",
      "expected_behaviour": "Cycle halted; STALE_MARKET_DATA logged; no reports emitted",
      "recovery": "Automatic on next cycle."
    },
    {
      "scenario": "ALL_CALENDARS_EMPTY",
      "how_to_inject": "Return empty event lists from all calendar feeds",
      "expected_behaviour": "All markets get CALENDAR_MISMATCH; EventCalendarMapperNoMappings alert fires",
      "recovery": "Automatic when calendar feeds return data."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behaviour": "Mapping runs internally; zero ObservationReports emitted",
      "recovery": "Emissions resume after KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "EventCalendarMapper incidents are typically calendar feed outages or NLP threshold misconfiguration. Bot is read-only; incidents do not affect active positions.",
    "oncall_actions": [
      {
        "alert": "EventCalendarMapperNoMappings",
        "first_action": "Check calendar feed health; verify min_event_relevance threshold has not been raised.",
        "escalate_to": "Intelligence pod lead after 15 minutes."
      },
      {
        "alert": "EventCalendarMapperGammaAPIDown",
        "first_action": "Check Gamma API status page.",
        "escalate_to": "Intelligence pod lead after 10 minutes."
      }
    ],
    "manual_overrides": [
      {
        "name": "disable-calendar",
        "how": "Set enabled_calendars to exclude the failing source",
        "when": "One calendar feed is returning corrupt data."
      }
    ],
    "healthcheck": "GET /internal/health/eventcalendarmapper \u2192 green if Last cycle completed within 2\u00d7 mapping interval; at least one event mapped in last 24h.; red if No cycle completed in 2\u00d7 interval or zero mappings in 24h during active markets."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "NLP match correctly identifies top-5 market categories in test suite",
        "how_measured": "Unit test suite",
        "threshold": "\u22654/5 correct"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Mapping coverage \u226510% of active markets over 48h shadow run",
        "how_measured": "polytraders_disc_eventcalendarmapper_mappings_emitted_total",
        "threshold": "\u226510%"
      }
    ],
    "to_general_live": [
      {
        "gate": "No false-positive mappings detected via human spot-check of 50 samples",
        "how_measured": "Manual review",
        "threshold": "\u22642 false positives"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "retention_class": "30d",
    "cadence": "every-event",
    "sampling_rule": "emit-every",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}