33ad86f352
CI/CD / build-and-push (push) Successful in 1m32s
- CACHE_TTL: 4h → 6h (≤36 req/day with ≤9 politics markets) - GNews only called for is_politics markets (BTC/F&G cover crypto/macro) - MAX_NEWS_QUERIES_PER_CYCLE=5: BayesianStrategy.reset_cycle() called each iteration; counter increments only on actual API call (cache hits free) - 2s asyncio.sleep in news.py finally block after each real HTTP request - main.py sorts markets: politics first by end_date ascending, so soonest- resolving markets consume the 5-query budget before others Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""
|
|
Polymarket Trading Bot — Main Entry Point
|
|
# ci-test: 2026-04-14
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
from datetime import datetime, timezone
|
|
|
|
from bot.data.polymarket import PolymarketClient
|
|
from bot.data.external import ExternalDataClient
|
|
from bot.data.news import NewsClient
|
|
from bot.strategy.bayesian import BayesianStrategy
|
|
from bot.risk.manager import RiskManager
|
|
from bot.executor.paper import PaperExecutor
|
|
from bot.metrics.tracker import MetricsTracker
|
|
from bot.data.db import Database
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
log = logging.getLogger("bot.main")
|
|
|
|
PAPER_MODE = os.getenv("PAPER_MODE", "true").lower() == "true"
|
|
PAPER_BANKROLL = float(os.getenv("PAPER_BANKROLL", "10000"))
|
|
|
|
|
|
async def run_trading_loop(
|
|
poly: PolymarketClient,
|
|
external: ExternalDataClient,
|
|
strategy: BayesianStrategy,
|
|
risk: RiskManager,
|
|
executor: PaperExecutor,
|
|
metrics: MetricsTracker,
|
|
) -> None:
|
|
"""Main trading loop — runs every 60 seconds."""
|
|
log.info("Trading loop started. PAPER_MODE=%s", PAPER_MODE)
|
|
|
|
while True:
|
|
try:
|
|
# 1. Fetch active crypto/finance markets
|
|
markets = await poly.get_active_markets()
|
|
log.info("Found %d active markets", len(markets))
|
|
|
|
# Sort: politics markets first (soonest-resolving → highest GNews priority),
|
|
# then all others. This ensures the 5-query-per-cycle cap hits the most
|
|
# time-sensitive political markets before the budget runs out.
|
|
def _sort_key(m):
|
|
is_pol = m.category == "politics"
|
|
try:
|
|
dt = datetime.fromisoformat(m.end_date.replace("Z", "+00:00"))
|
|
except Exception:
|
|
dt = datetime(9999, 12, 31, tzinfo=timezone.utc)
|
|
return (0 if is_pol else 1, dt)
|
|
|
|
markets = sorted(markets, key=_sort_key)
|
|
for _m in markets:
|
|
log.info(" [market] %s | ends: %s | yes_price: %.3f",
|
|
_m.question, _m.end_date, _m.yes_price)
|
|
|
|
# 2. Get external signals
|
|
ext_data = await external.get_all_signals()
|
|
|
|
# Reset per-cycle GNews counter so the limit applies fresh each cycle
|
|
strategy.reset_cycle()
|
|
|
|
for market in markets:
|
|
# 3. Estimate true probability
|
|
signal = await strategy.evaluate(market, ext_data)
|
|
if signal is None:
|
|
continue
|
|
|
|
log.info(
|
|
"Signal: market=%s poly_price=%.3f our_estimate=%.3f confidence=%.2f",
|
|
market.question[:50],
|
|
signal.polymarket_price,
|
|
signal.estimated_prob,
|
|
signal.confidence,
|
|
)
|
|
|
|
# 4. Risk check + position sizing
|
|
order = risk.size_order(signal, executor.get_portfolio())
|
|
if order is None:
|
|
log.debug("Risk manager rejected order for %s", market.id)
|
|
continue
|
|
|
|
# 5. Execute (paper or real)
|
|
trade = await executor.execute(order)
|
|
if trade:
|
|
await metrics.record_trade(trade)
|
|
log.info("Trade executed: %s", trade)
|
|
|
|
# 6. Update daily metrics
|
|
await metrics.update_daily_summary()
|
|
|
|
except Exception as e:
|
|
log.error("Error in trading loop: %s", e, exc_info=True)
|
|
|
|
await asyncio.sleep(60)
|
|
|
|
|
|
async def main() -> None:
|
|
if PAPER_MODE:
|
|
log.info("=" * 60)
|
|
log.info(" PAPER TRADING MODE — No real money at risk")
|
|
log.info(" Bankroll: $%.2f simulated", PAPER_BANKROLL)
|
|
log.info("=" * 60)
|
|
else:
|
|
log.warning("REAL TRADING MODE ACTIVE — Real money at risk!")
|
|
|
|
db = Database()
|
|
await db.connect()
|
|
await db.run_migrations()
|
|
|
|
poly = PolymarketClient()
|
|
external = ExternalDataClient()
|
|
news = NewsClient()
|
|
strategy = BayesianStrategy(news=news)
|
|
risk = RiskManager(max_position_pct=0.05, max_exposure_pct=0.30)
|
|
executor = PaperExecutor(db=db, bankroll=PAPER_BANKROLL) if PAPER_MODE else None
|
|
metrics = MetricsTracker(db=db)
|
|
|
|
if executor is None:
|
|
# Import real executor only when explicitly needed
|
|
from bot.executor.real import RealExecutor # noqa
|
|
executor = RealExecutor(db=db)
|
|
|
|
if PAPER_MODE:
|
|
await executor.initialize()
|
|
|
|
try:
|
|
await run_trading_loop(poly, external, strategy, risk, executor, metrics)
|
|
finally:
|
|
await db.disconnect()
|
|
await news.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|