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
6 changes: 3 additions & 3 deletions .claude/skills/alphapoly-enter-position/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ Enter a hedged pair position (target + cover) from a detected alphapoly portfoli

1. **Backend running** on `http://localhost:8000`
2. **Wallet unlocked** — check `GET /wallet/status` for `"unlocked": true`; unlock via `POST /wallet/unlock` with `{"password": "<password>"}`
3. **Sufficient USDC.e balance** — total cost is `amount_per_position * 2`
3. **Sufficient pUSD balance** — total cost is `amount_per_position * 2`

## 7-Step Flow

1. **List portfolios** — `GET /data/portfolios`; note `pair_id`, market IDs, and position sides
2. **Pick a pair** — select by `pair_id`
3. **Set amount** — choose `amount_per_position` in USDC.e
3. **Set amount** — choose `amount_per_position` in pUSD
4. **Estimate** (required) — `POST /trading/buy-pair/estimate`; show result to user; stop if `sufficient_balance: false`
5. **Confirm** — require explicit user approval before proceeding
6. **Execute** — `POST /trading/buy-pair`; check `success: true` on both legs; surface any `warnings`
Expand All @@ -27,7 +27,7 @@ For full request/response schemas, see [api-reference.md](api-reference.md).

## Safety Rules

These trades involve real USDC on Polygon — transactions are irreversible once submitted on-chain. The estimate step is the only chance to catch problems before money moves.
These trades involve real pUSD on Polygon — transactions are irreversible once submitted on-chain. The estimate step is the only chance to catch problems before money moves.

- Always run `/trading/buy-pair/estimate` before `/trading/buy-pair` — the estimate reveals actual costs, slippage, and balance issues before committing funds
- Never execute without explicit user confirmation — the user needs to see the estimate and agree to the cost
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/alphapoly-enter-position/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
```
GET /wallet/status
```
Response: `{ "exists": true, "unlocked": true, "address": "0x...", "balances": { "usdc_e": 150.0, "pol": 0.8 }, "approvals_set": true }`
Response: `{ "exists": true, "unlocked": true, "address": "0x...", "balances": { "pusd": 150.0, "pol": 0.8 }, "approvals_set": true }`

### Unlock
```
Expand Down Expand Up @@ -75,7 +75,7 @@ Same request body as estimate. Response (`BuyPairResponse`):
},
"cover": { "...same shape..." },
"total_spent": 20.0,
"final_balances": { "usdc_e": 130.0, "pol": 0.8 },
"final_balances": { "pusd": 130.0, "pol": 0.8 },
"warnings": []
}
```
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/alphapoly-exit-position/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Response (`SellTokenResponse`):

## Merge Tokens

Burns equal amounts of YES+NO tokens and returns USDC.e collateral. Use when a market has resolved. Merged amount = `min(yes_balance, no_balance)`.
Burns equal amounts of YES+NO tokens and returns pUSD collateral. Use when a market has resolved. Merged amount = `min(yes_balance, no_balance)`.

