diff --git a/polymarket_bot/README.md b/polymarket_bot/README.md new file mode 100644 index 00000000..e1fd8cc3 --- /dev/null +++ b/polymarket_bot/README.md @@ -0,0 +1,42 @@ +# PolyMarket Finance Bot (Scaffold) + +Khung xương bot tự động cho PolyMarket, dùng Kronos (`model.Kronos`, `model.KronosTokenizer`, `model.KronosPredictor`) để suy luận xác suất xu hướng ngắn hạn. + +## Cấu trúc + +- `data/collector.py`: lấy market snapshot + lịch sử giá (mock, sẵn sàng thay API thật) +- `model/kronos_adapter.py`: adapter cho Kronos inference +- `decision/engine.py`: EV + Kelly + risk caps +- `execution/executor.py`: place order abstraction + retry +- `monitoring/logger.py`: logging giao dịch và metrics +- `main.py`: orchestration pipeline end-to-end + +## Chạy demo + +### Cách 1 (khuyến nghị): từ thư mục root repo `Kronos/` + +```bash +python -m polymarket_bot.main +``` + +### Cách 2: nếu đang đứng trong thư mục `Kronos/polymarket_bot/` + +```bash +python -m main +``` + +## Kiểm tra compile + +### Từ root repo `Kronos/` + +```bash +python -m py_compile polymarket_bot/main.py polymarket_bot/data/collector.py polymarket_bot/model/kronos_adapter.py polymarket_bot/decision/engine.py polymarket_bot/execution/executor.py polymarket_bot/monitoring/logger.py polymarket_bot/config/settings.py +``` + +### Từ `Kronos/polymarket_bot/` + +```bash +python -m py_compile main.py data/collector.py model/kronos_adapter.py decision/engine.py execution/executor.py monitoring/logger.py config/settings.py +``` + +> Mặc định đang chạy chế độ `dry_run=True`, không gửi lệnh thật. diff --git a/polymarket_bot/__init__.py b/polymarket_bot/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/config/__init__.py b/polymarket_bot/config/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/config/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/config/settings.py b/polymarket_bot/config/settings.py new file mode 100644 index 00000000..630e29e1 --- /dev/null +++ b/polymarket_bot/config/settings.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass + + +@dataclass +class RiskConfig: + max_risk_per_trade: float = 0.02 + max_daily_loss: float = 0.05 + min_liquidity: float = 5000.0 + min_probability: float = 0.68 + + +@dataclass +class StrategyConfig: + kelly_fraction_cap: float = 0.25 + fee_rate: float = 0.01 + + +@dataclass +class RuntimeConfig: + dry_run: bool = True + device: str = "cpu" + lookback: int = 256 + pred_len: int = 1 diff --git a/polymarket_bot/data/__init__.py b/polymarket_bot/data/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/data/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/data/collector.py b/polymarket_bot/data/collector.py new file mode 100644 index 00000000..18570087 --- /dev/null +++ b/polymarket_bot/data/collector.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +import random + + +@dataclass +class MarketState: + market_id: str + yes_price: float + no_price: float + liquidity: float + history_yes_price: list[float] + + +class PolyMarketCollector: + """Mock collector; thay bằng PolyMarket API ở bước tích hợp thật.""" + + def fetch_market_state(self, market_id: str, lookback: int = 256) -> MarketState: + base = 0.55 + history = [max(0.01, min(0.99, base + random.uniform(-0.03, 0.03))) for _ in range(lookback)] + yes_price = history[-1] + no_price = 1.0 - yes_price + liquidity = random.uniform(6_000, 20_000) + return MarketState( + market_id=market_id, + yes_price=yes_price, + no_price=no_price, + liquidity=liquidity, + history_yes_price=history, + ) diff --git a/polymarket_bot/decision/__init__.py b/polymarket_bot/decision/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/decision/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/decision/engine.py b/polymarket_bot/decision/engine.py new file mode 100644 index 00000000..d3d8ab32 --- /dev/null +++ b/polymarket_bot/decision/engine.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from polymarket_bot.config.settings import RiskConfig, StrategyConfig + + +@dataclass +class TradeDecision: + should_trade: bool + side: str | None + size_fraction: float + expected_value: float + reason: str + + +class DecisionEngine: + def __init__(self, risk: RiskConfig, strategy: StrategyConfig): + self.risk = risk + self.strategy = strategy + + def evaluate(self, prob_yes: float, yes_price: float, liquidity: float) -> TradeDecision: + if liquidity < self.risk.min_liquidity: + return TradeDecision(False, None, 0.0, 0.0, "liquidity_too_low") + + if prob_yes < self.risk.min_probability: + return TradeDecision(False, None, 0.0, 0.0, "probability_below_threshold") + + b = (1 - yes_price) / yes_price + q = 1 - prob_yes + kelly_f = max(0.0, (b * prob_yes - q) / b) if b > 0 else 0.0 + size = min(kelly_f, self.strategy.kelly_fraction_cap, self.risk.max_risk_per_trade) + + ev = prob_yes * (1 - yes_price) - (1 - prob_yes) * yes_price - self.strategy.fee_rate + if ev <= 0 or size <= 0: + return TradeDecision(False, None, 0.0, ev, "non_positive_ev_or_size") + + return TradeDecision(True, "YES", size, ev, "ok") diff --git a/polymarket_bot/execution/__init__.py b/polymarket_bot/execution/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/execution/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/execution/executor.py b/polymarket_bot/execution/executor.py new file mode 100644 index 00000000..d4cf84c4 --- /dev/null +++ b/polymarket_bot/execution/executor.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import time + + +class ExecutionEngine: + def __init__(self, dry_run: bool = True, max_retries: int = 3): + self.dry_run = dry_run + self.max_retries = max_retries + + def place_order(self, market_id: str, side: str, size_fraction: float) -> dict: + for attempt in range(1, self.max_retries + 1): + try: + if self.dry_run: + return { + "status": "simulated_filled", + "market_id": market_id, + "side": side, + "size_fraction": size_fraction, + "attempt": attempt, + } + # TODO: gọi API thật của PolyMarket tại đây + raise NotImplementedError("Live execution not implemented yet") + except Exception as exc: # retry policy + if attempt == self.max_retries: + return {"status": "failed", "error": str(exc), "attempt": attempt} + time.sleep(0.5 * attempt) + return {"status": "failed", "error": "unknown"} diff --git a/polymarket_bot/main.py b/polymarket_bot/main.py new file mode 100644 index 00000000..c5bee3c9 --- /dev/null +++ b/polymarket_bot/main.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +<<<<<<< codex/create-project-instructions-for-poly-market-finance-47wtli +import sys +from pathlib import Path + + +# Allow running via `python main.py` when cwd is `Kronos/polymarket_bot`. +if __package__ is None or __package__ == "": + repo_root = Path(__file__).resolve().parents[1] + if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) + +======= +>>>>>>> master +from polymarket_bot.config.settings import RiskConfig, RuntimeConfig, StrategyConfig +from polymarket_bot.data.collector import PolyMarketCollector +from polymarket_bot.decision.engine import DecisionEngine +from polymarket_bot.execution.executor import ExecutionEngine +from polymarket_bot.model.kronos_adapter import KronosAdapter +from polymarket_bot.monitoring.logger import TradeLogger + + +def run_once(market_id: str = "demo_market_1") -> dict: + runtime = RuntimeConfig() + collector = PolyMarketCollector() + predictor = KronosAdapter(device=runtime.device) + decision_engine = DecisionEngine(RiskConfig(), StrategyConfig()) + executor = ExecutionEngine(dry_run=runtime.dry_run) + logger = TradeLogger() + + state = collector.fetch_market_state(market_id=market_id, lookback=runtime.lookback) + prob_yes = predictor.predict_yes_probability(state.history_yes_price, pred_len=runtime.pred_len) + decision = decision_engine.evaluate(prob_yes=prob_yes, yes_price=state.yes_price, liquidity=state.liquidity) + + result = { + "market_id": state.market_id, + "prob_yes": prob_yes, + "yes_price": state.yes_price, + "liquidity": state.liquidity, + "decision": decision.__dict__, + } + + if decision.should_trade: + result["execution"] = executor.place_order( + market_id=state.market_id, + side=decision.side or "YES", + size_fraction=decision.size_fraction, + ) + + logger.log(result) + return result + + +if __name__ == "__main__": + print(run_once()) diff --git a/polymarket_bot/model/__init__.py b/polymarket_bot/model/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/model/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/model/kronos_adapter.py b/polymarket_bot/model/kronos_adapter.py new file mode 100644 index 00000000..c68a277f --- /dev/null +++ b/polymarket_bot/model/kronos_adapter.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pandas as pd + +from model import Kronos, KronosPredictor, KronosTokenizer + + +class KronosAdapter: + """Adapter gói inference từ shiyu-coder/Kronos.""" + + def __init__(self, device: str = "cpu", max_context: int = 512): + self.device = device + self.tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base") + self.model = Kronos.from_pretrained("NeoQuasar/Kronos-small") + self.predictor = KronosPredictor(self.model, self.tokenizer, device=device, max_context=max_context) + + def predict_yes_probability(self, price_series: list[float], pred_len: int = 1) -> float: + df = pd.DataFrame({"close": price_series}) + pred = self.predictor.predict(df, x_timestamp=None, pred_len=pred_len) + current = float(price_series[-1]) + next_price = float(pred[0]) if len(pred) else current + + # Heuristic mapping price delta -> probability adjustment + delta = next_price - current + p = max(0.01, min(0.99, current + delta)) + return p diff --git a/polymarket_bot/monitoring/__init__.py b/polymarket_bot/monitoring/__init__.py new file mode 100644 index 00000000..6714d331 --- /dev/null +++ b/polymarket_bot/monitoring/__init__.py @@ -0,0 +1 @@ +"""PolyMarket bot package.""" diff --git a/polymarket_bot/monitoring/logger.py b/polymarket_bot/monitoring/logger.py new file mode 100644 index 00000000..2c207542 --- /dev/null +++ b/polymarket_bot/monitoring/logger.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone + + +class TradeLogger: + def __init__(self, log_path: str = "polymarket_bot_trades.log"): + self.log_path = log_path + + def log(self, payload: dict) -> None: + record = { + "ts": datetime.now(timezone.utc).isoformat(), + **payload, + } + with open(self.log_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n")