Initial commit — BTC 5-minute binary options edge study
Reconstructed strategy engine + execution layer, trained XGBoost models, a manual trading tool, and the research writeup. Paper mode runs keyless over live WebSocket feeds; live trading requires your own wallet. No secrets committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Chainlink Predictor — Data Collector + Independent Trading System
|
||||
|
||||
Collects real-time price data from multiple exchanges + PM (Chainlink) feed,
|
||||
computes SIM price, and trades on Polymarket using its own wallet.
|
||||
|
||||
Usage:
|
||||
cd ~/btc_15m_collab/rewrite
|
||||
source .venv/bin/activate
|
||||
|
||||
# Data collection only (no trading)
|
||||
python3 tools/chainlink_predictor.py
|
||||
|
||||
# Data collection + live trading ($10/trade, $100 capital)
|
||||
python3 tools/chainlink_predictor.py --trade
|
||||
|
||||
# Custom trade size
|
||||
python3 tools/chainlink_predictor.py --trade --max-trade 5
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.predictor.models import SourceName, SourceTick
|
||||
from src.predictor.feeds import ALL_FEEDS
|
||||
from src.predictor.pm_feed import pm_direct_task
|
||||
from src.predictor.orderbook import ALL_ORDERBOOK_FEEDS
|
||||
from src.predictor.collector import Collector
|
||||
from src.predictor.clob_feed import clob_feed_task, clob_feed_15m_task, current_window as clob_window
|
||||
from src.predictor.strategy import PredictorStrategy, TradeSignal
|
||||
from src.predictor.pnl import PnLTracker, Trade
|
||||
# NOTE: PredictorExecutor is imported lazily inside the --trade branch below, so
|
||||
# paper/collect mode never pulls in the live-trading deps (clob client, web3, dotenv).
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_SOURCES = [
|
||||
SourceName.COINBASE,
|
||||
SourceName.KRAKEN,
|
||||
SourceName.BITSTAMP,
|
||||
SourceName.CRYPTOCOMPARE,
|
||||
SourceName.GEMINI,
|
||||
SourceName.OKX,
|
||||
SourceName.BYBIT,
|
||||
SourceName.BINANCE,
|
||||
]
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(description="Chainlink Predictor + Trader")
|
||||
p.add_argument(
|
||||
"--sources",
|
||||
default=",".join(s.value for s in DEFAULT_SOURCES),
|
||||
help="Comma-separated source names",
|
||||
)
|
||||
p.add_argument(
|
||||
"--output-dir",
|
||||
default="data/chainlink_predictor",
|
||||
help="Output directory (default: data/chainlink_predictor)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--cryptocompare-key",
|
||||
default="",
|
||||
help="CryptoCompare API key (optional)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--trade",
|
||||
action="store_true",
|
||||
help="Enable live trading (default: data collection only)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--max-trade",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Max USD per trade (default: $10)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--max-daily-loss",
|
||||
type=float,
|
||||
default=30.0,
|
||||
help="Max daily loss before stopping (default: $30)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--capital",
|
||||
type=float,
|
||||
default=500.0,
|
||||
help="Capital for dynamic sizing (default: $500)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--shares",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Fixed shares per trade (overrides capital sizing). e.g. --shares 10",
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
async def collector_and_strategy_task(
|
||||
tick_queue: asyncio.Queue[SourceTick],
|
||||
shutdown: asyncio.Event,
|
||||
output_dir: str,
|
||||
trading_enabled: bool,
|
||||
max_trade_usd: float,
|
||||
max_daily_loss: float,
|
||||
capital: float = 500.0,
|
||||
fixed_shares: int = 0,
|
||||
):
|
||||
"""Combined collector + strategy evaluation loop."""
|
||||
collector = Collector(output_dir=output_dir)
|
||||
logger.info(f"[collector] started, output={output_dir}")
|
||||
|
||||
# Strategy and executor (only if trading enabled)
|
||||
strategy = None
|
||||
executor = None
|
||||
pnl = None
|
||||
|
||||
# Always create strategy (for paper trade + evaluate)
|
||||
strategy = PredictorStrategy(
|
||||
max_trade_usd=max_trade_usd,
|
||||
max_daily_loss=max_daily_loss,
|
||||
capital=capital,
|
||||
fixed_shares=fixed_shares,
|
||||
)
|
||||
|
||||
if trading_enabled:
|
||||
try:
|
||||
from src.predictor.executor import PredictorExecutor # live-only import
|
||||
executor = PredictorExecutor()
|
||||
pnl = PnLTracker(output_dir=output_dir)
|
||||
balance = await executor.get_balance()
|
||||
logger.info(f"[trading] ENABLED: wallet balance=${balance:.2f}, max_trade=${max_trade_usd}")
|
||||
except Exception as e:
|
||||
logger.error(f"[trading] failed to initialize: {e}")
|
||||
trading_enabled = False
|
||||
|
||||
last_window_id = 0
|
||||
prev_window_pm_a = 0.0 # track pm_a at end of previous window
|
||||
last_settle_check = 0.0
|
||||
|
||||
try:
|
||||
while not shutdown.is_set():
|
||||
try:
|
||||
tick = await asyncio.wait_for(tick_queue.get(), timeout=0.5)
|
||||
# Save pm_a before process_tick (it resets on new window)
|
||||
prev_window_pm_a = collector._pm_a
|
||||
collector.process_tick(tick)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
# Strategy evaluation — every tick for fast cb_flip detection
|
||||
if strategy is None:
|
||||
continue
|
||||
|
||||
# Check for new window — settle previous window
|
||||
window_id = collector.current_window
|
||||
if window_id != last_window_id:
|
||||
# Collect all window IDs that need settlement (current + any backlog)
|
||||
settle_wids = set()
|
||||
if last_window_id > 0:
|
||||
settle_wids.add(last_window_id)
|
||||
if pnl:
|
||||
for t in pnl.pending_trades:
|
||||
settle_wids.add(int(t.window_id))
|
||||
# Also check paper backlog
|
||||
if strategy:
|
||||
for p in strategy._paper_pending:
|
||||
settle_wids.add(int(p.get("window_id", 0)))
|
||||
|
||||
# Query chain result for each window
|
||||
chain_results = {}
|
||||
for wid in settle_wids:
|
||||
if wid <= 0:
|
||||
continue
|
||||
try:
|
||||
result = await asyncio.to_thread(collector.check_settlement, wid)
|
||||
if result:
|
||||
chain_results[wid] = result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
chain_dir = chain_results.get(last_window_id)
|
||||
|
||||
# Settle real trades — only with chain result
|
||||
if pnl and pnl.pending_trades:
|
||||
for trade in list(pnl.pending_trades):
|
||||
trade_wid = int(trade.window_id)
|
||||
trade_chain_dir = chain_results.get(trade_wid)
|
||||
|
||||
if not trade_chain_dir:
|
||||
logger.warning(f"[settle] {trade_wid} no chain result, keeping pending")
|
||||
continue
|
||||
|
||||
# Check actual fill from chain
|
||||
fill_info = None
|
||||
if executor and trade.order_id:
|
||||
try:
|
||||
fill_info = await executor.check_order_filled(trade.order_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if fill_info and fill_info["filled"] == 0:
|
||||
# Not filled — but still record direction correctness for analysis
|
||||
trade.settled = True
|
||||
trade.pnl = 0.0
|
||||
trade.won = False
|
||||
trade.filled = False
|
||||
trade.direction_correct = (trade.direction == trade_chain_dir)
|
||||
trade.chain_direction = trade_chain_dir
|
||||
pnl._write(trade)
|
||||
pnl.pending_trades.remove(trade)
|
||||
logger.info(f"[settle] {trade_wid} {trade.direction} NO FILL (chain={trade_chain_dir}, dir_correct={trade.direction_correct})")
|
||||
elif fill_info and fill_info["filled"] > 0:
|
||||
if trade.settled:
|
||||
continue
|
||||
# Use actual fill amount AND avg fill price from chain
|
||||
actual_shares = fill_info["filled"]
|
||||
actual_price = fill_info.get("avg_fill_price", trade.entry_price)
|
||||
if actual_shares != trade.shares:
|
||||
logger.info(f"[settle] partial fill: ordered={trade.shares} filled={actual_shares:.2f}")
|
||||
if abs(actual_price - trade.entry_price) > 0.001:
|
||||
logger.info(f"[settle] price improved: limit={trade.entry_price:.4f} fill={actual_price:.4f}")
|
||||
trade.shares = actual_shares
|
||||
trade.entry_price = round(actual_price, 4)
|
||||
trade.cost = round(actual_shares * actual_price, 2)
|
||||
won = trade.direction == trade_chain_dir
|
||||
trade.filled = True
|
||||
trade.direction_correct = won
|
||||
trade.chain_direction = trade_chain_dir
|
||||
settled_pnl = pnl.record_settlement(
|
||||
window_id=str(trade_wid),
|
||||
direction=trade.direction,
|
||||
won=won,
|
||||
order_id=trade.order_id,
|
||||
)
|
||||
if settled_pnl is not None:
|
||||
strategy.record_pnl(settled_pnl)
|
||||
# Daily-loss circuit breaker (shared across all strategies)
|
||||
strategy.check_daily_loss_circuit_breaker()
|
||||
# Lock cooldown after loss (Gate 2)
|
||||
if 'lock' in trade.reason and actual_shares > 0:
|
||||
strategy.record_lock_result(won)
|
||||
logger.info(f"[settle] {trade_wid} chain→{trade_chain_dir}, trade={trade.direction} {'WIN' if won else 'LOSS'} (filled={actual_shares:.2f})")
|
||||
else:
|
||||
# fill_info is None (API error) — retry up to 10 times then give up
|
||||
trade.settle_retries = getattr(trade, 'settle_retries', 0) + 1
|
||||
if trade.settle_retries >= 10:
|
||||
logger.warning(f"[settle] {trade_wid} giving up after {trade.settle_retries} retries, marking as NFIL")
|
||||
trade.settled = True
|
||||
trade.pnl = 0.0
|
||||
trade.won = False
|
||||
trade.filled = False
|
||||
trade.direction_correct = False
|
||||
trade.chain_direction = ""
|
||||
pnl._write(trade)
|
||||
pnl.pending_trades.remove(trade)
|
||||
else:
|
||||
logger.warning(f"[settle] {trade_wid} fill check failed (retry {trade.settle_retries}/10)")
|
||||
continue
|
||||
|
||||
# Settle paper trade — only with chain result, no fallback
|
||||
# Settle paper trades using chain results
|
||||
if strategy:
|
||||
strategy.settle_paper(prev_window_pm_a, chain_results)
|
||||
|
||||
last_window_id = window_id
|
||||
strategy.new_window(window_id)
|
||||
if executor:
|
||||
executor.new_window(window_id)
|
||||
|
||||
# Evaluate strategy (returns list of signals)
|
||||
try:
|
||||
signals = strategy.evaluate(collector)
|
||||
except Exception as e:
|
||||
logger.error(f"[strategy] evaluate error: {e}", exc_info=True)
|
||||
signals = []
|
||||
|
||||
if not signals and collector._pm_left_sec < 35:
|
||||
_left_int = int(collector._pm_left_sec)
|
||||
if _left_int != getattr(strategy, '_last_log_left', -1):
|
||||
strategy._last_log_left = _left_int
|
||||
print(f" [strategy] left={_left_int}s no signal", flush=True)
|
||||
|
||||
for sig in signals:
|
||||
if not sig.should_trade or not executor or not executor.can_trade:
|
||||
continue
|
||||
|
||||
G = "\033[92m"
|
||||
R = "\033[91m"
|
||||
B = "\033[1m"
|
||||
RST = "\033[0m"
|
||||
color = G if sig.direction == "UP" else R
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {B}{color}TRADE SIGNAL [{sig.tier}]: {sig.direction}{RST}")
|
||||
print(f" {sig.reason}")
|
||||
print(f" CLOB ask={sig.price:.2f} Amount: ${sig.amount_usd:.2f} → {int(sig.amount_usd / sig.price)} shares")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
try:
|
||||
order_id = await executor.place_buy(
|
||||
token_id=sig.token_id,
|
||||
price=sig.price,
|
||||
amount_usd=sig.amount_usd,
|
||||
expiration_sec=max(int(collector._pm_left_sec) + 60, 90),
|
||||
)
|
||||
except Exception as _ex:
|
||||
order_id = None
|
||||
print(f" [executor] FAILED: {_ex}", flush=True)
|
||||
logger.error(f"[executor] place_buy exception: {_ex}")
|
||||
|
||||
if order_id is None:
|
||||
print(f" [executor] order_id=None — order not placed", flush=True)
|
||||
|
||||
if order_id and pnl:
|
||||
shares = int(sig.amount_usd / sig.price)
|
||||
trade = Trade(
|
||||
ts=int(time.time()),
|
||||
window_id=str(window_id),
|
||||
direction=sig.direction,
|
||||
entry_price=sig.price,
|
||||
shares=shares,
|
||||
cost=round(shares * sig.price, 2),
|
||||
token_id=sig.token_id,
|
||||
order_id=order_id,
|
||||
sim_a=sig.sim_a,
|
||||
pm_a=sig.pm_a,
|
||||
source_agreement=sig.source_agreement,
|
||||
ob_depth_btc=sig.ob_depth_btc,
|
||||
reason=sig.reason,
|
||||
)
|
||||
pnl.record_entry(trade)
|
||||
strategy.mark_traded()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
if collector.window_ticks:
|
||||
collector._flush_window()
|
||||
collector.close()
|
||||
if pnl:
|
||||
logger.info(f"[pnl] {pnl.summary()}")
|
||||
pnl.close()
|
||||
logger.info(f"[collector] stopped. {collector.total_windows} windows processed.")
|
||||
|
||||
|
||||
async def main():
|
||||
args = parse_args()
|
||||
|
||||
# Parse source names
|
||||
source_names = []
|
||||
for s in args.sources.split(","):
|
||||
s = s.strip().lower()
|
||||
try:
|
||||
source_names.append(SourceName(s))
|
||||
except ValueError:
|
||||
logger.warning(f"Unknown source: {s}, skipping")
|
||||
|
||||
logger.info(f"Sources: {[s.value for s in source_names]}")
|
||||
logger.info(f"Output dir: {args.output_dir}")
|
||||
logger.info(f"Trading: {'ENABLED' if args.trade else 'DISABLED'}")
|
||||
|
||||
# Shared queue and shutdown event
|
||||
tick_queue: asyncio.Queue[SourceTick] = asyncio.Queue(maxsize=5000)
|
||||
shutdown = asyncio.Event()
|
||||
|
||||
# Handle SIGINT/SIGTERM
|
||||
loop = asyncio.get_event_loop()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(sig, lambda: shutdown.set())
|
||||
|
||||
# Build task list
|
||||
tasks = []
|
||||
|
||||
# PM direct feed
|
||||
tasks.append(asyncio.create_task(
|
||||
pm_direct_task(tick_queue, shutdown),
|
||||
name="pm_direct",
|
||||
))
|
||||
|
||||
# Exchange feeds
|
||||
for src in source_names:
|
||||
feed_fn = ALL_FEEDS.get(src)
|
||||
if feed_fn is None:
|
||||
continue
|
||||
if src == SourceName.CRYPTOCOMPARE:
|
||||
tasks.append(asyncio.create_task(
|
||||
feed_fn(tick_queue, shutdown, api_key=args.cryptocompare_key),
|
||||
name=f"feed_{src.value}",
|
||||
))
|
||||
else:
|
||||
tasks.append(asyncio.create_task(
|
||||
feed_fn(tick_queue, shutdown),
|
||||
name=f"feed_{src.value}",
|
||||
))
|
||||
|
||||
# Order book feeds
|
||||
for ob_src, ob_fn in ALL_ORDERBOOK_FEEDS.items():
|
||||
tasks.append(asyncio.create_task(
|
||||
ob_fn(shutdown),
|
||||
name=f"ob_{ob_src.value}",
|
||||
))
|
||||
|
||||
# CLOB feed (for Polymarket token prices — needed for trading + paper trade)
|
||||
tasks.append(asyncio.create_task(
|
||||
clob_feed_task(shutdown),
|
||||
name="clob_feed",
|
||||
))
|
||||
# 15m CLOB feed (parallel) — exposes current_window_15m for arb/magic combo sum
|
||||
tasks.append(asyncio.create_task(
|
||||
clob_feed_15m_task(shutdown),
|
||||
name="clob_feed_15m",
|
||||
))
|
||||
|
||||
# Collector + Strategy + Executor
|
||||
tasks.append(asyncio.create_task(
|
||||
collector_and_strategy_task(
|
||||
tick_queue, shutdown, args.output_dir,
|
||||
trading_enabled=args.trade,
|
||||
max_trade_usd=args.max_trade,
|
||||
max_daily_loss=args.max_daily_loss,
|
||||
capital=args.capital,
|
||||
fixed_shares=args.shares,
|
||||
),
|
||||
name="collector_strategy",
|
||||
))
|
||||
|
||||
mode = "COLLECT + TRADE" if args.trade else "COLLECT ONLY"
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f" Chainlink Predictor — {mode}")
|
||||
print(f" Sources: {', '.join(s.value for s in source_names)}")
|
||||
print(f" PM: direct WebSocket")
|
||||
if args.trade:
|
||||
print(f" Trading: ${args.max_trade:.0f}/trade, capital=${args.capital:.0f}, daily limit -${args.max_daily_loss:.0f}")
|
||||
print(f" Output: {args.output_dir}")
|
||||
print(" Press Ctrl+C to stop")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Wait for shutdown
|
||||
await shutdown.wait()
|
||||
logger.info("Shutdown signal received")
|
||||
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("All tasks stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,665 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Quick trade — press U/D to instantly buy. Watch the chainlink_watch for prices.
|
||||
|
||||
!! LIVE, REAL MONEY. Every keystroke places a REAL order on Polymarket with the
|
||||
wallet in src/predictor/.env. There is NO paper mode and NO confirmation dialog.
|
||||
Provided as-is and UNAUDITED — read the code first. Needs the collector
|
||||
(tools/chainlink_predictor.py) running for live prices. Use at your own risk.
|
||||
|
||||
Keys:
|
||||
u → Buy UP 10sh d → Buy DOWN 10sh (market ask + 0.02)
|
||||
! → Buy UP @0.01 $ → Buy DOWN @0.01 (Shift+1/4, limit order)
|
||||
@ → Buy UP @0.02 % → Buy DOWN @0.02 (Shift+2/5)
|
||||
# → Buy UP @0.03 ^ → Buy DOWN @0.03 (Shift+3/6)
|
||||
i → Sell UP o → Sell DOWN (sells most expensive first)
|
||||
b → Balance r → Redeem w → Refresh window
|
||||
q → Quit
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tty
|
||||
import termios
|
||||
import subprocess
|
||||
import select
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src', 'predictor', '.env'), override=True)
|
||||
|
||||
import aiohttp
|
||||
from web3 import Web3
|
||||
from py_clob_client_v2.client import ClobClient
|
||||
from py_clob_client_v2.clob_types import OrderArgs, OrderType, PartialCreateOrderOptions
|
||||
from py_clob_client_v2.order_builder.constants import BUY
|
||||
import httpx
|
||||
import py_clob_client_v2.http_helpers.helpers as _clob_helpers
|
||||
|
||||
G = "\033[92m"
|
||||
R = "\033[91m"
|
||||
B = "\033[1m"
|
||||
C = "\033[96m"
|
||||
Y = "\033[93m"
|
||||
DIM = "\033[2m"
|
||||
RST = "\033[0m"
|
||||
|
||||
BUCKET_SEC = 300
|
||||
TRADE_SHARES = 10 # fixed 10 shares per click
|
||||
MAX_ASK = 0.99
|
||||
SNAPSHOT_FILE = "data/chainlink_predictor/snapshots.jsonl"
|
||||
MANUAL_TRADES_FILE = "data/chainlink_predictor/manual_trades.jsonl"
|
||||
|
||||
|
||||
class QuickTrader:
|
||||
def __init__(self):
|
||||
_clob_helpers._http_client = httpx.Client(http2=False, timeout=30)
|
||||
key = os.environ.get("PREDICTOR_WALLET_KEY", "")
|
||||
address = os.environ.get("PREDICTOR_WALLET_ADDRESS", "")
|
||||
self.address = address
|
||||
self.client = ClobClient("https://clob.polymarket.com", key=key, chain_id=137, signature_type=0, funder=address)
|
||||
self.client.set_api_creds(self.client.create_or_derive_api_key())
|
||||
|
||||
self.token_up = ""
|
||||
self.token_down = ""
|
||||
self.window_id = 0
|
||||
self._preflight_cache: dict[str, tuple] = {}
|
||||
self.pending_trades: list[dict] = []
|
||||
|
||||
# Tail snapshots for live prices
|
||||
self._snap_proc = None
|
||||
self._last_clob = {}
|
||||
self._last_left = 0.0
|
||||
self._last_pm_a = 0.0
|
||||
self._last_sim_a = 0.0
|
||||
if os.path.exists(SNAPSHOT_FILE):
|
||||
self._snap_proc = subprocess.Popen(
|
||||
["tail", "-F", "-n", "1", SNAPSHOT_FILE],
|
||||
stdout=subprocess.PIPE, text=True,
|
||||
)
|
||||
|
||||
def read_snapshot(self):
|
||||
if not self._snap_proc:
|
||||
return
|
||||
while True:
|
||||
ready, _, _ = select.select([self._snap_proc.stdout], [], [], 0)
|
||||
if not ready:
|
||||
break
|
||||
line = self._snap_proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
try:
|
||||
d = json.loads(line)
|
||||
self._last_clob = d.get("clob", {})
|
||||
self._last_left = d.get("left_sec", 0)
|
||||
self._last_pm_a = d.get("pm_a", 0)
|
||||
sim = d.get("sim", 0)
|
||||
pm = d.get("pm", 0)
|
||||
pm_a = d.get("pm_a", 0)
|
||||
ptb = pm - pm_a if pm_a else pm
|
||||
self._last_sim_a = sim - ptb if sim > 0 and ptb > 0 else 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def discover(self):
|
||||
now = time.time()
|
||||
base = int(now // BUCKET_SEC) * BUCKET_SEC
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for ts in [base, base + BUCKET_SEC, base - BUCKET_SEC]:
|
||||
slug = f"btc-updown-5m-{ts}"
|
||||
try:
|
||||
async with session.get("https://gamma-api.polymarket.com/events",
|
||||
params={"slug": slug}, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
||||
if resp.status != 200:
|
||||
continue
|
||||
data = await resp.json()
|
||||
if not data:
|
||||
continue
|
||||
market = data[0].get("markets", [{}])[0]
|
||||
raw_ids = market.get("clobTokenIds")
|
||||
token_ids = json.loads(raw_ids) if isinstance(raw_ids, str) else raw_ids
|
||||
if token_ids and len(token_ids) >= 2:
|
||||
self.token_up = token_ids[0]
|
||||
self.token_down = token_ids[1]
|
||||
self.window_id = ts
|
||||
await self._warm(self.token_up)
|
||||
await self._warm(self.token_down)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
async def _warm(self, token_id: str):
|
||||
if token_id in self._preflight_cache:
|
||||
return
|
||||
try:
|
||||
tick = await asyncio.to_thread(self.client.get_tick_size, token_id)
|
||||
neg = await asyncio.to_thread(self.client.get_neg_risk, token_id)
|
||||
self._preflight_cache[token_id] = (tick, neg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def buy(self, direction: str) -> str:
|
||||
token_id = self.token_up if direction == "UP" else self.token_down
|
||||
if not token_id:
|
||||
return f"{R}No token — press W to refresh{RST}"
|
||||
|
||||
self.read_snapshot()
|
||||
ua = self._last_clob.get("up_ask", 0)
|
||||
da = self._last_clob.get("down_ask", 0)
|
||||
|
||||
if direction == "UP":
|
||||
price = ua
|
||||
else:
|
||||
price = da
|
||||
if price <= 0:
|
||||
other = da if direction == "UP" else ua
|
||||
if other > 0:
|
||||
price = round(1.0 - other, 2)
|
||||
|
||||
if price <= 0:
|
||||
return f"{R}No price{RST}"
|
||||
if price > MAX_ASK:
|
||||
return f"{R}Too expensive: {price:.2f}{RST}"
|
||||
|
||||
# Add $0.02 buffer for fill, cap at MAX_ASK
|
||||
limit = min(round(price, 2), MAX_ASK)
|
||||
qty = TRADE_SHARES
|
||||
# Auto-adjust qty to meet $1 minimum
|
||||
if qty * limit < 1.0:
|
||||
qty = math.ceil(1.0 / limit)
|
||||
cost = round(qty * limit, 2)
|
||||
|
||||
try:
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
if not preflight:
|
||||
await self._warm(token_id)
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
tick_size, neg_risk = preflight
|
||||
left = (self.window_id + BUCKET_SEC) - time.time()
|
||||
|
||||
exp = int(time.time()) + 90 # GTD requires at least now+60s
|
||||
|
||||
order_args = OrderArgs(price=limit, size=float(qty), side=BUY,
|
||||
token_id=token_id, expiration=str(exp))
|
||||
options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk)
|
||||
|
||||
t0 = time.monotonic()
|
||||
signed = await asyncio.to_thread(self.client.create_order, order_args, options)
|
||||
result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD)
|
||||
latency = (time.monotonic() - t0) * 1000
|
||||
|
||||
oid_full = result.get("orderID", "")
|
||||
trade = {
|
||||
"ts": int(time.time()),
|
||||
"window_id": self.window_id,
|
||||
"order_id": oid_full,
|
||||
"direction": direction,
|
||||
"qty": qty,
|
||||
"price": limit,
|
||||
"display_price": price,
|
||||
"cost": round(qty * limit, 2),
|
||||
"left_sec": round(left, 1),
|
||||
"pm_a": self._last_pm_a if hasattr(self, '_last_pm_a') else 0,
|
||||
"settled": False,
|
||||
"won": False,
|
||||
"pnl": 0,
|
||||
}
|
||||
self.pending_trades.append(trade)
|
||||
with open(MANUAL_TRADES_FILE, "a") as f:
|
||||
f.write(json.dumps(trade) + "\n")
|
||||
|
||||
color = G if direction == "UP" else R
|
||||
return f"{color}{B}{direction}{RST} {qty}@{price:.2f} (limit {limit:.2f}) = ${qty*limit:.2f} | {latency:.0f}ms wid={self.window_id}"
|
||||
except Exception as e:
|
||||
return f"{R}Failed: {e}{RST}"
|
||||
|
||||
async def buy_fixed(self, direction: str, price: float, qty: int) -> str:
|
||||
"""Buy at a fixed price and quantity (for cheap lottery tickets)."""
|
||||
token_id = self.token_up if direction == "UP" else self.token_down
|
||||
if not token_id:
|
||||
return f"{R}No token — press W to refresh{RST}"
|
||||
|
||||
# Auto-adjust qty to meet $1 minimum
|
||||
if qty * price < 1.0:
|
||||
qty = math.ceil(1.0 / price)
|
||||
cost = round(qty * price, 2)
|
||||
|
||||
try:
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
if not preflight:
|
||||
await self._warm(token_id)
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
tick_size, neg_risk = preflight
|
||||
|
||||
left = (self.window_id + BUCKET_SEC) - time.time()
|
||||
# GTD with minimum 90s expiration (API requires at least 60s)
|
||||
# Won't actually last — window settles and tokens become worthless
|
||||
exp = int(time.time()) + 90
|
||||
|
||||
order_args = OrderArgs(price=price, size=float(qty), side=BUY,
|
||||
token_id=token_id, expiration=str(exp))
|
||||
options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk)
|
||||
|
||||
t0 = time.monotonic()
|
||||
signed = await asyncio.to_thread(self.client.create_order, order_args, options)
|
||||
result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD)
|
||||
latency = (time.monotonic() - t0) * 1000
|
||||
|
||||
oid_full = result.get("orderID", "")
|
||||
trade = {
|
||||
"ts": int(time.time()),
|
||||
"window_id": self.window_id,
|
||||
"order_id": oid_full,
|
||||
"direction": direction,
|
||||
"qty": qty,
|
||||
"price": price,
|
||||
"display_price": price,
|
||||
"cost": cost,
|
||||
"left_sec": round(left, 1),
|
||||
"pm_a": self._last_pm_a if hasattr(self, '_last_pm_a') else 0,
|
||||
"settled": False,
|
||||
"won": False,
|
||||
"pnl": 0,
|
||||
}
|
||||
self.pending_trades.append(trade)
|
||||
with open(MANUAL_TRADES_FILE, "a") as f:
|
||||
f.write(json.dumps(trade) + "\n")
|
||||
|
||||
color = G if direction == "UP" else R
|
||||
return f"{color}{B}{direction}{RST} {qty}@{price:.2f} = ${cost:.2f} | {latency:.0f}ms wid={self.window_id}"
|
||||
except Exception as e:
|
||||
return f"{R}Failed: {e}{RST}"
|
||||
|
||||
async def sell(self, sell_dir: str = "") -> str:
|
||||
"""Sell tokens. sell_dir='UP'/'DOWN' to choose, or '' to sell most recent."""
|
||||
trade = None
|
||||
if sell_dir:
|
||||
# Find matching trade — most recent first
|
||||
matching = [t for t in self.pending_trades if t["direction"] == sell_dir]
|
||||
if matching:
|
||||
trade = matching[-1]
|
||||
else:
|
||||
# No pending trade, but check chain balance directly (e.g. from cb_lead_live)
|
||||
direction = sell_dir
|
||||
token_id = self.token_up if direction == "UP" else self.token_down
|
||||
if token_id:
|
||||
trade = {"direction": direction, "price": 0, "ts": int(time.time()), "_chain_only": True}
|
||||
elif self.pending_trades:
|
||||
trade = self.pending_trades[-1]
|
||||
|
||||
if not trade:
|
||||
return f"{R}No position to sell{RST}"
|
||||
|
||||
direction = trade["direction"]
|
||||
token_id = self.token_up if direction == "UP" else self.token_down
|
||||
if not token_id:
|
||||
return f"{R}No token{RST}"
|
||||
|
||||
# Check on-chain balance for this token
|
||||
try:
|
||||
from web3 import Web3 as _W3
|
||||
rpc = os.environ.get("POLYGON_RPC_URL", "")
|
||||
w3 = _W3(_W3.HTTPProvider(rpc))
|
||||
eoa = _W3.to_checksum_address(self.address)
|
||||
CTF = _W3.to_checksum_address("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045")
|
||||
ctf = w3.eth.contract(address=CTF, abi=[
|
||||
{"name": "balanceOf", "type": "function", "stateMutability": "view",
|
||||
"inputs": [{"name": "account", "type": "address"}, {"name": "id", "type": "uint256"}],
|
||||
"outputs": [{"type": "uint256"}]}])
|
||||
bal = ctf.functions.balanceOf(eoa, int(token_id)).call()
|
||||
shares = bal / 1e6
|
||||
except Exception as e:
|
||||
return f"{R}Balance check failed: {e}{RST}"
|
||||
|
||||
if shares < 1:
|
||||
return f"{R}No shares to sell (balance={shares:.2f}){RST}"
|
||||
|
||||
# Get best bid
|
||||
try:
|
||||
book = await asyncio.to_thread(self.client.get_order_book, token_id)
|
||||
# V2: get_order_book returns dict (was OrderBookSummary in V1)
|
||||
bids = (book.get("bids") or []) if isinstance(book, dict) else (getattr(book, "bids", None) or [])
|
||||
if not bids:
|
||||
return f"{R}No bids available{RST}"
|
||||
first = bids[0]
|
||||
best_bid = float(first["price"]) if isinstance(first, dict) else float(first.price)
|
||||
except Exception as e:
|
||||
return f"{R}Order book error: {e}{RST}"
|
||||
|
||||
if best_bid <= 0:
|
||||
return f"{R}No bid price{RST}"
|
||||
|
||||
# Place SELL order
|
||||
try:
|
||||
from py_clob_client_v2.order_builder.constants import SELL
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
if not preflight:
|
||||
await self._warm(token_id)
|
||||
preflight = self._preflight_cache.get(token_id)
|
||||
tick_size, neg_risk = preflight
|
||||
|
||||
sell_qty = math.floor(shares * 100) / 100 # truncate to 2dp
|
||||
exp = int(time.time()) + 90
|
||||
|
||||
order_args = OrderArgs(price=best_bid, size=sell_qty, side=SELL,
|
||||
token_id=token_id, expiration=str(exp))
|
||||
options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk)
|
||||
|
||||
t0 = time.monotonic()
|
||||
signed = await asyncio.to_thread(self.client.create_order, order_args, options)
|
||||
result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD)
|
||||
latency = (time.monotonic() - t0) * 1000
|
||||
order_id = result.get("orderID", "")
|
||||
|
||||
# API confirm — get actual fill price from trades history
|
||||
import asyncio as _aio
|
||||
await _aio.sleep(2)
|
||||
matched = 0
|
||||
status = ""
|
||||
actual_price = best_bid
|
||||
try:
|
||||
raw_client = getattr(self.client, '_client', None) or self.client
|
||||
order_info = await asyncio.to_thread(raw_client.get_order, order_id)
|
||||
if order_info:
|
||||
matched = float(order_info.get("size_matched", 0))
|
||||
status = order_info.get("status", "")
|
||||
|
||||
# get_order.price is the LIMIT price, not fill price
|
||||
# Use get_trades to find actual fill price
|
||||
from py_clob_client_v2.clob_types import TradeParams
|
||||
recent_trades = await asyncio.to_thread(
|
||||
raw_client.get_trades,
|
||||
TradeParams(asset_id=token_id, after=int(time.time()) - 30)
|
||||
)
|
||||
if recent_trades:
|
||||
for tr in recent_trades:
|
||||
if abs(float(tr.get("size", 0)) - matched) < 1:
|
||||
actual_price = float(tr.get("price", best_bid))
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if matched <= 0:
|
||||
return f"{R}SELL NOT FILLED{RST} {direction} {sell_qty}@{best_bid:.2f} status={status} | {latency:.0f}ms"
|
||||
|
||||
sell_value = matched * actual_price
|
||||
buy_price = trade.get("price", 0)
|
||||
buy_cost = trade.get("cost", matched * buy_price)
|
||||
pnl = sell_value - buy_cost
|
||||
|
||||
sell_record = {
|
||||
"ts": int(time.time()),
|
||||
"window_id": trade["window_id"],
|
||||
"order_id": order_id,
|
||||
"action": "SELL",
|
||||
"direction": direction,
|
||||
"qty": matched,
|
||||
"sell_price": actual_price,
|
||||
"limit_price": best_bid,
|
||||
"buy_price": buy_price,
|
||||
"pnl": round(pnl, 2),
|
||||
"api_status": status,
|
||||
"api_matched": matched,
|
||||
}
|
||||
with open(MANUAL_TRADES_FILE, "a") as f:
|
||||
f.write(json.dumps(sell_record) + "\n")
|
||||
|
||||
if trade in self.pending_trades:
|
||||
self.pending_trades.remove(trade)
|
||||
|
||||
color = G if pnl >= 0 else R
|
||||
return (
|
||||
f"{G}✓ SOLD{RST} {direction} {matched:.0f}@{actual_price:.2f} "
|
||||
f"(bought @{buy_price:.2f}) "
|
||||
f"pnl={color}${pnl:+.2f}{RST} | {latency:.0f}ms"
|
||||
)
|
||||
except Exception as e:
|
||||
return f"{R}Sell failed: {e}{RST}"
|
||||
|
||||
async def check_results(self):
|
||||
import requests
|
||||
settled = []
|
||||
now = time.time()
|
||||
for trade in list(self.pending_trades):
|
||||
# Step 1: verify fill via API (once, a few seconds after order)
|
||||
order_id = trade.get("order_id", "")
|
||||
if order_id and not trade.get("fill_verified") and now - trade.get("ts", 0) > 3:
|
||||
try:
|
||||
raw_client = getattr(self.client, '_client', None) or self.client
|
||||
order = await asyncio.to_thread(raw_client.get_order, order_id)
|
||||
if order:
|
||||
matched = float(order.get("size_matched", 0))
|
||||
status = order.get("status", "")
|
||||
trade["fill_verified"] = True
|
||||
trade["order_status"] = status
|
||||
trade["matched"] = matched
|
||||
if matched > 0:
|
||||
trade["filled"] = True
|
||||
trade["fill_qty"] = matched
|
||||
settled.append(f"{G}✓ FILLED{RST} {trade['direction']} {matched:.0f}@{trade['price']:.2f} (status={status})")
|
||||
elif status in ("EXPIRED", "CANCELLED"):
|
||||
trade["filled"] = False
|
||||
settled.append(f"{R}✗ NOT FILLED{RST} {trade['direction']} @{trade['price']:.2f} — {status}")
|
||||
self.pending_trades.remove(trade)
|
||||
# else LIVE/MATCHED — check again later
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Step 2: settle after window ends (window_id + 300s + 30s grace)
|
||||
window_end = trade["window_id"] + BUCKET_SEC + 30
|
||||
if now < window_end:
|
||||
continue
|
||||
# Skip settlement if we know it didn't fill
|
||||
if trade.get("fill_verified") and not trade.get("filled"):
|
||||
continue
|
||||
|
||||
slug = f"btc-updown-5m-{trade['window_id']}"
|
||||
try:
|
||||
r = requests.get("https://gamma-api.polymarket.com/events",
|
||||
params={"slug": slug}, timeout=5)
|
||||
data = r.json()
|
||||
if not data:
|
||||
continue
|
||||
market = data[0].get("markets", [{}])[0]
|
||||
prices = market.get("outcomePrices", "")
|
||||
if isinstance(prices, str) and prices:
|
||||
prices = json.loads(prices)
|
||||
if prices and len(prices) >= 2:
|
||||
up_p = float(prices[0])
|
||||
# Only settle when price is definitively 0 or 1
|
||||
is_settled = up_p >= 0.99 or up_p <= 0.01
|
||||
if is_settled:
|
||||
actual = "UP" if up_p > 0.5 else "DOWN"
|
||||
won = trade["direction"] == actual
|
||||
fill_qty = trade.get("fill_qty", trade["qty"])
|
||||
fill_price = trade.get("price", 0)
|
||||
pnl = fill_qty * (1.0 - fill_price) if won else -fill_qty * fill_price
|
||||
color = G if won else R
|
||||
w = "WIN" if won else "LOSS"
|
||||
filled_str = f" (filled {fill_qty:.0f})" if trade.get("fill_verified") else ""
|
||||
settled.append(f"{color}{B}{w}{RST} {trade['direction']} {fill_qty:.0f}@{fill_price:.2f} pnl={color}${pnl:+.2f}{RST}{filled_str}")
|
||||
trade["settled"] = True
|
||||
trade["won"] = won
|
||||
trade["pnl"] = round(pnl, 2)
|
||||
trade["actual"] = actual
|
||||
with open(MANUAL_TRADES_FILE, "a") as f:
|
||||
f.write(json.dumps(trade) + "\n")
|
||||
self.pending_trades.remove(trade)
|
||||
except Exception:
|
||||
pass
|
||||
return settled
|
||||
|
||||
async def get_balance(self) -> float:
|
||||
rpc = os.environ.get("POLYGON_RPC_URL", "")
|
||||
w3 = Web3(Web3.HTTPProvider(rpc))
|
||||
usdc = w3.eth.contract(
|
||||
address=Web3.to_checksum_address("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"),
|
||||
abi=[{"name": "balanceOf", "type": "function", "stateMutability": "view",
|
||||
"inputs": [{"name": "account", "type": "address"}],
|
||||
"outputs": [{"name": "", "type": "uint256"}]}],
|
||||
)
|
||||
bal = usdc.functions.balanceOf(Web3.to_checksum_address(self.address)).call()
|
||||
return bal / 1e6
|
||||
|
||||
def cleanup(self):
|
||||
if self._snap_proc:
|
||||
self._snap_proc.terminate()
|
||||
|
||||
|
||||
async def main():
|
||||
trader = QuickTrader()
|
||||
print(f"{B}Quick Trade{RST} — initializing...")
|
||||
await trader.discover()
|
||||
bal = await trader.get_balance()
|
||||
print(f"{B}Quick Trade{RST} — READY Balance: {G}${bal:.2f}{RST}")
|
||||
print(f" {G}U{RST}=Buy UP 5 shares {R}D{RST}=Buy DOWN 5 shares (press multiple times to add)")
|
||||
print(f" {C}B{RST}=Balance {C}R{RST}=Redeem {C}Q{RST}=Quit\n")
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
|
||||
_window_count = 0 # count windows for auto-redeem
|
||||
_last_redeem_wid = 0
|
||||
|
||||
try:
|
||||
tty.setcbreak(fd)
|
||||
while True:
|
||||
# Read snapshots in background
|
||||
trader.read_snapshot()
|
||||
|
||||
# Auto-refresh window
|
||||
left = (trader.window_id + BUCKET_SEC) - time.time()
|
||||
if left < -5:
|
||||
await trader.discover()
|
||||
_window_count += 1
|
||||
print(f" {DIM}[new window wid={trader.window_id}]{RST}")
|
||||
|
||||
# Auto-redeem disabled — conflicts with manual trading nonce
|
||||
# Press R to redeem manually
|
||||
if False and _window_count % 3 == 0 and trader.window_id != _last_redeem_wid:
|
||||
_last_redeem_wid = trader.window_id
|
||||
try:
|
||||
from src.predictor.executor import PredictorExecutor
|
||||
executor = PredictorExecutor()
|
||||
now = int(time.time())
|
||||
base = now - (now % BUCKET_SEC)
|
||||
total = 0
|
||||
for i in range(20):
|
||||
wid = str(base - i * BUCKET_SEC)
|
||||
delta = await executor.auto_redeem(f"btc-updown-5m-{wid}")
|
||||
if delta > 0:
|
||||
total += delta
|
||||
# Also redeem pending manual trades
|
||||
for t in trader.pending_trades:
|
||||
wid = str(t["window_id"])
|
||||
delta = await executor.auto_redeem(f"btc-updown-5m-{wid}")
|
||||
if delta > 0:
|
||||
total += delta
|
||||
if total > 0:
|
||||
print(f" {G}[auto-redeem +${total:.2f}]{RST}")
|
||||
except Exception as e:
|
||||
pass # silent fail
|
||||
|
||||
# Check results
|
||||
if trader.pending_trades:
|
||||
results = await trader.check_results()
|
||||
for r in results:
|
||||
print(f" {r}")
|
||||
|
||||
# Non-blocking key check
|
||||
ready, _, _ = select.select([sys.stdin], [], [], 0.5)
|
||||
if not ready:
|
||||
continue
|
||||
|
||||
key = sys.stdin.read(1)
|
||||
|
||||
if key in ("q", "Q", "\x03"):
|
||||
print("\n Bye.")
|
||||
break
|
||||
elif key in ("u", "U"):
|
||||
result = await trader.buy("UP")
|
||||
print(f" {result}")
|
||||
elif key in ("d", "D"):
|
||||
result = await trader.buy("DOWN")
|
||||
print(f" {result}")
|
||||
elif key in ("b", "B"):
|
||||
bal = await trader.get_balance()
|
||||
print(f" Balance: {G}${bal:.2f}{RST}")
|
||||
elif key == "!":
|
||||
result = await trader.buy_fixed("UP", 0.01, 100)
|
||||
print(f" {result}")
|
||||
elif key == "$":
|
||||
result = await trader.buy_fixed("DOWN", 0.01, 100)
|
||||
print(f" {result}")
|
||||
elif key == "@":
|
||||
result = await trader.buy_fixed("UP", 0.02, 50)
|
||||
print(f" {result}")
|
||||
elif key == "%":
|
||||
result = await trader.buy_fixed("DOWN", 0.02, 50)
|
||||
print(f" {result}")
|
||||
elif key == "#":
|
||||
result = await trader.buy_fixed("UP", 0.03, 35)
|
||||
print(f" {result}")
|
||||
elif key == "^":
|
||||
result = await trader.buy_fixed("DOWN", 0.03, 35)
|
||||
print(f" {result}")
|
||||
elif key == "i":
|
||||
result = await trader.sell("UP")
|
||||
print(f" {result}")
|
||||
elif key == "o":
|
||||
result = await trader.sell("DOWN")
|
||||
print(f" {result}")
|
||||
elif key in ("w", "W"):
|
||||
await trader.discover()
|
||||
print(f" Window refreshed: {trader.window_id}")
|
||||
elif key in ("r", "R"):
|
||||
print(f" Redeeming...")
|
||||
from src.predictor.executor import PredictorExecutor
|
||||
executor = PredictorExecutor()
|
||||
# Redeem from pending trades + recent windows
|
||||
seen = set()
|
||||
# 1. Pending manual trades
|
||||
for t in trader.pending_trades:
|
||||
seen.add(str(t["window_id"]))
|
||||
# 2. Recent windows (last 2 hours)
|
||||
now = int(time.time())
|
||||
base = now - (now % BUCKET_SEC)
|
||||
for i in range(24): # last 24 windows = 2 hours
|
||||
seen.add(str(base - i * BUCKET_SEC))
|
||||
# 3. From predictor trades file
|
||||
trades_file = "data/chainlink_predictor/predictor_trades.jsonl"
|
||||
if os.path.exists(trades_file):
|
||||
with open(trades_file) as f:
|
||||
for line in f:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
wid = d.get("window_id", "")
|
||||
if wid:
|
||||
seen.add(wid)
|
||||
except Exception:
|
||||
pass
|
||||
total = 0
|
||||
for wid in seen:
|
||||
delta = await executor.auto_redeem(f"btc-updown-5m-{wid}")
|
||||
if delta > 0:
|
||||
print(f" {G}+${delta:.2f}{RST}")
|
||||
total += delta
|
||||
if total > 0:
|
||||
print(f" Total: {G}+${total:.2f}{RST}")
|
||||
bal = await trader.get_balance()
|
||||
print(f" Balance: {G}${bal:.2f}{RST}")
|
||||
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
trader.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user