```
POST /positions/{position_id}/merge
Expand Down
8 changes: 0 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,3 @@ CHAINSTACK_NODE=https://polygon-mainnet.core.chainstack.com/xxxxxxxx
# OPTIONAL: HTTPS proxy for Polymarket CLOB API
# Required if trading is geo-restricted in your region (403 errors)
# HTTPS_PROXY=http://user:pass@proxy-host:port

# OPTIONAL: Polymarket V2 / V1 path selector. Post-2026-04-28 cutover the
# bot defaults to V2 (pUSD collateral, py-clob-client-v2, EIP-712 v2). The
# V1 endpoints stopped accepting orders at the cutover, so V2 is the only
# path that actually trades. Set this to false only if you're running
# against the legacy V1 stack via the v1-final tag — V1 paths will be
# removed in v2.0 (~2026-05-05).
# POLYMARKET_V2_ENABLED=true
13 changes: 3 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

> Polymarket alpha detection platform. LLM pipeline groups related markets, extracts logical implications, and builds covering portfolios (hedged positions via contrapositive logic). Next.js dashboard with real-time price tracking and position management.

## Polymarket V2 cutover (2026-04-28 11:00 UTC)

The bot supports both V1 (USDC.e collateral) and V2 (pUSD collateral) trading paths, gated by `POLYMARKET_V2_ENABLED`. Single source of truth: `core/feature_flags.v2_enabled()`.

**Phase 2 PR #49** (`feat/v2-flip-default`) flips the default unset → true at cutover. **DO NOT MERGE BEFORE 2026-04-28 11:00 UTC** — V2 books are empty pre-cutover.

Operator post-flip: re-run `WalletManager.set_approvals()` once (approvals are mode-gated). USDC.e holders wrap to pUSD via `experiments/trading/02_wrap_to_pusd.py`. V1 retirement (#37) is queued for ~2026-05-05 after a 1-week soak; the `v1-final` git tag at `94e89ce` is the permanent legacy reference.
Trades against Polymarket V2 (pUSD collateral, `py-clob-client-v2`, EIP-712 domain version "2"). Legacy V1 was retired on 2026-04-28; for archaeology see the `v1-final` git tag.

## Commands

Expand Down Expand Up @@ -72,11 +66,11 @@ data/ # Pipeline outputs (gitignored)
| `POST /positions/{id}/exit` | Orchestrated sell + merge fallback |
| `POST /trading/buy-pair` | Execute covered pair trade |
| `POST /trading/buy-pair/estimate` | Estimate trade cost |
| `GET /wallet/status` | Wallet state (balances, approvals, V2-aware) |
| `GET /wallet/status` | Wallet state (POL + pUSD balances, approvals) |
| `POST /wallet/generate` | Generate new wallet (encrypted with passphrase) |
| `POST /wallet/import` | Import private key (encrypted with passphrase) |
| `POST /wallet/unlock` / `lock` | Decrypt key into memory / clear |
| `POST /wallet/approve-contracts` | Set all Polymarket approvals (V2-aware) |
| `POST /wallet/approve-contracts` | Set all Polymarket V2 approvals |
| `GET /health` | Health check |

> Debug: `GET /prices/current`, `WS /prices/ws`
Expand Down Expand Up @@ -109,7 +103,6 @@ VALIDATION_MODEL=... # Required: model for validation
POLYMARKET_TAG=politics # Optional: market filter
MARKET_POLLING_ENABLED=false # Optional: background polling
CHAINSTACK_NODE=https://... # Optional: Polygon RPC for on-chain trades
POLYMARKET_V2_ENABLED=true # Optional: V1/V2 path selector. Pre-#49 default false; post-#49 default true. Set =false explicitly to opt back to V1 (until V1 is removed in #37).
```

## Git
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@

# Alphapoly - Polymarket alpha detection platform

> ⚠️ **Polymarket V2 cutover: 2026-04-28 11:00 UTC.**
> After cutover, set `POLYMARKET_V2_ENABLED=true` in your `.env` and re-run `WalletManager.set_approvals()` once (approvals are mode-gated).
> If you hold USDC.e, wrap it to pUSD with [`experiments/trading/02_wrap_to_pusd.py`](experiments/trading/02_wrap_to_pusd.py) before trading.
> V1 code paths will be removed in **v2.0** (~2026-05-05). To pin to the legacy V1 stack: `git checkout v1-final`.
> Trades against Polymarket V2 (pUSD collateral). The legacy V1 (USDC.e) stack was retired on 2026-04-28 — `git checkout v1-final` to inspect it. If you hold USDC.e, wrap it to pUSD with [`experiments/trading/02_wrap_to_pusd.py`](experiments/trading/02_wrap_to_pusd.py) before trading.

