defmodule Polymarket.Schemas.Market do @moduledoc """ A market returned by the Gamma REST API (e.g. `GET /markets/:id`). The Gamma API delivers its keys in `camelCase`; `from_attrs/1` normalises them to the `snake_case` fields below. A handful of fields are delivered as JSON-encoded strings (e.g. `outcomes` arrives as `"[\\"Up\\", \\"Down\\"]"`); `from_attrs/1` decodes these via `Polymarket.Schemas.Coerce`. Field coverage follows the Gamma OpenAPI `Market` schema. The remaining nested relational objects (`events`, `categories`, `imageOptimized`, `iconOptimized`) are intentionally omitted: `GET /markets/:id` does not return them, and `events` is a large, self-referential `Market` <-> `Event` graph. """ use TypedEctoSchema alias Polymarket.JsonUtil alias Polymarket.Schemas.ClobReward alias Polymarket.Schemas.Coerce alias Polymarket.Schemas.FeeSchedule alias Polymarket.Schemas.MarketMetadata alias Polymarket.Schemas.Tag @primary_key false @derive Jason.Encoder typed_embedded_schema do # Identity / description field(:id, :string) field(:question, :string) field(:question_id, :string) field(:condition_id, :string) field(:slug, :string) field(:description, :string) field(:resolution_source, :string) field(:image, :string) field(:icon, :string) field(:market_maker_address, :string) field(:submitted_by, :string) field(:resolved_by, :string) field(:creator, :string) field(:created_by, :integer) field(:updated_by, :integer) field(:category, :string) field(:subcategory, :string) field(:market_type, :string) field(:format_type, :string) field(:market_group, :integer) field(:curation_order, :integer) field(:score, :integer) field(:twitter_card_image, :string) field(:twitter_card_location, :string) field(:twitter_card_last_refreshed, :string) field(:twitter_card_last_validated, :string) field(:sponsor_name, :string) field(:sponsor_image, :string) field(:chart_color, :string) field(:series_color, :string) field(:mailchimp_tag, :string) field(:category_mailchimp_tag, :string) field(:disqus_thread, :string) field(:past_slugs, :string) field(:game_id, :string) field(:team_aid, :string) field(:team_bid, :string) field(:group_item_range, :string) # Scalar / range markets field(:amm_type, :string) field(:fee, :string) field(:denomination_token, :string) field(:lower_bound, :string) field(:upper_bound, :string) field(:lower_bound_date, :string) field(:upper_bound_date, :string) field(:x_axis_value, :string) field(:y_axis_value, :string) # Status flags field(:active, :boolean) field(:closed, :boolean) field(:archived, :boolean) field(:new, :boolean) field(:featured, :boolean) field(:restricted, :boolean) field(:ready, :boolean) field(:funded, :boolean) field(:approved, :boolean) field(:cyom, :boolean) field(:deploying, :boolean) field(:pending_deployment, :boolean) field(:automatically_active, :boolean) field(:automatically_resolved, :boolean) field(:manual_activation, :boolean) field(:accepting_orders, :boolean) field(:enable_order_book, :boolean) field(:clear_book_on_start, :boolean) field(:has_reviewed_dates, :boolean) field(:fees_enabled, :boolean) field(:holding_rewards_enabled, :boolean) field(:rfq_enabled, :boolean) field(:requires_translation, :boolean) field(:pager_duty_notification_enabled, :boolean) field(:neg_risk, :boolean) field(:neg_risk_other, :boolean) field(:show_gmp_series, :boolean) field(:show_gmp_outcome, :boolean) field(:wide_format, :boolean) field(:ready_for_cron, :boolean) field(:comments_enabled, :boolean) field(:notifications_enabled, :boolean) field(:sent_discord, :boolean) field(:fpmm_live, :boolean) # Dates field(:start_date, :utc_datetime_usec) field(:end_date, :utc_datetime_usec) field(:start_date_iso, :date) field(:end_date_iso, :date) field(:created_at, :utc_datetime_usec) field(:updated_at, :utc_datetime_usec) field(:closed_time, :utc_datetime_usec) field(:accepting_orders_timestamp, :utc_datetime_usec) field(:deploying_timestamp, :utc_datetime_usec) field(:event_start_time, :utc_datetime_usec) field(:game_start_time, :utc_datetime_usec) field(:ready_timestamp, :utc_datetime_usec) field(:funded_timestamp, :utc_datetime_usec) field(:scheduled_deployment_timestamp, :utc_datetime_usec) # The spec types both of these as plain strings (no `date-time` format). # `GET /markets` returns ISO timestamps for `uma_end_date`, but the events # endpoint returns free-form dates (e.g. "April 25, 2022") for older markets, # so it is kept a string. The `*_iso` fields, by contrast, the fixtures prove # are dates. field(:uma_end_date, :string) field(:uma_end_date_iso, :string) # Pricing / order book state field(:best_bid, :float) field(:best_ask, :float) field(:spread, :float) field(:last_trade_price, :float) field(:one_hour_price_change, :float) field(:one_day_price_change, :float) field(:one_week_price_change, :float) field(:one_month_price_change, :float) field(:one_year_price_change, :float) field(:competitive, :float) field(:line, :float) # Order configuration field(:order_min_size, :integer) field(:order_price_min_tick_size, :float) field(:combo_status, :string) field(:custom_liveness, :integer) field(:seconds_delay, :integer) field(:group_item_threshold, :string) field(:group_item_title, :string) field(:sports_market_type, :string) # Liquidity / volume (the `*_num`/`*_clob` variants are numeric; the bare # `liquidity`/`volume` fields arrive as strings and are kept verbatim). field(:liquidity, :string) field(:liquidity_num, :float) field(:liquidity_amm, :float) field(:liquidity_clob, :float) field(:volume, :string) field(:volume_num, :float) field(:volume_clob, :float) field(:volume24hr, :float) field(:volume24hr_clob, :float) field(:volume1wk, :float) field(:volume1wk_clob, :float) field(:volume1mo, :float) field(:volume1mo_clob, :float) field(:volume1yr, :float) field(:volume1yr_clob, :float) field(:volume_amm, :float) field(:volume24hr_amm, :float) field(:volume1wk_amm, :float) field(:volume1mo_amm, :float) field(:volume1yr_amm, :float) # Fees / rewards field(:fee_type, :string) field(:maker_base_fee, :integer) field(:taker_base_fee, :integer) field(:maker_rebates_fee_share_bps, :integer) field(:rewards_min_size, :integer) field(:rewards_max_spread, :float) # UMA resolution field(:uma_bond, :string) field(:uma_reward, :string) field(:uma_resolution_status, :string) field(:neg_risk_request_id, :string) field(:neg_risk_market_id, :string) # Array fields. `outcomes`/`outcome_prices`/`clob_token_ids`/ # `uma_resolution_statuses` arrive as JSON-encoded strings; `position_ids` # arrives as a real array. field(:outcomes, {:array, :string}) field(:short_outcomes, {:array, :string}) field(:outcome_prices, {:array, :float}) field(:clob_token_ids, {:array, :string}) field(:uma_resolution_statuses, {:array, :string}) field(:position_ids, {:array, :string}) embeds_one(:fee_schedule, FeeSchedule) embeds_one(:market_metadata, MarketMetadata) embeds_many(:tags, Tag) embeds_many(:clob_rewards, ClobReward) end @required [:id, :condition_id, :slug] @doc """ Builds a `Market` from the raw (JSON-decoded) Gamma attributes. Keys may be in `camelCase` (atom or string); they are normalised to the `snake_case` fields and each value is coerced to its field type. Returns `{:error, {:missing_fields, fields}}` when a required field is absent. """ @spec from_attrs(map()) :: {:ok, t()} | {:error, {:missing_fields, [atom()]}} def from_attrs(attrs) do attrs = JsonUtil.snake_case_keys(attrs) %__MODULE__{ id: attrs["id"], question: attrs["question"], question_id: attrs["question_id"], condition_id: attrs["condition_id"], slug: attrs["slug"], description: attrs["description"], resolution_source: attrs["resolution_source"], image: attrs["image"], icon: attrs["icon"], market_maker_address: attrs["market_maker_address"], submitted_by: attrs["submitted_by"], resolved_by: attrs["resolved_by"], creator: attrs["creator"], created_by: Coerce.to_int(attrs["created_by"]), updated_by: Coerce.to_int(attrs["updated_by"]), category: attrs["category"], subcategory: attrs["subcategory"], market_type: attrs["market_type"], format_type: attrs["format_type"], market_group: Coerce.to_int(attrs["market_group"]), curation_order: Coerce.to_int(attrs["curation_order"]), score: Coerce.to_int(attrs["score"]), twitter_card_image: attrs["twitter_card_image"], twitter_card_location: attrs["twitter_card_location"], twitter_card_last_refreshed: attrs["twitter_card_last_refreshed"], twitter_card_last_validated: attrs["twitter_card_last_validated"], sponsor_name: attrs["sponsor_name"], sponsor_image: attrs["sponsor_image"], chart_color: attrs["chart_color"], series_color: attrs["series_color"], mailchimp_tag: attrs["mailchimp_tag"], category_mailchimp_tag: attrs["category_mailchimp_tag"], disqus_thread: attrs["disqus_thread"], past_slugs: attrs["past_slugs"], game_id: attrs["game_id"], team_aid: attrs["team_aid"], team_bid: attrs["team_bid"], group_item_range: attrs["group_item_range"], amm_type: attrs["amm_type"], fee: attrs["fee"], denomination_token: attrs["denomination_token"], lower_bound: attrs["lower_bound"], upper_bound: attrs["upper_bound"], lower_bound_date: attrs["lower_bound_date"], upper_bound_date: attrs["upper_bound_date"], x_axis_value: attrs["x_axis_value"], y_axis_value: attrs["y_axis_value"], active: attrs["active"], closed: attrs["closed"], archived: attrs["archived"], new: attrs["new"], featured: attrs["featured"], restricted: attrs["restricted"], ready: attrs["ready"], funded: attrs["funded"], approved: attrs["approved"], cyom: attrs["cyom"], deploying: attrs["deploying"], pending_deployment: attrs["pending_deployment"], automatically_active: attrs["automatically_active"], automatically_resolved: attrs["automatically_resolved"], manual_activation: attrs["manual_activation"], accepting_orders: attrs["accepting_orders"], enable_order_book: attrs["enable_order_book"], clear_book_on_start: attrs["clear_book_on_start"], has_reviewed_dates: attrs["has_reviewed_dates"], fees_enabled: attrs["fees_enabled"], holding_rewards_enabled: attrs["holding_rewards_enabled"], rfq_enabled: attrs["rfq_enabled"], requires_translation: attrs["requires_translation"], pager_duty_notification_enabled: attrs["pager_duty_notification_enabled"], neg_risk: attrs["neg_risk"], neg_risk_other: attrs["neg_risk_other"], show_gmp_series: attrs["show_gmp_series"], show_gmp_outcome: attrs["show_gmp_outcome"], wide_format: attrs["wide_format"], ready_for_cron: attrs["ready_for_cron"], comments_enabled: attrs["comments_enabled"], notifications_enabled: attrs["notifications_enabled"], sent_discord: attrs["sent_discord"], fpmm_live: attrs["fpmm_live"], start_date: Coerce.to_datetime(attrs["start_date"]), end_date: Coerce.to_datetime(attrs["end_date"]), start_date_iso: Coerce.to_date(attrs["start_date_iso"]), end_date_iso: Coerce.to_date(attrs["end_date_iso"]), created_at: Coerce.to_datetime(attrs["created_at"]), updated_at: Coerce.to_datetime(attrs["updated_at"]), closed_time: Coerce.to_datetime(attrs["closed_time"]), accepting_orders_timestamp: Coerce.to_datetime(attrs["accepting_orders_timestamp"]), deploying_timestamp: Coerce.to_datetime(attrs["deploying_timestamp"]), event_start_time: Coerce.to_datetime(attrs["event_start_time"]), game_start_time: Coerce.to_datetime(attrs["game_start_time"]), ready_timestamp: Coerce.to_datetime(attrs["ready_timestamp"]), funded_timestamp: Coerce.to_datetime(attrs["funded_timestamp"]), scheduled_deployment_timestamp: Coerce.to_datetime(attrs["scheduled_deployment_timestamp"]), uma_end_date: attrs["uma_end_date"], uma_end_date_iso: attrs["uma_end_date_iso"], best_bid: Coerce.to_float(attrs["best_bid"]), best_ask: Coerce.to_float(attrs["best_ask"]), spread: Coerce.to_float(attrs["spread"]), last_trade_price: Coerce.to_float(attrs["last_trade_price"]), one_hour_price_change: Coerce.to_float(attrs["one_hour_price_change"]), one_day_price_change: Coerce.to_float(attrs["one_day_price_change"]), one_week_price_change: Coerce.to_float(attrs["one_week_price_change"]), one_month_price_change: Coerce.to_float(attrs["one_month_price_change"]), one_year_price_change: Coerce.to_float(attrs["one_year_price_change"]), competitive: Coerce.to_float(attrs["competitive"]), line: Coerce.to_float(attrs["line"]), order_min_size: Coerce.to_int(attrs["order_min_size"]), order_price_min_tick_size: Coerce.to_float(attrs["order_price_min_tick_size"]), combo_status: attrs["combo_status"], custom_liveness: Coerce.to_int(attrs["custom_liveness"]), seconds_delay: Coerce.to_int(attrs["seconds_delay"]), group_item_threshold: attrs["group_item_threshold"], group_item_title: attrs["group_item_title"], sports_market_type: attrs["sports_market_type"], liquidity: attrs["liquidity"], liquidity_num: Coerce.to_float(attrs["liquidity_num"]), liquidity_amm: Coerce.to_float(attrs["liquidity_amm"]), liquidity_clob: Coerce.to_float(attrs["liquidity_clob"]), volume: attrs["volume"], volume_num: Coerce.to_float(attrs["volume_num"]), volume_clob: Coerce.to_float(attrs["volume_clob"]), volume24hr: Coerce.to_float(attrs["volume24hr"]), volume24hr_clob: Coerce.to_float(attrs["volume24hr_clob"]), volume1wk: Coerce.to_float(attrs["volume1wk"]), volume1wk_clob: Coerce.to_float(attrs["volume1wk_clob"]), volume1mo: Coerce.to_float(attrs["volume1mo"]), volume1mo_clob: Coerce.to_float(attrs["volume1mo_clob"]), volume1yr: Coerce.to_float(attrs["volume1yr"]), volume1yr_clob: Coerce.to_float(attrs["volume1yr_clob"]), volume_amm: Coerce.to_float(attrs["volume_amm"]), volume24hr_amm: Coerce.to_float(attrs["volume24hr_amm"]), volume1wk_amm: Coerce.to_float(attrs["volume1wk_amm"]), volume1mo_amm: Coerce.to_float(attrs["volume1mo_amm"]), volume1yr_amm: Coerce.to_float(attrs["volume1yr_amm"]), fee_type: attrs["fee_type"], maker_base_fee: Coerce.to_int(attrs["maker_base_fee"]), taker_base_fee: Coerce.to_int(attrs["taker_base_fee"]), maker_rebates_fee_share_bps: Coerce.to_int(attrs["maker_rebates_fee_share_bps"]), rewards_min_size: Coerce.to_int(attrs["rewards_min_size"]), rewards_max_spread: Coerce.to_float(attrs["rewards_max_spread"]), uma_bond: attrs["uma_bond"], uma_reward: attrs["uma_reward"], uma_resolution_status: attrs["uma_resolution_status"], neg_risk_request_id: attrs["neg_risk_request_id"], neg_risk_market_id: attrs["neg_risk_market_id"], outcomes: Coerce.to_string_list(attrs["outcomes"]), short_outcomes: Coerce.to_string_list(attrs["short_outcomes"]), outcome_prices: Coerce.to_float_list(attrs["outcome_prices"]), clob_token_ids: Coerce.to_string_list(attrs["clob_token_ids"]), uma_resolution_statuses: Coerce.to_string_list(attrs["uma_resolution_statuses"]), position_ids: attrs["position_ids"], fee_schedule: Coerce.embed(FeeSchedule, attrs["fee_schedule"]), market_metadata: Coerce.embed(MarketMetadata, attrs["market_metadata"]), tags: Coerce.embed_list(Tag, attrs["tags"]), clob_rewards: Coerce.embed_list(ClobReward, attrs["clob_rewards"]) } |> Coerce.require_fields(@required) end end