feat: initial commit — polymarket-bot source + CI/CD pipeline
CI/CD / build-and-push (push) Failing after 30s
CI/CD / build-and-push (push) Failing after 30s
This commit is contained in:
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user