Find covering portfolios across correlated prediction markets using predefined rules and LLM decisions. The system detects relationships between markets, classifies them to identify hedging pairs, and tracks their prices. The platform offers a smooth UI for entering detected pairs when profit opportunities exist and tracking your positions.

Expand Down Expand Up @@ -113,7 +110,7 @@ Standalone research scripts (no imports from `backend/`). Three groups:
| Folder | Description |
|--------|-------------|
| [`experiments/`](experiments/) | Pipeline-step learning examples — fetch events, build groups, extract implications, validate, score portfolios, stream prices. Mirrors what `backend/core/runner.py` orchestrates, one stage per file. |
| [`experiments/trading/`](experiments/trading/) | Wallet + funding + position helpers. Generate a wallet, swap native USDC → USDC.e (legacy), wrap USDC.e → pUSD (Polymarket V2 collateral), buy a position, transfer tokens. Flag-aware: scripts read `POLYMARKET_V2_ENABLED` to mirror the backend's V1/V2 routing. |
| [`experiments/trading/`](experiments/trading/) | Wallet + funding + position helpers. Generate a wallet, swap native USDC → USDC.e (legacy), wrap USDC.e → pUSD (Polymarket V2 collateral), buy a position, transfer tokens. |
| [`experiments/onchain-otc/`](experiments/onchain-otc/) | On-chain OTC trading without the CLOB — split/merge, P2P transfers, atomic escrow, NegRisk conversions, and intent-based settlement on an Anvil fork of Polygon. Forked-chain research; uses USDC.e collateral (the fork's frozen state predates Polymarket V2 / pUSD). |

---
Expand Down
24 changes: 0 additions & 24 deletions backend/core/feature_flags.py

This file was deleted.

44 changes: 16 additions & 28 deletions backend/core/positions/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,13 @@
from web3 import Web3


from core.feature_flags import v2_enabled
from core.http_retry import fetch_json_with_retry
from core.positions.service import PositionService
from core.positions.storage import PositionStorage
from core.wallet.contracts import CONTRACTS, V2_CONTRACTS, CTF_ABI
from core.wallet.contracts import CONTRACTS, CTF_ABI
from core.wallet.manager import WalletManager


def _collateral_address() -> str:
"""Active collateral token (pUSD under V2 flag, USDC.e otherwise)."""
return V2_CONTRACTS["PUSD"] if v2_enabled() else CONTRACTS["USDC_E"]


@dataclass
class SellResult:
"""Result of selling tokens via CLOB."""
Expand Down Expand Up @@ -101,30 +95,22 @@ def _merge_tokens(
self,
condition_id: str,
amount: float,
neg_risk: bool = False,
) -> tuple[Optional[str], Optional[str]]:
"""Merge YES+NO tokens back to USDC. Returns (tx_hash, error).
"""Merge YES+NO tokens back to pUSD. Returns (tx_hash, error).

For binary markets, merges via CTF directly.
For NegRisk (multi-outcome) markets, merges via the NegRisk adapter.
Always routes through the standard CTF: the per-outcome conditionId
from Gamma is registered on CTF. Gamma's `negRisk` flag is a market
grouping hint, not an on-chain routing instruction.
"""
w3 = self._get_web3()
address = Web3.to_checksum_address(self.wallet.address)
account = w3.eth.account.from_key(self.wallet.get_unlocked_key())

# Always merge via standard CTF: the per-outcome conditionId from
# Gamma is registered on CTF. Same reasoning as the split path in
# `executor.py` — Gamma's `negRisk` flag is a market grouping hint,
# not an on-chain routing instruction.
merge_contract_addr = CONTRACTS["CTF"]
contract = w3.eth.contract(
address=Web3.to_checksum_address(merge_contract_addr),
address=Web3.to_checksum_address(CONTRACTS["CTF"]),
abi=CTF_ABI,
)
logger.info(
f"Merge via CTF{' (NegRisk-grouped market)' if neg_risk else ''}: "
f"{merge_contract_addr[:10]}..."
)
logger.info(f"Merge via CTF: {CONTRACTS['CTF'][:10]}...")

amount_wei = int(amount * 1e6)
condition_bytes = bytes.fromhex(
Expand All @@ -135,17 +121,21 @@ def _merge_tokens(
base_gas_price = w3.eth.gas_price
gas_price = int(base_gas_price * 1.2)

tx = contract.functions.mergePositions(
Web3.to_checksum_address(_collateral_address()),
# Estimate gas live — see executor._split_position for context.
merge_fn = contract.functions.mergePositions(
Web3.to_checksum_address(CONTRACTS["PUSD"]),
bytes(32), # parentCollectionId
condition_bytes,
[1, 2], # partition for YES, NO
amount_wei,
).build_transaction(
)
gas_estimate = int(merge_fn.estimate_gas({"from": address}) * 1.25)

tx = merge_fn.build_transaction(
{
"from": address,
"nonce": w3.eth.get_transaction_count(address),
"gas": 300000,
"gas": gas_estimate,
"gasPrice": gas_price,
"chainId": 137,
}
Expand Down Expand Up @@ -325,9 +315,7 @@ async def merge_position_tokens(
error=f"Failed to fetch market info: {e}",
)

# Execute merge — use NegRisk adapter for multi-outcome markets
neg_risk = bool(market_data.get("negRisk", False))
tx_hash, error = self._merge_tokens(condition_id, mergeable, neg_risk=neg_risk)
tx_hash, error = self._merge_tokens(condition_id, mergeable)

if error:
return MergeResult(
Expand Down
31 changes: 12 additions & 19 deletions backend/core/trading/clob.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
"""Shared CLOB market sell — FAK order with slippage protection.

Under ``POLYMARKET_V2_ENABLED`` the V2 SDK is used. The V2 ``MarketOrderArgsV2``
struct drops ``fee_rate_bps``/``nonce``/``taker`` (taker fees are computed
server-side at match time in V2) and adds an optional ``metadata`` field. The
EIP-712 domain version flips from ``"1"`` to ``"2"`` internally in
``py_clob_client_v2`` — we don't pass it.
Uses V2 SDK exclusively post-2026-04-28 cutover. The ``MarketOrderArgsV2``
struct has no ``fee_rate_bps``/``nonce``/``taker`` (taker fees are computed
server-side at match time) and adds an optional ``metadata`` field. The
EIP-712 domain version is "2", handled internally by ``py_clob_client_v2``.
"""

import time
from typing import Optional

from loguru import logger

from core.feature_flags import v2_enabled


def _tick_decimals(tick_size: float) -> int:
"""Count decimal places from a tick size value."""
Expand Down Expand Up @@ -56,18 +53,14 @@ def sell_via_clob(
return None, 0.0, msg

try:
if v2_enabled():
# V2 struct: no fee_rate_bps / nonce / taker; fees are match-time.
# `metadata` defaults to BYTES32_ZERO; `builder_code` is set on the
# client via builder_config (we leave it None per #27 scope).
from py_clob_client_v2.clob_types import (
MarketOrderArgsV2 as MarketOrderArgs,
OrderType,
)
from py_clob_client_v2.order_builder.constants import SELL
else:
from py_clob_client.clob_types import MarketOrderArgs, OrderType
from py_clob_client.order_builder.constants import SELL
# V2 struct: no fee_rate_bps / nonce / taker; fees are match-time.
# `metadata` defaults to BYTES32_ZERO; `builder_code` is set on the
# client via builder_config (we leave it None per #27 scope).
from py_clob_client_v2.clob_types import (
MarketOrderArgsV2 as MarketOrderArgs,
OrderType,
)
from py_clob_client_v2.order_builder.constants import SELL

# Fetch market's tick size for correct price precision
try:
Expand Down
Loading