411d346261
CI/CD / build-and-push (push) Successful in 2m16s
BayesianStrategy now tracks per-cycle counters (reset each cycle): - skip_prior_extreme, skip_family - skip_edge_net_nonpositive (edge_net ≤ 0) - skip_edge_net_below_regime (0 < edge_net < regime_min) - evaluated_edges list for max/pct computations main.py logs one structured [CYCLE SUMMARY] block per cycle with: markets_total, markets_uncertainty_zone, max_edge_gross, max_edge_net, pct_edge_gross_gt_002, pct_edge_gross_gt_004, all blocked_by_* counters, trades_executed, gnews_queries_used/cap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
8.1 KiB
Python
212 lines
8.1 KiB
Python
"""
|
|
Polymarket Trading Bot — Main Entry Point
|
|
# ci-test: 2026-04-16
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
from bot.data.polymarket import PolymarketClient, market_family_key
|
|
from bot.data.external import ExternalDataClient
|
|
from bot.data.news import NewsClient
|
|
from bot.strategy.bayesian import BayesianStrategy, gnews_priority, MAX_NEWS_QUERIES_PER_CYCLE
|
|
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,
|
|
db: Database,
|
|
) -> None:
|
|
"""Main trading loop — runs every 60 seconds."""
|
|
log.info("Trading loop started. PAPER_MODE=%s", PAPER_MODE)
|
|
|
|
while True:
|
|
try:
|
|
# 1. Fetch active markets (90-day window)
|
|
markets = await poly.get_active_markets()
|
|
log.info("Found %d active markets", len(markets))
|
|
|
|
# 2. Get external signals
|
|
ext_data = await external.get_all_signals()
|
|
|
|
# 3. Build occupied_families from the current open portfolio positions.
|
|
# This prevents re-entering a family where we already hold a position.
|
|
# We also pull from DB to survive pod restarts.
|
|
portfolio = executor.get_portfolio()
|
|
occupied_families: set[str] = set()
|
|
for market_id in portfolio.positions:
|
|
mkt = next((m for m in markets if m.id == market_id), None)
|
|
if mkt:
|
|
occupied_families.add(market_family_key(mkt))
|
|
# Also seed from DB in case a family was traded in a prior cycle
|
|
# that isn't reflected in the current markets list
|
|
db_families = await db.get_open_families()
|
|
occupied_families |= db_families
|
|
if occupied_families:
|
|
log.info("Occupied families (from portfolio): %s", sorted(occupied_families))
|
|
|
|
# 4. Sort markets.
|
|
# Politics: sort by gnews_priority DESC (highest-value markets get
|
|
# GNews budget first — Phase 3).
|
|
# Others: sort by end_date ASC (soonest-resolving first).
|
|
def _sort_key(m):
|
|
try:
|
|
dt = datetime.fromisoformat(m.end_date.replace("Z", "+00:00"))
|
|
except Exception:
|
|
dt = datetime(9999, 12, 31, tzinfo=timezone.utc)
|
|
if m.category == "politics":
|
|
priority = gnews_priority(m, strategy._news) if strategy._news else 0.0
|
|
# Bucket 0 = politics, sort by priority DESC (negate for asc sort)
|
|
return (0, -priority, dt)
|
|
return (1, 0.0, dt)
|
|
|
|
markets = sorted(markets, key=_sort_key)
|
|
|
|
for _m in markets:
|
|
log.info(
|
|
" [market] %-55s | cat=%-12s | family=%-28s | ends=%s | yes=%.3f",
|
|
_m.question[:55], _m.category, market_family_key(_m),
|
|
_m.end_date[:10] if _m.end_date else "?", _m.yes_price,
|
|
)
|
|
|
|
# Reset per-cycle GNews counter
|
|
strategy.reset_cycle()
|
|
|
|
# 5. Evaluate each market
|
|
cycle_trades = 0
|
|
for market in markets:
|
|
# evaluate() returns None for all skips — reasons are logged internally
|
|
signal = await strategy.evaluate(market, ext_data, occupied_families)
|
|
if signal is None:
|
|
continue
|
|
|
|
log.info(
|
|
"Signal generated: market=%-50s | edge_gross=%+.3f | edge_net=%+.3f | "
|
|
"regime_min=%.2f | family=%s | conf=%.2f",
|
|
market.question[:50],
|
|
signal.edge_gross,
|
|
signal.edge_net,
|
|
signal.regime_min_edge,
|
|
signal.family_key,
|
|
signal.confidence,
|
|
)
|
|
|
|
# 6. Risk check + position sizing
|
|
order = risk.size_order(signal, portfolio)
|
|
if order is None:
|
|
log.debug("Risk manager rejected order for %s", market.id)
|
|
continue
|
|
|
|
# 7. Execute (paper)
|
|
trade = await executor.execute(order)
|
|
if trade:
|
|
await metrics.record_trade(trade)
|
|
log.info("Trade executed: %s", trade)
|
|
# Block this family for the rest of the cycle (Phase 2)
|
|
occupied_families.add(signal.family_key)
|
|
cycle_trades += 1
|
|
|
|
# 8. [CYCLE SUMMARY] — one block per cycle, stable format for grep/compare
|
|
stats = strategy.get_cycle_stats()
|
|
n_total = len(markets)
|
|
n_uncertainty = sum(1 for m in markets if 0.35 <= m.yes_price <= 0.65)
|
|
n_eval = stats["evaluated_count"]
|
|
def _pct(n: int, denom: int) -> str:
|
|
if denom == 0:
|
|
return "0% (0/0)"
|
|
return f"{n * 100 // denom}% ({n}/{denom})"
|
|
gnews_cap = strategy._news_queries_this_cycle # already updated by reset below
|
|
|
|
log.info(
|
|
"[CYCLE SUMMARY]\n"
|
|
" markets_total: %d\n"
|
|
" markets_uncertainty_zone: %d (prior 0.35-0.65)\n"
|
|
" max_edge_gross: %+.3f\n"
|
|
" max_edge_net: %+.3f\n"
|
|
" pct_edge_gross_gt_002: %s\n"
|
|
" pct_edge_gross_gt_004: %s\n"
|
|
" blocked_by_family: %d\n"
|
|
" blocked_by_prior_extreme: %d\n"
|
|
" blocked_by_edge_net_nonpositive:%d\n"
|
|
" blocked_by_edge_net_below_regime:%d\n"
|
|
" trades_executed: %d\n"
|
|
" gnews_queries_used: %d/%d",
|
|
n_total,
|
|
n_uncertainty,
|
|
stats["max_edge_gross"],
|
|
stats["max_edge_net"],
|
|
_pct(stats["gross_gt_002"], n_total),
|
|
_pct(stats["gross_gt_004"], n_total),
|
|
stats["skip_family"],
|
|
stats["skip_prior_extreme"],
|
|
stats["skip_edge_net_nonpositive"],
|
|
stats["skip_edge_net_below_regime"],
|
|
cycle_trades,
|
|
stats["gnews_queries_used"], MAX_NEWS_QUERIES_PER_CYCLE,
|
|
)
|
|
|
|
# 9. 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:
|
|
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, db)
|
|
finally:
|
|
await db.disconnect()
|
|
await news.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|