Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
651 changes: 651 additions & 0 deletions polymarket/_shared/pair_stateful_replay.py

Large diffs are not rendered by default.

119 changes: 107 additions & 12 deletions polymarket/_shared/polymarket_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def normalize_history(
if isinstance(item, dict):
t = safe_int(item.get("t"), -1)
p = safe_float(item.get("p"), -1.0)
elif isinstance(item, list | tuple) and len(item) >= 2:
elif isinstance(item, (list, tuple)) and len(item) >= 2:
t = safe_int(item[0], -1)
p = safe_float(item[1], -1.0)
if t < 0 or not (0.0 <= p <= 1.0) or t in seen:
Expand Down Expand Up @@ -162,7 +162,7 @@ def best_price(levels: Any, fallback: float = 0.0) -> float:
level = levels[0]
if isinstance(level, dict):
return safe_float(level.get("price"), fallback)
if isinstance(level, list | tuple) and level:
if isinstance(level, (list, tuple)) and level:
return safe_float(level[0], fallback)
return fallback

Expand Down Expand Up @@ -361,7 +361,7 @@ def _extract_call_publisher_body(result: dict[str, Any]) -> Any:
structured = result.get("structuredContent")
if isinstance(structured, dict):
body = structured.get("body")
if isinstance(body, dict | list):
if isinstance(body, (dict, list)):
return body
return structured
content = result.get("content")
Expand All @@ -380,13 +380,13 @@ def _extract_call_publisher_body(result: dict[str, Any]) -> Any:
continue
if isinstance(parsed, dict):
body = parsed.get("body")
if isinstance(body, dict | list):
if isinstance(body, (dict, list)):
return body
return parsed
if isinstance(parsed, list):
return parsed
body = result.get("body")
if isinstance(body, dict | list):
if isinstance(body, (dict, list)):
return body
return result.get("value")

Expand Down Expand Up @@ -954,6 +954,99 @@ def get_positions(self) -> Any:
return self._call("GET", "/positions")


class DirectClobTrader:
"""Direct Polymarket CLOB client for local py-clob-client execution."""

def __init__(
self,
*,
skill_root: Path,
client_name: str,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
) -> None:
maybe_load_dotenv(skill_root)
self.client_name = client_name
self.timeout_seconds = timeout_seconds

try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import ApiCreds
except ImportError as exc:
raise RuntimeError(
"Direct CLOB trading requires `py-clob-client`. "
"Create and activate a virtual environment first, then run "
"`python -m pip install -r requirements.txt`."
) from exc

private_key = safe_str(
os.getenv("POLY_PRIVATE_KEY") or os.getenv("WALLET_PRIVATE_KEY"),
"",
).strip()
api_key = safe_str(os.getenv("POLY_API_KEY"), "").strip()
api_passphrase = safe_str(os.getenv("POLY_PASSPHRASE"), "").strip()
api_secret = safe_str(os.getenv("POLY_SECRET"), "").strip()
if not private_key:
raise RuntimeError(
"Direct CLOB trading requires `POLY_PRIVATE_KEY` or `WALLET_PRIVATE_KEY`."
)
if not api_key or not api_passphrase or not api_secret:
raise RuntimeError(
"Missing required Polymarket L2 credentials. Set "
"`POLY_API_KEY`, `POLY_PASSPHRASE`, and `POLY_SECRET`."
)

chain_id = safe_int(os.getenv("POLY_CHAIN_ID"), DEFAULT_CHAIN_ID)
creds = ApiCreds(
api_key=api_key,
api_secret=api_secret,
api_passphrase=api_passphrase,
)
self._client = ClobClient(
host="https://clob.polymarket.com",
key=private_key,
chain_id=chain_id,
creds=creds,
)
self.address = self._client.get_address()

def create_order(
self,
*,
token_id: str,
side: str,
price: float,
size: float,
tick_size: str,
neg_risk: bool,
fee_rate_bps: int,
) -> Any:
del tick_size, neg_risk, fee_rate_bps
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY, SELL

clob_side = BUY if side.upper() == "BUY" else SELL
order_args = OrderArgs(
price=price,
size=size,
side=clob_side,
token_id=token_id,
)
signed_order = self._client.create_order(order_args)
return self._client.post_order(signed_order, OrderType.GTC)

def cancel_all(self) -> Any:
return self._client.cancel_all()

def get_orders(self) -> Any:
return self._client.get_orders()

def get_positions(self) -> Any:
try:
return self._client.get_balance_allowance()
except Exception:
return []


def single_market_inventory_notional(
*,
raw_positions: Any,
Expand Down Expand Up @@ -1010,7 +1103,7 @@ def live_settings_from_execution(execution: dict[str, Any]) -> LiveExecutionSett

def execute_single_market_quotes(
*,
trader: PolymarketPublisherTrader,
trader: PolymarketPublisherTrader | DirectClobTrader,
quotes: list[dict[str, Any]],
markets: list[dict[str, Any]],
execution_settings: LiveExecutionSettings,
Expand Down Expand Up @@ -1038,16 +1131,18 @@ def execute_single_market_quotes(
tick_size = safe_str(market.get("tick_size"), "0.01")
neg_risk = bool(market.get("neg_risk", False))
fee_rate_bps = fetch_fee_rate_bps(token_id)
quote_notional = max(0.0, safe_float(quote.get("quote_notional_usd"), 0.0))
if quote_notional <= 0.0:
fallback_notional = max(0.0, safe_float(quote.get("quote_notional_usd"), 0.0))
bid_notional = max(0.0, safe_float(quote.get("bid_notional_usd"), fallback_notional))
ask_notional = max(0.0, safe_float(quote.get("ask_notional_usd"), fallback_notional))
if bid_notional <= 0.0 and ask_notional <= 0.0:
skips.append({"market_id": market["market_id"], "reason": "zero_quote_notional"})
continue

bid_price = snap_price(safe_float(quote.get("bid_price"), 0.0), tick_size, "BUY")
ask_price = snap_price(safe_float(quote.get("ask_price"), 0.0), tick_size, "SELL")

if bid_price > 0.0:
bid_size = quote_notional / max(bid_price, 1e-9)
if bid_price > 0.0 and bid_notional > 0.0:
bid_size = bid_notional / max(bid_price, 1e-9)
response = trader.create_order(
token_id=token_id,
side="BUY",
Expand All @@ -1069,7 +1164,7 @@ def execute_single_market_quotes(
)

available_shares = max(0.0, position_sizes.get(token_id, 0.0))
sell_notional = min(quote_notional, available_shares * max(ask_price, 0.0))
sell_notional = min(ask_notional, available_shares * max(ask_price, 0.0))
if ask_price > 0.0 and sell_notional > 0.0:
ask_size = sell_notional / max(ask_price, 1e-9)
response = trader.create_order(
Expand Down Expand Up @@ -1124,7 +1219,7 @@ def execute_single_market_quotes(

def execute_pair_trades(
*,
trader: PolymarketPublisherTrader,
trader: PolymarketPublisherTrader | DirectClobTrader,
pair_trades: list[dict[str, Any]],
markets: list[dict[str, Any]],
execution_settings: LiveExecutionSettings,
Expand Down
14 changes: 11 additions & 3 deletions polymarket/high-throughput-paired-basis-maker/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ description: "Run a paired-market basis strategy on Polymarket with mandatory ba

## Workflow Summary

1. `load_backtest_pairs` pulls live market histories from the Seren Polymarket Publisher (Gamma + CLOB proxied), builds pairs from the active market universe, and timestamp-aligns each pair.
2. `simulate_basis_reversion` evaluates entry/exit behavior on basis widening and convergence.
3. `summarize_backtest` reports total return, annualized return, Sharpe-like score, max drawdown, hit rate, trade-rate, and pair-level contributions.
1. `load_backtest_pairs` pulls live market histories from the Seren Polymarket Publisher (Gamma + CLOB proxied), attaches per-leg order-book snapshots, builds pairs from the active market universe, and timestamp-aligns each pair.
2. `simulate_basis_reversion` runs an event-driven stateful replay with carried cash and inventory across both legs, order-book-aware fills, and pessimistic spread-decay.
3. `summarize_backtest` reports total return, annualized return, Sharpe-like score, max drawdown, hit rate, quoted/fill counts, order-book mode coverage, telemetry counts, and pair-level contributions.
4. `sample_gate` fails backtest if `events < backtest.min_events` (default `200`).
5. `backtest_gate` blocks trade mode by default if backtest return is non-positive.
6. `emit_pair_trades` outputs two-leg trade intents (`primary` + `pair`) with risk caps.
Expand Down Expand Up @@ -65,6 +65,14 @@ If you are already running inside Seren Desktop, the runtime can use injected au
python3 scripts/agent.py --config config.json --run-type trade
```

## Optional Fixture Replay

```bash
python3 scripts/agent.py --config config.json --backtest-file tests/fixtures/backtest_pairs.json
```

Set `backtest.telemetry_path` to capture JSONL replay telemetry for each decision step.

## Disclaimer

This skill can lose money. Basis spreads can persist or widen, hedge legs can slip, and liquidity can fail during volatility. Backtests are hypothetical and do not guarantee future results. This skill is software tooling and not financial advice. Use dry-run first and only trade with risk capital.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"participation_rate": 0.95,
"min_history_points": 72,
"min_events": 200,
"volatility_window_points": 24,
"require_orderbook_history": false,
"spread_decay_bps": 45,
"join_best_queue_factor": 0.85,
"off_best_queue_factor": 0.35,
"synthetic_orderbook_half_spread_bps": 18,
"synthetic_orderbook_depth_usd": 125,
"telemetry_path": "",
"min_liquidity_usd": 5000,
"markets_fetch_page_size": 500,
"max_markets": 0,
Expand Down
Loading