Skip to content
Merged
Show file tree
Hide file tree
Changes from 78 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
32b0e70
Release 1.176.0
cjdsellers Jul 31, 2023
e658ba6
Release 1.177.0
cjdsellers Aug 25, 2023
ef75e5f
Release 1.178.0
cjdsellers Sep 2, 2023
ef4d72e
Release 1.179.0
cjdsellers Oct 22, 2023
622014e
Release 1.180.0
cjdsellers Nov 3, 2023
63c191b
Release 1.181.0
cjdsellers Dec 2, 2023
f1b643d
Release 1.182.0
cjdsellers Dec 23, 2023
178d621
Release 1.183.0
cjdsellers Jan 12, 2024
a79c2bb
Release 1.184.0
cjdsellers Jan 22, 2024
9d09e96
Release 1.185.0
cjdsellers Jan 26, 2024
8aae236
Release 1.186.0
cjdsellers Feb 2, 2024
61debf2
Release 1.187.0
cjdsellers Feb 9, 2024
92aee66
Release 1.188.0
cjdsellers Feb 25, 2024
5e12bdf
Release 1.189.0
cjdsellers Mar 15, 2024
5f31d43
Release 1.190.0
cjdsellers Mar 22, 2024
0eb0b56
Release 1.191.0
cjdsellers Apr 20, 2024
3db0827
Release 1.192.0
cjdsellers May 18, 2024
c059eb8
Release 1.193.0
cjdsellers May 24, 2024
86acc83
Release 1.194.0
cjdsellers May 31, 2024
1364ac3
Release 1.195.0
cjdsellers Jun 17, 2024
e40eb3d
Release 1.196.0
cjdsellers Jul 5, 2024
c760fb0
Release 1.197.0
cjdsellers Aug 2, 2024
84442a2
Release 1.198.0
cjdsellers Aug 9, 2024
7a496b4
Release 1.199.0
cjdsellers Aug 19, 2024
5823764
Release 1.200.0
cjdsellers Sep 7, 2024
eed7861
Release 1.201.0
cjdsellers Sep 9, 2024
5089347
Release 1.201.0
cjdsellers Sep 9, 2024
fad3968
Release 1.202.0
cjdsellers Sep 27, 2024
928f5a7
Release 1.203.0
cjdsellers Oct 5, 2024
3fd212d
Release 1.204.0
cjdsellers Oct 22, 2024
59ca217
Release 1.205.0
cjdsellers Nov 3, 2024
230b42f
Release 1.206.0
cjdsellers Nov 17, 2024
02a9ef1
Release 1.207.0
cjdsellers Nov 29, 2024
845ebea
Release 1.208.0
cjdsellers Dec 15, 2024
290bd8e
Release 1.209.0
cjdsellers Dec 25, 2024
fa0f1c0
Release 1.210.0
cjdsellers Jan 10, 2025
014098b
Release 1.211.0
cjdsellers Feb 9, 2025
c63653f
Release 1.211.0
cjdsellers Feb 9, 2025
8ca4a13
Release 1.212.0
cjdsellers Mar 10, 2025
c68468b
Release 1.212.0
cjdsellers Mar 11, 2025
e317039
Release 1.213.0
cjdsellers Mar 16, 2025
e4fff98
Release 1.214.0
cjdsellers Mar 28, 2025
4a95295
Release 1.215.0
cjdsellers Apr 4, 2025
bdc275e
Release 1.216.0
cjdsellers Apr 13, 2025
e8d6677
Fix CI build workflow for release
cjdsellers Apr 13, 2025
e479ab4
Release 1.217.0
cjdsellers Apr 30, 2025
f8f2122
Release 1.218.0
cjdsellers May 31, 2025
e9f3471
Add high-precision settings
odobias Jun 4, 2025
62c1f34
Fix the dependencies format
odobias Jun 4, 2025
44ac6ab
Fix the dependencies format
odobias Jun 4, 2025
cab4f67
Fix the dependencies format, again
odobias Jun 4, 2025
c56b4a0
Remove high-precision feature
DeirhX Jun 4, 2025
1a7d2dd
Try to find a good home for the native cpu flag
DeirhX Jun 4, 2025
3adbdaa
Merge branch 'develop' of https://github.com/nautechsystems/nautilus_…
DeirhX Jun 9, 2025
fd0dd74
Merge with nautech develop
DeirhX Jun 24, 2025
fc33c90
When venue_id can be obtained from cache, use it for the query instea…
DeirhX Aug 13, 2025
e5da382
Merge branch 'develop' of https://github.com/nautechsystems/nautilus_…
DeirhX Aug 15, 2025
66c8f9a
Merge branch 'develop' of https://github.com/DeirhX/nautilus_trader i…
DeirhX Aug 29, 2025
1a68ec4
Merge branch 'develop' of https://github.com/nautechsystems/nautilus_…
DeirhX Aug 29, 2025
f92220e
Merge branch 'develop' of https://github.com/nautechsystems/nautilus_…
DeirhX Sep 21, 2025
04dcc19
Merge with develop and remove native CPU
DeirhX Oct 28, 2025
94c7649
Optionally use Gamma API to fetch a large number of markets
DeirhX Oct 28, 2025
dc11142
Add more data that was present in clob api
DeirhX Nov 2, 2025
2d6d8b6
Normalize as much Gamma API data to CLOB format
DeirhX Nov 2, 2025
44823df
Don't iterate all, just the condition ids we need
DeirhX Nov 2, 2025
196e35c
Formalize the condition_id limit after measuring it is indeed 100
DeirhX Nov 2, 2025
2db77b6
Merge branch 'develop' of https://github.com/nautechsystems/nautilus_…
DeirhX Nov 2, 2025
13e9c5a
Formatting adjustments
DeirhX Nov 2, 2025
a034ef2
Formatting fix
DeirhX Nov 2, 2025
19d8eda
Formatting fix
DeirhX Nov 2, 2025
69e218a
Styling improvements
DeirhX Nov 2, 2025
8b7ff16
Docformatted
DeirhX Nov 2, 2025
5aab08b
Style fixes
DeirhX Nov 2, 2025
aa5c3a5
Remove proprietary logger
DeirhX Nov 3, 2025
9eff8dc
Adjust unit tests for gamma markets API
DeirhX Nov 4, 2025
a43dc5f
Better documentation
DeirhX Nov 4, 2025
b2f7637
Trimmed whitespace
DeirhX Nov 4, 2025
b7ecf9b
Merged with develop
DeirhX Nov 4, 2025
ea10488
Made _load_ids_using_gamma_markets async
DeirhX Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nautilus_trader/adapters/databento/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(
super().__init__(config=config)

self._clock = clock
self._config = config
self._config = config or InstrumentProviderConfig()
self._live_api_key = live_api_key or http_client.key
self._live_gateway = live_gateway

Expand Down
293 changes: 293 additions & 0 deletions nautilus_trader/adapters/polymarket/common/gamma_markets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# -------------------------------------------------------------------------------------------------
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
# https://nautechsystems.io
#
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -------------------------------------------------------------------------------------------------
"""
Thin Gamma Markets API client utilities for Polymarket.

Provides functions to fetch markets using server-side filters, returning
raw market dictionaries ready for further client-side filtering.

References
----------
- Gamma Get Markets docs: https://docs.polymarket.com/developers/gamma-markets-api/get-markets

"""

from __future__ import annotations

import os
from collections.abc import Generator
from typing import Any

import requests


DEFAULT_GAMMA_BASE_URL = os.getenv("GAMMA_API_URL", "https://gamma-api.polymarket.com")


def _normalize_base_url(base_url: str | None) -> str:
url = base_url or DEFAULT_GAMMA_BASE_URL
return url[:-1] if url.endswith("/") else url


def build_markets_query(filters: dict[str, Any] | None = None) -> dict[str, Any]:
"""
Build query params for Gamma Get Markets from a generic filter dict.

Supported keys (passed through if present):
- active, archived, closed, limit, offset, order, ascending, id, slug,
clob_token_ids, condition_ids,
liquidity_num_min, liquidity_num_max,
volume_num_min, volume_num_max,
start_date_min, start_date_max,
end_date_min, end_date_max,
tag_id, related_tags

Special handling:
- is_active=True implies active=true, archived=false, closed=false
- next_cursor: will be added separately by the fetch function

"""
params: dict[str, Any] = {}
if not filters:
return params

if filters.get("is_active") is True:
params["active"] = "true"
params["archived"] = "false"
params["closed"] = "false"

passthrough_keys = (
"active",
"archived",
"closed",
"limit",
"offset",
"order",
"ascending",
"id",
"slug",
"clob_token_ids",
"condition_ids",
"liquidity_num_min",
"liquidity_num_max",
"volume_num_min",
"volume_num_max",
"start_date_min",
"start_date_max",
"end_date_min",
"end_date_max",
"tag_id",
"related_tags",
)
for key in passthrough_keys:
if key in filters and filters[key] is not None:
params[key] = filters[key]

return params


def _request_markets_page(
session: requests.Session,
base_url: str,
params: dict[str, Any],
offset: int,
limit: int,
timeout: float,
) -> list[dict[str, Any]]:
"""
Fetch a single page of markets using limit/offset pagination.

Returns a list of market dicts.

"""
url = f"{base_url}/markets"
effective_params = dict(params)
effective_params["limit"] = limit
effective_params["offset"] = offset

resp = session.get(url, params=effective_params, timeout=timeout)
if resp.status_code != 200:
raise RuntimeError(f"Gamma Get Markets failed: {resp.status_code} for url {url} with params {effective_params} and body {resp.text}")

data = resp.json()
if isinstance(data, list):
return data
if isinstance(data, dict) and "data" in data:
return data.get("data", []) or []

raise RuntimeError("Unrecognized response schema from Gamma Get Markets")


def iter_markets(
filters: dict[str, Any] | None = None,
base_url: str | None = None,
timeout: float = 10.0,
) -> Generator[dict[str, Any], None, None]:
"""
Iterate markets that pass server-side filters, yielding raw market dicts.
"""
base = _normalize_base_url(base_url)
params = build_markets_query(filters)
limit = int(filters.get("limit", 500)) if filters else 500
offset = int(filters.get("offset", 0)) if filters else 0

with requests.Session() as session:
while True:
markets = _request_markets_page(
session=session,
base_url=base,
params=params,
offset=offset,
limit=limit,
timeout=timeout,
)
if not markets:
break
yield from markets
if len(markets) < limit:
break
offset += limit


def normalize_gamma_market_to_clob_format(gamma_market: dict[str, Any]) -> dict[str, Any]:
"""
Normalize Gamma API market format to CLOB API format.

Gamma API uses camelCase field names, while the CLOB API and parsing code
expects snake_case field names.

Parameters
----------
gamma_market : dict[str, Any]
Market data from Gamma API in camelCase format.

Returns
-------
dict[str, Any]
Market data normalized to CLOB API format with snake_case fields.

"""
import json

# Handle rewards field
rewards = gamma_market.get("clobRewards", [])
rewards_dict = None
if rewards and len(rewards) > 0:
reward = rewards[0]
rewards_dict = {
"rates": reward.get("rewardsDailyRate"),
"min_size": gamma_market.get("rewardsMinSize"),
"max_spread": gamma_market.get("rewardsMaxSpread"),
}

# Build tokens array from clobTokenIds and outcomes
tokens = []
clob_token_ids = gamma_market.get("clobTokenIds", [])
outcomes = gamma_market.get("outcomes", [])
outcome_prices = gamma_market.get("outcomePrices", [])

# Parse JSON strings if needed
if isinstance(clob_token_ids, str):
clob_token_ids = json.loads(clob_token_ids)
if isinstance(outcomes, str):
outcomes = json.loads(outcomes)
if isinstance(outcome_prices, str):
outcome_prices = json.loads(outcome_prices)

# Create tokens array in CLOB format
for i, (token_id, outcome) in enumerate(zip(clob_token_ids, outcomes)):
token_entry = {
"token_id": token_id,
"outcome": outcome,
"price": float(outcome_prices[i]) if i < len(outcome_prices) else 0.5,
"winner": False, # Default value
}
tokens.append(token_entry)

normalized = {
# Core identifiers
"condition_id": gamma_market.get("conditionId"),
"question_id": gamma_market.get("questionID"),
"question": gamma_market.get("question"),
"description": gamma_market.get("description"),
"market_slug": gamma_market.get("slug"),

# Order book and trading settings
"enable_order_book": gamma_market.get("enableOrderBook", True),
"minimum_tick_size": gamma_market.get("orderPriceMinTickSize", 0.001),
"minimum_order_size": gamma_market.get("orderMinSize", 5),
"accepting_orders": gamma_market.get("acceptingOrders", True),
"accepting_order_timestamp": gamma_market.get("acceptingOrdersTimestamp"),
"seconds_delay": gamma_market.get("secondsDelay", 0),

# Market status flags
"active": gamma_market.get("active", False),
"closed": gamma_market.get("closed", False),
"archived": gamma_market.get("archived", False),

# Dates
"end_date_iso": gamma_market.get("endDateIso"),
"game_start_time": gamma_market.get("startDateIso"),

# Fee structure
"maker_base_fee": 0, # Gamma API doesn't provide fees directly, use known defaults
"taker_base_fee": 0, # Gamma API doesn't provide fees directly, use known defaults
"fpmm": gamma_market.get("marketMakerAddress", ""),

# Negative risk settings
"neg_risk": gamma_market.get("negRisk", False),
"neg_risk_market_id": gamma_market.get("negRiskMarketID"),
"neg_risk_request_id": gamma_market.get("negRiskRequestID"),

# Media
"icon": gamma_market.get("icon"),
"image": gamma_market.get("image"),

# Rewards and notifications
"rewards": rewards_dict,
"notifications_enabled": True, # Default value

# Outcome pricing flag
# "is_50_50_outcome": False, # Gamma API doesn't provide this flag

# Tokens array (CLOB API format)
"tokens": tokens,

# Preserve original data for reference
"_gamma_original": gamma_market,
}
return normalized


def list_markets(
filters: dict[str, Any] | None = None,
base_url: str | None = None,
timeout: float = 10.0,
max_results: int | None = None,
) -> list[dict[str, Any]]:
"""
Collect markets into a list.

Use `max_results` to cap total items fetched.

"""
results: list[dict[str, Any]] = []
count = 0
for market in iter_markets(filters=filters, base_url=base_url, timeout=timeout):
results.append(market)
count += 1
if max_results is not None and len(results) >= max_results:
break
return results
64 changes: 58 additions & 6 deletions nautilus_trader/adapters/polymarket/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from py_clob_client.client import ClobClient

from nautilus_trader.adapters.polymarket.common.constants import POLYMARKET_VENUE
from nautilus_trader.adapters.polymarket.common.gamma_markets import list_markets
from nautilus_trader.adapters.polymarket.common.gamma_markets import normalize_gamma_market_to_clob_format
from nautilus_trader.adapters.polymarket.common.parsing import parse_polymarket_instrument
from nautilus_trader.adapters.polymarket.common.symbol import get_polymarket_condition_id
from nautilus_trader.adapters.polymarket.common.symbol import get_polymarket_token_id
Expand Down Expand Up @@ -63,6 +65,59 @@ def __init__(
async def load_all_async(self, filters: dict | None = None) -> None:
await self._load_markets([], filters)

def _load_ids_using_gamma_markets(
self,
instrument_ids: list[InstrumentId],
filters: dict | None = None,
) -> None:
"""
Load instruments using Gamma API markets.
"""
condition_ids = [get_polymarket_condition_id(inst_id) for inst_id in instrument_ids]

if filters is None:
filters = {}

if len(condition_ids) <= 100: # We can filter directly by condition_id, but there is an API limit of max 100 condition_ids in the query string
self._log.info(f"Loading {len(condition_ids)} instruments, using direct condition_id filtering")
filters["condition_ids"] = condition_ids
else:
self._log.info(f"Loading {len(condition_ids)} instruments, using bulk load of all markets")

markets = list_markets(filters=filters) # Usually, you would use filters={"is_active": True} to skip archived markets
self._log.info(f"Loaded {len(markets)} markets using Gamma API")
for market in markets:
condition_id = market.get("conditionId")
if not condition_id:
continue

if condition_ids and condition_id not in condition_ids:
continue

normalized_market = normalize_gamma_market_to_clob_format(market)

# Use the normalized tokens array
for token_info in normalized_market.get("tokens", []):
token_id = token_info["token_id"]
outcome = token_info["outcome"]
self._load_instrument(normalized_market, token_id, outcome)

async def _load_ids_using_clob_api(
self,
instrument_ids: list[InstrumentId],
filters: dict | None = None,
) -> None:
"""
Load instruments using CLOB API.
"""
if len(instrument_ids) > 200:
self._log.warning(
f"Loading {len(instrument_ids)} instruments, using bulk load of all markets as a faster alternative",
)
await self._load_markets(instrument_ids, filters)
else:
await self._load_markets_seq(instrument_ids, filters)

async def load_ids_async(
self,
instrument_ids: list[InstrumentId],
Expand All @@ -81,13 +136,10 @@ async def load_ids_async(
"POLYMARKET",
)

if len(instrument_ids) > 200:
self._log.warning(
f"Loading {len(instrument_ids)} instruments, using bulk load of all markets as a faster alternative",
)
await self._load_markets(instrument_ids, filters)
if self._config.use_gamma_markets:
self._load_ids_using_gamma_markets(instrument_ids, filters)
else:
await self._load_markets_seq(instrument_ids, filters)
await self._load_ids_using_clob_api(instrument_ids, filters)

async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None:
PyCondition.not_none(instrument_id, "instrument_id")
Expand Down
Loading
Loading