{
  "schema_version": "1.0.0",
  "bot_id": "4.13",
  "bot_name": "LiquidityForecastModel",
  "slug": "liquidityforecastmodel",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "",
    "runs_after": "",
    "applies_to": "",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Predict near-term book depth and trade pacing per market for sizing decisions.",
  "why_it_matters": [
    {
      "failure": "Strategies size on stale liquidity",
      "consequence": "Sizing decisions made from current top-of-book ignore that depth can collapse seconds before fill. A forecast \u2014 even a coarse one \u2014 gives strategies a concrete number to clamp size against before submitting an OrderIntent.",
      "worked_example": {
        "setup": "Market 0xc4d9 shows 8,500 pUSD of asks within 2 ticks of mid. A strategy plans to take 6,000 pUSD of inventory based on that visible depth.",
        "without_bot": "Between the strategy's read and the OrderIntent reaching CTFExchangeV2, three other takers have already cleared the visible asks. The strategy's order walks the book and fills the last 2,000 pUSD at 30 bps worse than planned.",
        "with_bot": "LiquidityForecastModel estimates 5-second forward depth at 3,200 pUSD. PortfolioGuard clamps the order to 2,500 pUSD on that input, the strategy fills at the planned price, and the remaining size is re-evaluated next tick."
      }
    },
    {
      "failure": "Execution router under-quotes its own fill probability",
      "consequence": "Without forward-looking depth, smartrouter can only react to the book it sees. With a forecast, it can prefer a passive ladder that is expected to refill, instead of crossing into thin liquidity that disappears mid-fill."
    },
    {
      "failure": "Risk caps lag the actual market",
      "consequence": "Static per-market notional caps assume yesterday's depth. A liquidity forecast lets Risk widen or tighten caps in line with predicted near-term depth, reducing both missed opportunities in deep books and oversize entries in thin ones."
    },
    {
      "failure": "Operators have no leading indicator before a liquidity collapse",
      "consequence": "When depth dries up before a known event (a debate, a game start), having no model means the team only sees the collapse on the chart after it happens. A forecast emits a warning the operator can act on."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB V2 order book snapshot (bids and asks) per condition_id",
      "source": "clob_public",
      "required": true,
      "use": "Compute current bid/ask depth in pUSD for forecast baseline."
    },
    {
      "input": "Historical fill rate for each condition_id",
      "source": "data",
      "required": true,
      "use": "Weight forecast using observed fill rate to discount unfilled book depth."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all forecast emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "forecast_horizon_min \u00b7 int",
    "min_history_h \u00b7 int",
    "publish_to \u00b7 list",
    "fallback_to_realised \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "forecast_horizon_s",
      "default": 3600,
      "warning": 7200,
      "hard": 86400,
      "controls": "Seconds ahead to forecast liquidity availability.",
      "why_default_matters": "3600 s (1 h) matches typical strategy hold horizon without requiring multi-day extrapolation.",
      "threshold_logic": [
        {
          "condition": "horizon <= 3600 s",
          "action": "Normal"
        },
        {
          "condition": "3600\u20137200 s",
          "action": "WARN \u2014 forecast uncertainty increases with horizon"
        },
        {
          "condition": "> 86400 s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.forecast_horizon_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Liquidity forecasts are computed for a configurable time horizon ahead."
    },
    {
      "name": "min_liquidity_pusd",
      "default": 1000,
      "warning": 500,
      "hard": 100,
      "controls": "Minimum forecasted pUSD liquidity required to avoid a low-liquidity signal.",
      "why_default_matters": "1000 pUSD ensures strategies have enough depth to enter and exit without significant slippage.",
      "threshold_logic": [
        {
          "condition": "forecast >= 1000 pUSD",
          "action": "Normal"
        },
        {
          "condition": "100\u20131000 pUSD",
          "action": "WARN \u2014 LIQUIDITYFORECAST_LOW_FORECAST; strategies reduce size"
        },
        {
          "condition": "< 100 pUSD",
          "action": "Reject \u2014 emit low_forecast; strategies avoid entry"
        }
      ],
      "dev_check": "if (forecast_pusd < p.min_liquidity_pusd.hard) emit('LIQUIDITYFORECAST_LOW_FORECAST');",
      "user_facing": "A minimum liquidity level is required before strategies can enter a market."
    }
  ],
  "default_config": {
    "bot_id": "intel.liquidityforecastmodel",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "forecast_horizon_s": 3600,
      "min_liquidity_pusd": 1000
    },
    "locked": {
      "forecast_horizon_s": {
        "max": 86400
      },
      "min_liquidity_pusd": {
        "min": 100
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_lfm_0xf1a2_1746703000000",
    "trace_id": "trc_0xbeef0102030405060714",
    "bot_id": "intel.liquidityforecastmodel",
    "kind": "ObservationReport",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "current_bid_depth_pusd": 5350,
    "current_ask_depth_pusd": 6500,
    "forecast_pusd": 4800,
    "horizon_s": 3600,
    "low_forecast": false,
    "emitted_at_ms": 1746703005000
  },
  "developer_log": {
    "bot_id": "intel.liquidityforecastmodel",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "current_bid_depth_pusd": 5350,
    "current_ask_depth_pusd": 6500,
    "forecast_pusd": 4800,
    "fill_rate": 0.72,
    "low_forecast": false,
    "killswitch_active": false
  },
  "user_explanations": [
    {
      "situation": "Strategy reduced size due to low-liquidity forecast",
      "message": "The model projects that available market depth will fall below the minimum threshold over the next hour. Position size was reduced to limit potential slippage."
    },
    {
      "situation": "Liquidity forecast indicates adequate depth",
      "message": "The market is projected to have sufficient depth over the strategy's hold horizon. No size adjustment was triggered."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "CLOB API outage prevents fresh order book snapshots, causing LiquidityForecastModel to serve stale forecasts that may overstate available depth, leading strategies to size into low-liquidity markets.",
    "false_positive_risk": "Large spoof orders inflating apparent book depth cause the forecast to overestimate available liquidity and suppress low-forecast signals.",
    "false_negative_risk": "Rapid order withdrawal post-fill leaves the forecast below actual liquidity, causing unnecessary LIQUIDITYFORECAST_LOW_FORECAST signals on adequate markets.",
    "safe_fallback": "If CLOB API is unavailable for > staleness_threshold_s, emit STALE_DATA WARN and suspend new forecast emissions. Do not serve stale forecasts older than staleness_threshold_s to downstream consumers.",
    "required_dependencies": [
      "Polymarket CLOB V2 public API",
      "Polymarket Data API for fill rate",
      "KillSwitch active flag",
      "Redis for forecast state"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Adequate forecast emits ObservationReport with low_forecast=false",
        "setup": "bid_depth=5350 pUSD, ask_depth=6500 pUSD, fill_rate=0.72, horizon=3600 s, min_liquidity=1000 pUSD",
        "expected": "ObservationReport emitted with forecast_pusd=4800, low_forecast=false"
      },
      {
        "test": "Low forecast emits LIQUIDITYFORECAST_LOW_FORECAST warning",
        "setup": "bid_depth=50 pUSD, ask_depth=30 pUSD, min_liquidity_pusd hard=100 pUSD",
        "expected": "ObservationReport emitted with low_forecast=true, LIQUIDITYFORECAST_LOW_FORECAST WARN"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; forecast computed",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Forecast consumed by LiquidityGuard for pre-trade risk check",
        "expected": "LiquidityGuard receives forecast_pusd and horizon_s from ObservationReport"
      },
      {
        "test": "CLOB API down: STALE_DATA emitted; no new forecasts served",
        "expected": "STALE_DATA WARN; downstream consumers receive no new forecast ObservationReports"
      }
    ],
    "property": [
      {
        "property": "LiquidityForecastModel never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Predict near-term book depth and trade pacing per market for sizing decisions.",
  "legacy_pm_signals": [
    "Recent book depth, trade frequency, and cancel rate",
    "Time-of-day, day-of-week, and event-proximity features",
    "Listing age and quality-rank features"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "data",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-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": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Reads CLOB V2 order book snapshots to forecast pUSD liquidity depth over a configurable horizon. Read-only. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION forecastLiquidity(condition_id, horizon_s):\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch order book snapshot\n  book = FETCH clob_public.GET('/book?token_id=' + condition_id)\n  IF book IS NULL:\n    EMIT WARN 'STALE_DATA'\n    RETURN\n\n  // 2. Compute current depth\n  bid_depth_pusd = sum(l.size * l.price for l in book.bids)\n  ask_depth_pusd = sum(l.size for l in book.asks)\n\n  // 3. Fetch historical fill rate from Data API\n  fill_rate = FETCH data.GET('/fills?condition_id=' + condition_id + '&window=3600')\n\n  // 4. Forecast using exponential decay model\n  forecast_pusd = forecastDepth(bid_depth_pusd, ask_depth_pusd, fill_rate, horizon_s)\n\n  // 5. Check against min_liquidity_pusd threshold\n  IF forecast_pusd < min_liquidity_pusd:\n    EMIT WARN 'LIQUIDITYFORECAST_LOW_FORECAST'\n\n  // 6. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    condition_id: condition_id,\n    current_bid_depth_pusd: bid_depth_pusd,\n    current_ask_depth_pusd: ask_depth_pusd,\n    forecast_pusd: forecast_pusd,\n    horizon_s: horizon_s,\n    emitted_at_ms: now_ms()\n  }",
    "sdk_calls": [
      "clob_public.GET('/book?token_id=<condition_id>')",
      "data.GET('/fills?condition_id=<condition_id>&window=3600')",
      "internal.killswitch.status"
    ],
    "complexity": "O(B) per condition_id where B = order book depth levels"
  },
  "wire_examples": {
    "input": {
      "label": "CLOB V2 order book snapshot for liquidity forecast",
      "source": "clob_public",
      "payload": {
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "bids": [
          {
            "price": "0.67",
            "size": "5000"
          },
          {
            "price": "0.66",
            "size": "3000"
          }
        ],
        "asks": [
          {
            "price": "0.69",
            "size": "4500"
          },
          {
            "price": "0.70",
            "size": "2000"
          }
        ],
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 liquidity forecast",
      "payload": {
        "report_id": "rep_lfm_0xf1a2_1746703000000",
        "trace_id": "trc_0xbeef0102030405060714",
        "bot_id": "intel.liquidityforecastmodel",
        "kind": "ObservationReport",
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "current_bid_depth_pusd": 5350,
        "current_ask_depth_pusd": 6500,
        "forecast_pusd": 4800,
        "horizon_s": 3600,
        "emitted_at_ms": 1746703005000
      }
    }
  },
  "reason_codes": [
    {
      "code": "LIQUIDITYFORECAST_LOW_FORECAST",
      "severity": "WARN",
      "meaning": "Forecasted liquidity over the horizon falls below min_liquidity_pusd threshold.",
      "action": "Emit ObservationReport with low_forecast=true; downstream strategies may reduce size.",
      "user_message": "Projected market liquidity is below the minimum threshold for this horizon."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "CLOB order book snapshot is unavailable or older than staleness_threshold_s.",
      "action": "Skip emission; retry on next poll.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all LiquidityForecastModel emissions suppressed.",
      "action": "Continue computing forecasts but suppress ObservationReport emissions.",
      "user_message": "Liquidity forecast signals paused while trading is suspended system-wide."
    },
    {
      "code": "LIQUIDITYFORECAST_BOOK_EMPTY",
      "severity": "WARN",
      "meaning": "Order book has no bids or asks \u2014 market may be in a halted or pre-open state.",
      "action": "Emit ObservationReport with book_empty=true; strategies avoid entry on this market.",
      "user_message": "Market order book is currently empty."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_liquidityforecastmodel_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "condition_id"
        ],
        "meaning": "ObservationReports emitted per tracked market."
      },
      {
        "name": "polytraders_intel_liquidityforecastmodel_forecast_pusd",
        "type": "gauge",
        "unit": "pUSD",
        "labels": [
          "condition_id"
        ],
        "meaning": "Latest liquidity forecast in pUSD for each tracked market."
      },
      {
        "name": "polytraders_intel_liquidityforecastmodel_low_forecast_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Total low-liquidity forecast events emitted."
      }
    ],
    "alerts": [
      {
        "name": "LiquidityForecastModelLowForecast",
        "condition": "polytraders_intel_liquidityforecastmodel_forecast_pusd < 1000",
        "severity": "warn",
        "runbook": "#runbook-liquidityforecastmodel-low-forecast"
      },
      {
        "name": "LiquidityForecastModelStale",
        "condition": "rate(polytraders_intel_liquidityforecastmodel_observations_emitted_total[15m]) == 0",
        "severity": "warn",
        "runbook": "#runbook-liquidityforecastmodel-stale"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / LiquidityForecastModel forecast_pusd per market"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Per condition_id: last_forecast_pusd, last_bid_depth, last_ask_depth, last_computed_at_ms, fill_rate_cache.",
    "ttl": "Per-market state expires after 2 h of no activity",
    "recovery": "On cold start, re-compute on first poll cycle; no historical backfill required.",
    "size_estimate": "~2 KB per tracked market"
  },
  "concurrency": {
    "execution_model": "async per-market poll loop",
    "max_in_flight": 25,
    "idempotency_key": "condition_id + snapshot_ts_ms",
    "timeout_ms": 10000,
    "backpressure": "drop-after-buffer \u2014 skip condition_ids that have not drained within 2x poll interval",
    "locking": "Redis SETNX on condition_id + snapshot_ts to prevent duplicate computations"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      },
      {
        "bot_id": "risk.liquidity_guard",
        "why": "Provide forward liquidity signals for LiquidityGuard risk decisions."
      }
    ],
    "emits_to": [
      {
        "bot_id": "risk.liquidity_guard",
        "what": "ObservationReport with forecast_pusd and horizon_s for pre-trade liquidity risk checks"
      }
    ],
    "sibling": [
      "intel.onchainwatcher"
    ],
    "external": [
      {
        "service": "Polymarket CLOB V2 (public book)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.9% / 200 ms p99",
        "fallback": "Emit STALE_DATA if book unavailable; skip forecast for this cycle"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Adversary places large spoofed orders to inflate apparent order book depth and mislead forecast",
      "Rapid order cancellations during forecast window cause overestimate of available liquidity"
    ],
    "mitigations": [
      "Forecast uses fill_rate weighting to discount unfilled book depth that never converts to trades",
      "Forecasts are informational only \u2014 LiquidityGuard applies independent depth checks at execution time"
    ]
  },
  "failure_injection": [
    {
      "scenario": "CLOB_API_DOWN",
      "how_to_inject": "Block CLOB public API for 10 min",
      "expected_behaviour": "STALE_DATA WARN; no forecasts; LiquidityForecastModelStale alert fires",
      "recovery": "Automatic on API recovery; next poll cycle resumes normally"
    },
    {
      "scenario": "EMPTY_ORDER_BOOK",
      "how_to_inject": "Return empty bids and asks for a test condition_id",
      "expected_behaviour": "LIQUIDITYFORECAST_BOOK_EMPTY WARN; ObservationReport emitted with book_empty=true",
      "recovery": "Automatic when orders return to book"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active forecasting",
      "expected_behaviour": "Forecasts computed internally; emissions suppressed; KILL_SWITCH_ACTIVE logged",
      "recovery": "Automatic on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "LiquidityForecastModel incidents are typically CLOB API outages. Low forecasts are signals, not errors. Extended outages will leave strategies without forward liquidity data.",
    "oncall_actions": [
      {
        "alert": "LiquidityForecastModelLowForecast",
        "first_step": "Verify live order book depth on Polymarket.com for the affected market. If genuine, no action needed \u2014 downstream strategies will reduce size automatically.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if multiple markets simultaneously below threshold"
      },
      {
        "alert": "LiquidityForecastModelStale",
        "first_step": "Check CLOB API health and last_computed_at_ms for affected markets.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if stale > 15 min"
      }
    ],
    "manual_overrides": [
      {
        "command": "force_recompute",
        "effect": "POST /internal/liquidityforecastmodel/recompute?condition_id=<id> to trigger immediate recompute \u2014 After CLOB API recovery or suspected stale forecast"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/liquidityforecastmodel | Green: Last forecast < 5 min ago AND Redis reachable AND CLOB API returning 200 | Red: No forecast for > 15 min OR Redis unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for low-forecast gate, empty book detection, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Forecast computation completes in < 5 s p99 over 24 h on staging",
        "how_measured": "Integration test",
        "threshold": "p99 < 5 s"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero missed forecasts during 14-day soak with live CLOB V2 API",
        "how_measured": "LiquidityForecastModelStale alert audit",
        "threshold": "0 stale alerts"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-event",
    "retention_class": "30d",
    "retention_notes": "Full fidelity for 30 d; rolled-up summary retained for 1 y",
    "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"
  }
}