feat: initial commit — polymarket-bot source + CI/CD pipeline
CI/CD / build-and-push (push) Failing after 30s

This commit is contained in:
2026-04-13 16:05:45 +00:00
commit 4fda34df3b
23 changed files with 1436 additions and 0 deletions
View File
+142
View File
@@ -0,0 +1,142 @@
"""
Paper Trading Executor — simulates order execution without real money.
Simulates realistic slippage and fees to get accurate paper P&L.
All trades are logged to PostgreSQL for metrics analysis.
"""
import logging
import uuid
from dataclasses import dataclass
from datetime import datetime, UTC
from typing import Optional
from bot.risk.manager import Order, Portfolio
from bot.data.db import Database
log = logging.getLogger(__name__)
POLYMARKET_FEE = 0.02 # 2% fee on each trade
@dataclass
class Trade:
id: str
market_id: str
question: str
direction: str
size_usdc: float
entry_price: float
shares: float # How many YES/NO shares bought
fee_usdc: float
net_cost: float
timestamp: datetime
reasoning: str
paper: bool = True
def __str__(self) -> str:
return (
f"[PAPER] {self.direction} {self.shares:.1f} shares @ {self.entry_price:.3f} "
f"= ${self.net_cost:.2f} (fee ${self.fee_usdc:.2f}) | {self.question[:40]}"
)
class PaperExecutor:
"""Executes trades on paper — no real money, realistic simulation."""
def __init__(self, db: Database, bankroll: float) -> None:
self._db = db
self._portfolio = Portfolio(
cash=bankroll,
positions={},
)
log.info("Paper executor initialized with $%.2f bankroll", bankroll)
async def initialize(self) -> None:
"""Reconcile in-memory portfolio with DB state.
Called once after __init__ so the executor reflects any trades that
survived a pod restart. After a TRUNCATE the DB is empty and the
portfolio resets to a full bankroll automatically.
"""
positions = await self._db.get_open_positions()
if not positions:
log.info("No open positions in DB — starting with full bankroll")
return
total_deployed = sum(positions.values())
self._portfolio.positions = positions
self._portfolio.cash = max(0.0, self._portfolio.cash - total_deployed)
log.info(
"Restored %d open position(s) from DB — deployed $%.2f, cash $%.2f",
len(positions),
total_deployed,
self._portfolio.cash,
)
def get_portfolio(self) -> Portfolio:
return self._portfolio
async def execute(self, order: Order) -> Optional[Trade]:
"""Simulate order execution with fees and slippage."""
if order.size_usdc > self._portfolio.cash:
log.warning(
"Insufficient paper cash: need $%.2f have $%.2f",
order.size_usdc,
self._portfolio.cash,
)
return None
# Simulate small slippage (0.1-0.3% depending on order size)
slippage = min(0.003, order.size_usdc / 100000)
# Determine entry price based on direction.
# We fill at the current Polymarket mid price + slippage (buying at ask).
# BUY_YES → paying the YES price (order.market_price)
# BUY_NO → paying the NO price (1 - order.market_price)
if order.direction == "BUY_YES":
entry_price = min(0.99, order.market_price + slippage)
else:
entry_price = min(0.99, (1 - order.market_price) + slippage)
fee = order.size_usdc * POLYMARKET_FEE
net_cost = order.size_usdc + fee
shares = order.size_usdc / entry_price
trade = Trade(
id=str(uuid.uuid4()),
market_id=order.market_id,
question=order.question,
direction=order.direction,
size_usdc=order.size_usdc,
entry_price=entry_price,
shares=shares,
fee_usdc=fee,
net_cost=net_cost,
timestamp=datetime.now(UTC),
reasoning=order.reasoning,
paper=True,
)
# Update paper portfolio
self._portfolio.cash -= net_cost
self._portfolio.positions[order.market_id] = order.size_usdc
# Persist to DB
await self._db.save_trade(trade)
return trade
async def close_position(self, market_id: str, resolution: float) -> Optional[float]:
"""
Close a paper position after market resolution.
resolution: 1.0 if YES won, 0.0 if NO won.
Returns P&L in USDC.
"""
if market_id not in self._portfolio.positions:
return None
# This would be called by a settlement watcher (future feature)
# For now, positions auto-expire at market end date
position_cost = self._portfolio.positions.pop(market_id)
log.info("Closed position in %s, resolution=%.0f", market_id, resolution)
return position_cost * resolution - position_cost
+63
View File
@@ -0,0 +1,63 @@
"""
REAL TRADING EXECUTOR — Only loaded when PAPER_MODE=false.
DO NOT MODIFY unless explicitly asked and paper trading thresholds are met:
- Sharpe Ratio > 0.5 (over 4+ weeks)
- Win Rate > 52%
- Calibration Score > 0.7
- Explicit `make promote` confirmation
This file is intentionally minimal until paper trading validates the strategy.
"""
import logging
import os
from typing import Optional
from bot.risk.manager import Order
from bot.executor.paper import Trade
from bot.data.db import Database
log = logging.getLogger(__name__)
class RealExecutor:
"""
Real money executor using py-clob-client.
Requires: POLYMARKET_API_KEY, POLYMARKET_SECRET, POLYMARKET_PASSPHRASE, WALLET_PRIVATE_KEY
"""
def __init__(self, db: Database) -> None:
# Safety check
if os.getenv("PAPER_MODE", "true").lower() == "true":
raise RuntimeError("RealExecutor instantiated while PAPER_MODE=true — this is a bug!")
log.critical("REAL EXECUTOR ACTIVE — Real USDC will be spent")
self._db = db
self._client = self._init_client()
def _init_client(self):
try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import ApiCreds
host = "https://clob.polymarket.com"
key = os.getenv("WALLET_PRIVATE_KEY", "")
chain_id = 137 # Polygon
creds = ApiCreds(
api_key=os.getenv("POLYMARKET_API_KEY", ""),
api_secret=os.getenv("POLYMARKET_SECRET", ""),
api_passphrase=os.getenv("POLYMARKET_PASSPHRASE", ""),
)
return ClobClient(host, key=key, chain_id=chain_id, creds=creds)
except ImportError:
raise ImportError("py-clob-client not installed. Run: pip install py-clob-client")
async def execute(self, order: Order) -> Optional[Trade]:
"""Execute real order on Polymarket CLOB."""
# TODO: Implement after paper trading phase validates strategy
# See: https://docs.polymarket.com/developers/clob/examples
raise NotImplementedError("Real executor not yet implemented — complete paper trading phase first")
def get_portfolio(self):
raise NotImplementedError("Real portfolio tracking not yet implemented")