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:
+20
@@ -0,0 +1,20 @@
|
|||||||
|
# Secrets — never commit real keys
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# Runtime paper/collector output (regenerated on every run)
|
||||||
|
data/chainlink_predictor/*
|
||||||
|
!data/chainlink_predictor/.gitkeep
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# BTC 5-Minute Binary Options — An Edge Feasibility Study
|
||||||
|
|
||||||
|
**Can a retail trader find a real, *capturable* edge in Polymarket's BTC up/down 5-minute binary options?**
|
||||||
|
I built a research system to answer that rigorously. The honest conclusion: **almost certainly not — and the reason why is more interesting than a "yes" would have been.**
|
||||||
|
|
||||||
|
*A market-microstructure research project in Python / asyncio: it reconstructs a Chainlink-style reference from 8 exchanges over live WebSockets, runs a family of XGBoost-gated strategies, and executes on Polymarket's CLOB. AI-assisted with Claude Code.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## About this repository
|
||||||
|
|
||||||
|
This is a **reconstruction** — a runnable extraction of the strategy engine, the XGBoost models, and the execution layer from a larger private system I built and operated. It is provided **as-is and UNAUDITED**.
|
||||||
|
|
||||||
|
- **Paper mode runs with no keys** and reads live market data over WebSocket.
|
||||||
|
- The **execution layer places real orders with real funds** if you supply wallet credentials in `src/predictor/.env` — none are included (`.env.example` ships blank).
|
||||||
|
- If you intend to trade it: **audit the code yourself and accept the risk.** This is an engineering / research project, **not financial advice.**
|
||||||
|
|
||||||
|
Building it was AI-assisted with Claude Code: I designed the research questions, the market model, the strategy logic, and the architecture; Claude accelerated the implementation. The conclusions and the microstructure reasoning are mine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The market
|
||||||
|
|
||||||
|
Polymarket issues a fresh **BTC Up/Down** binary option every 5 minutes. At the close of each window it settles by a simple rule:
|
||||||
|
|
||||||
|
> If the **Chainlink BTC/USD reference price** at window close ≥ its value at the window **open** → **UP = $1.00, DOWN = $0.00**.
|
||||||
|
> Otherwise → **UP = $0.00, DOWN = $1.00**.
|
||||||
|
|
||||||
|
Each side trades as a token priced between **$0 and $1** on Polymarket's central limit order book (CLOB). The key fact that makes this analyzable:
|
||||||
|
|
||||||
|
> **A binary token's price *is* the market's implied probability of the outcome.**
|
||||||
|
> UP trading at **$0.60** means the market is pricing a **60%** chance BTC ends the window up.
|
||||||
|
|
||||||
|
So the whole game collapses to one question:
|
||||||
|
|
||||||
|
> **Can I estimate the true probability of "UP" more accurately than the market's price implies — *and act on it before the market reprices?***
|
||||||
|
|
||||||
|
That splits into three investigations. The first two ask *is there an edge?* The third asks *can you actually capture it?*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pillar 1 — Rebuilding the reference: 8 exchanges vs. the oracle
|
||||||
|
|
||||||
|
BTC does not have *one* price. It has a slightly different price on every exchange, updating at slightly different times — and settlement depends on the slowest of them all: the Chainlink oracle print.
|
||||||
|
|
||||||
|
So I rebuilt a **Chainlink-style reference in real time** by aggregating **8 exchanges**, while separately tracking the majors that move first and carry the most weight — **Coinbase, Binance, OKX**:
|
||||||
|
|
||||||
|
- **WebSocket trade streams:** Coinbase, Binance, OKX, Kraken, Bybit, Bitstamp
|
||||||
|
- **REST polls (1s — no free public trade WS):** Gemini, CryptoCompare
|
||||||
|
- **Polymarket itself, over two WebSockets:** the Chainlink price stream, and the CLOB order book (top 5 levels). REST is used only once per window, to discover the new market's token IDs.
|
||||||
|
|
||||||
|
The aggregation is deliberately not a naive average: sources stale by >2s are dropped, each venue gets a rolling bias correction, USDT-quoted venues (Binance/OKX/Bybit) are adjusted for the **USDT premium** (median USDT price − median USD price), and the final reference is a **lead-weighted mean** with a freshness bonus for sources that ticked in the last 500ms.
|
||||||
|
|
||||||
|
The question: does the **lead–lag between venues** create a *timing* edge? The measured hierarchy — the study's central empirical result:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. The CLOB moves first — market makers reprice the book ~4–5s before
|
||||||
|
Coinbase's own tape fully reflects the move
|
||||||
|
2. Coinbase & majors follow
|
||||||
|
3. The Chainlink oracle prints last — the settlement reference is the
|
||||||
|
slowest object in the room
|
||||||
|
```
|
||||||
|
|
||||||
|
There *is* a measurable lead–lag. The catch: the fastest thing in the market is the order book itself — which means the people you'd be racing have already moved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pillar 2 — A directional signal from a rich parameter set
|
||||||
|
|
||||||
|
Beyond raw price, can BTC's behavior inside the window fold into a sharper directional estimate than the market's implied odds? The signal stack is deliberately wide — several families of parameters, not one:
|
||||||
|
|
||||||
|
- **Displacement & clock** — oracle move since window open (`pm_a`), distance to strike, time remaining
|
||||||
|
- **Cross-venue edge** — spot leaders vs. the oracle, both raw and EMA-smoothed (raw is faster; smoothed resists single-tick noise)
|
||||||
|
- **CLOB microstructure** — mid, spread, ask *velocity* (how fast makers are repricing), one-sided buy-volume imbalance
|
||||||
|
- **Momentum & intensity** — oracle-move acceleration, trade counts
|
||||||
|
- **ML layer** — XGBoost models scoring late-window outcomes (top features: own ask level, ask margin, |oracle move|, trade count, move acceleration, ask velocity)
|
||||||
|
|
||||||
|
The core intuition — and the cleanest math in the study:
|
||||||
|
|
||||||
|
> Late in the window, with a clear gap between spot and the strike and low remaining volatility, the outcome becomes highly predictable. To flip, the price would have to move a lot, fast, in the time left.
|
||||||
|
|
||||||
|
Modeling the price as a random walk to settlement, the probability of finishing on the current side of the strike is approximately:
|
||||||
|
|
||||||
|
```
|
||||||
|
P(UP) ≈ Φ( gap / ( σ · √(time_remaining) ) )
|
||||||
|
```
|
||||||
|
|
||||||
|
- `gap` = current spot − the strike (the reference price at window open)
|
||||||
|
- `σ` = short-horizon volatility
|
||||||
|
- `Φ` = the normal CDF
|
||||||
|
|
||||||
|
Then the trade signal is simply the mispricing:
|
||||||
|
|
||||||
|
```
|
||||||
|
edge = P_estimate − P_market # P_market = the token's current price
|
||||||
|
```
|
||||||
|
|
||||||
|
Trade only when `edge` is large **and** late in the window (small `time_remaining`), where the estimate is most reliable. That is exactly where a real edge, if one exists, would live.
|
||||||
|
|
||||||
|
### Strategy families: taker and maker
|
||||||
|
|
||||||
|
The signals feed two kinds of strategies, mirroring the two ways to trade a book:
|
||||||
|
|
||||||
|
- **Taker** (cross the spread when a signal fires): *edge reversal* — in the final seconds, a large cross-venue edge opposing the current leader → buy the reversal side; *CLOB momentum* — the book's ask rising fast means makers are repricing → follow them; *CLOB volume* — one side absorbing 5× the flow → follow it; *early lock* — oracle acceleration + volume doubling + book confirmation → buy the likely winner *early*, at ~0.67 instead of ~0.93, for 3–4× better payoff on the same thesis.
|
||||||
|
- **Maker** (rest passive limit bids and let the market come to you): cheap trailing-side bids with a GTD expiry — a $1 "option" on a late flip — and counter-consensus resting bids that only fill if a stop-loss cascade briefly dislocates the book. No fill, no risk.
|
||||||
|
|
||||||
|
Exits run a ladder of aggression: post the ask → step to the bid → cross at market.
|
||||||
|
|
||||||
|
One economics finding worth stating plainly: **low win-rate + cheap entry + high payoff beats high win-rate + expensive entry.** When market makers price accurately, the "safe" side's profit is compressed to zero — a 96%-win-rate strategy can still lose money at the odds you're offered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Machine learning — and what a weak model proved
|
||||||
|
|
||||||
|
To pressure-test whether the directional signal was more than eyeballed thresholds, I trained gradient-boosted trees (XGBoost) as the ML layer. Two model families (all shipped, trained, and loadable in `models/`):
|
||||||
|
|
||||||
|
- **Outcome / "lock" model** — 15 pre-trigger features (`ask`, `abs_pm_a`, `left`, `rv_5m`, `rv_15m`, `ask_margin`, `pm_a_accel`, `ask_vel`, `vol_ratio`, `total_vol`, `sign_changes`, …). Label: *does the current leader hold to settlement?* Used as a gate — only take the trade if the model's confidence clears a threshold.
|
||||||
|
- **Direction models** — a **separate 32-feature model per time-to-close slice** (models at 250s, 200s, …, 10s remaining), because late-window dynamics differ from early. Features span both the asset (`cb_a`, `bn_a`, `edge`, `rv_5m`, `funding`, `oi_chg`, `pm_a_z`) and the counterparty (`bid_ratio`, `ask_from_peak`, `vol_ratio`, `total_vol`).
|
||||||
|
|
||||||
|
**How I framed it, and read it honestly:** the label is the settlement outcome, the metric is AUC, and the result was **deliberately unimpressive — direction AUC ≈ 0.666** (0.50 is a coin flip; 0.70 is where a signal starts being useful). The lock gate had to be *lowered* from 0.90 to 0.75 before it would release enough trades to matter, and the lock strategy it gated still finished at a **96% win rate but negative PnL** — accurate predictions, wrong odds.
|
||||||
|
|
||||||
|
That weakness is the finding, not a failure. Two things fall out of the feature importances:
|
||||||
|
|
||||||
|
> **The most predictive features are the market maker's own quote** — `ask`, `ask_margin`, `ask_velocity` — not BTC's price dynamics. The model taught itself that the single best estimate of the outcome is *what the maker is already charging.* You cannot beat a signal that **is** the counterparty. A near-random AUC on BTC-derived features is direct evidence that the exploitable information isn't in the price — it's already priced.
|
||||||
|
|
||||||
|
**See it yourself:** `python ml/xgb_eval.py` loads the trained models and prints their feature importances (the maker's quote on top) plus the per-entry-price economics from real paper trades (every price bucket is net-negative).
|
||||||
|
|
||||||
|
**What I'd do next (unexplored):** every model here predicts the *outcome*. The open direction is to change the **label** — model the **counterparty's next action** (will the maker pull the quote, widen the spread, shade the book?) rather than the asset. That reframes the problem from *out-predicting BTC* (a race you lose to better infrastructure) to *detecting maker behavior* (where retail-observable microstructure might actually carry signal). The codebase has a seed of this — a `maker_bias` strategy built on "the maker knows the outcome, follow their bias" — but a proper counterparty model is the study's most interesting untested hypothesis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pillar 3 — Execution: a faster manual trading interface
|
||||||
|
|
||||||
|
A correct signal is worthless if you can't act on it in time. Polymarket's native UI is **too slow** to place an order inside the seconds when an edge exists — and it's poor for building intuition about how the book behaves under time pressure.
|
||||||
|
|
||||||
|
So I built a **hotkey terminal interface** over the CLOB (`tools/quick_trade.py`): raw-keyboard mode where a single keystroke fires an order (buy up/down at the ask, sell at the bid, one-cent "lottery" limit bids), order parameters pre-warmed and cached so submission skips per-order lookups, prices streamed from a local feed instead of fetched per click — and every order prints its measured latency in milliseconds. Full product write-up: [docs/QUICK_TRADE_PRD.md](docs/QUICK_TRADE_PRD.md).
|
||||||
|
|
||||||
|
But building it surfaced the deeper execution problem — and it isn't clicking fast. **You don't buy; you post an order and hope.** Every trade is a little state machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
quote (best ask) → post → { full fill | partial fill | no fill } → retry / cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
On a thin binary book, partial fills and no-fills are the norm, not the exception. Each retry chases an ask that is *already moving* — the maker reprices the instant you show intent — so you either cross to a worse level (slippage, straight out of a margin that was pennies to begin with) or you miss and the window closes. Exits face the same gauntlet as a ladder of escalating aggression (post the ask → step to the bid → cross at market), with a circuit breaker that halts trading when round-trip latency spikes. That loop — quote → partial → retry → fill — routinely takes **longer than the edge itself exists**.
|
||||||
|
|
||||||
|
This pillar is where the thesis breaks: **the latency inside the fill lifecycle, plus the slippage from thin liquidity, eats essentially the entire edge.** A signal that's correct and a UI that's instant still lose money at the point of the fill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operating reality — what actually broke
|
||||||
|
|
||||||
|
Running this against live funds is where the thesis stopped being theoretical. These failures are first-hand — and they are *why* an edge that looks real on paper doesn't survive contact with the chain:
|
||||||
|
|
||||||
|
- **Latency** — the round trip from signal to acknowledged order regularly outran the seconds an edge exists.
|
||||||
|
- **Quoted vs. filled** — the ask you decide on and the price you actually pay diverge, worst during the fast moves where the signal fires.
|
||||||
|
- **Can't buy / can't sell** — thin books mean orders partially fill or don't fill at all; exiting a position under time pressure is not guaranteed.
|
||||||
|
- **Opening-balance desync** — on-chain settled state and the bot's local view drift apart (a redemption not yet reflected, a fill counted twice), and a wrong balance quietly corrupts the next decision.
|
||||||
|
- **Fragile to change** — every strategy tweak surfaced a new on-chain edge case: nonce/gas, order expiry, reprice races, partial-fill accounting — none of it visible in backtest.
|
||||||
|
|
||||||
|
None of these are bugs to be fixed away; they are the structural tax of executing at retail on a chain-settled venue. Together they are the concrete form of the conclusion: the math can find an edge; the execution layer eats it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The honest conclusion: retail cannot win this
|
||||||
|
|
||||||
|
Even when the analysis surfaces a theoretical edge, **retail can't capture it.**
|
||||||
|
|
||||||
|
- The market makers consume the same exchange feeds this system does — and reprice the book in **milliseconds**. By the time a retail-latency system, let alone a human on hotkeys, detects the move and submits an order, the ask has already moved. The signal hierarchy in Pillar 1 is the proof: the book is *faster* than the exchanges it's pricing.
|
||||||
|
- The directional edge from Pillar 2 is real but thin, and it is **exactly what the market makers are also computing**, faster, with better infrastructure. Watching maker behavior over several weeks of tick data made this vivid: for a stretch, makers were slow to reprice late-window reversals — and within about three weeks they had adapted and that edge collapsed. The market *learns*.
|
||||||
|
- Pillar 3 and the operating reality above make the ceiling concrete: even a heavily optimized execution path is too slow, and the fill lifecycle plus thin-book slippage consumes what little edge survives.
|
||||||
|
|
||||||
|
**The math can find the edge. Market structure makes it uncapturable at retail speed and cost.** Compute, co-location, and latency consume the entire edge before it reaches you. This is structurally an **institutional** game — which is the single most useful finding of the study.
|
||||||
|
|
||||||
|
For anyone building consumer-facing trading products, that's the lesson worth carrying: **on any market where the edge is faster than the retail user, the honest product isn't "help them trade the edge" — it's designing for the game retail can actually win.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's in the repo, and how to run it
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/chainlink_predictor.py the live engine — reconstructs the reference, runs the
|
||||||
|
strategies, prints signals + (paper) fills
|
||||||
|
tools/quick_trade.py hotkey MANUAL trading terminal (live only) — see the PRD
|
||||||
|
src/predictor/ the real engine: strategy.py (the ~4k-line core), collector,
|
||||||
|
feeds, orderbook, pnl — and the live executor
|
||||||
|
ml/ XGBoost: offline model inspection, training scripts, a sample
|
||||||
|
models/ the trained XGBoost models (lock + per-slice direction + hold7s)
|
||||||
|
docs/QUICK_TRADE_PRD.md product write-up of the manual trading tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires **Python 3.10+** (3.13 recommended). Set up once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
| What | Command | Wallet keys? |
|
||||||
|
|---|---|---|
|
||||||
|
| Strategy engine, **paper** — reads live data, simulates fills, prints records | `python tools/chainlink_predictor.py --shares 5` | no |
|
||||||
|
| Inspect the trained **XGBoost** models (offline, instant) | `python ml/xgb_eval.py` | no |
|
||||||
|
| Strategy engine, **live** — real automated orders | `python tools/chainlink_predictor.py --trade --shares 5` | yes |
|
||||||
|
| **Manual** trading terminal — real orders (run the engine in another terminal for prices) | `python tools/quick_trade.py` | yes |
|
||||||
|
|
||||||
|
**Paper mode** connects to Polymarket + 8 exchange WebSockets and needs nothing but internet and the market being open. (Binance blocks some regions with HTTP 451 — that source drops out and the reference uses the rest; non-fatal.)
|
||||||
|
|
||||||
|
**Live trading** needs your own wallet in `src/predictor/.env` (copy `.env.example` and fill it in) **and** a Polymarket CLOB client. The live path uses a client fork that is not on PyPI, so **live execution isn't reproducible out of the box; paper is.** No keys ship with this repo. Re-read [About this repository](#about-this-repository) before trading anything.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Coinbase ── WS ──┐
|
||||||
|
Binance ─── WS ──┤
|
||||||
|
OKX ──────── WS ──┤ 8-exchange reference
|
||||||
|
Kraken ───── WS ──┼ (staleness-filtered,
|
||||||
|
Bybit ────── WS ──┤ bias-corrected, ┌─► Strategy ─► Executor
|
||||||
|
Bitstamp ─── WS ──┤ lead-weighted mean) ──►│ (P_estimate, (risk gate, fills,
|
||||||
|
Gemini ──── REST ─┤ │ edge, maker paper PnL, or live
|
||||||
|
CryptoCompare REST┘ │ + taker orders on the CLOB)
|
||||||
|
Polymarket ─ WS (Chainlink price) ──► Merger ┘ signals)
|
||||||
|
Polymarket ─ WS (CLOB order book) ──► (one clock for many feeds)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Feeds** — independent async clients per venue (WebSocket where available, 1s REST polls otherwise); each normalizes into a common tick model.
|
||||||
|
- **Collector / Merger** — combines multi-source ticks into a single time-ordered market state (one clock for many feeds) and reconstructs the reference.
|
||||||
|
- **Strategy** — computes the reference, the gap, `P_estimate`, and the `edge`; runs the taker/maker strategies and the XGBoost gates; emits a signal only inside the gate (late window, edge threshold).
|
||||||
|
- **Executor** — simulates fills for paper mode, or (with `--trade` and your keys) signs and posts real orders to the CLOB and settles on-chain.
|
||||||
|
|
||||||
|
## Stack & authorship
|
||||||
|
|
||||||
|
Python · asyncio · aiohttp · numpy · XGBoost · web3 / py-clob-client (live path). Built **AI-assisted with Claude Code**: I designed the research questions, the market model, the strategy logic, and the architecture; Claude accelerated the implementation. The conclusions and the microstructure reasoning are mine. This public repo is a reconstruction of a larger private system I built and operated.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Mini-PRD — Quick-Trade: a hotkey execution interface for 5-minute binaries
|
||||||
|
|
||||||
|
> *Written retrospectively: this documents the product decisions behind a tool I built and used during the study. The tool itself belongs to the full (private) system — no live-trading code ships in this repo.*
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
A 5-minute binary market prices in new information within seconds; the actionable moment for any late-window signal is the **final 10–40 seconds**. Polymarket's native web UI needs multiple clicks and page-state changes to place one order, gives no feedback on how long an order actually took, and is unusable for building intuition about how the book behaves under time pressure.
|
||||||
|
|
||||||
|
If Pillar 3 of the study asks *"can a human capture the edge at all?"*, the native UI answers "no" for the wrong reason — **tool latency, not market latency**. To make the test fair, execution overhead had to be driven to its floor and *measured*, so that whatever latency remained was attributable to the market, not the mouse.
|
||||||
|
|
||||||
|
## User
|
||||||
|
|
||||||
|
One power user: me. (n = 1 by design — this is a research instrument, not a consumer product. That constraint drives most scope cuts below.)
|
||||||
|
|
||||||
|
## Goals & success metrics
|
||||||
|
|
||||||
|
| Goal | Metric |
|
||||||
|
|---|---|
|
||||||
|
| Minimal click-to-submit path | **One keystroke** = one signed, posted order (no Enter, no confirmation dialog) |
|
||||||
|
| Latency visible, not assumed | Every order prints its **measured sign-and-submit round-trip in ms** (`create_order` + `post_order` → API ack) |
|
||||||
|
| Fill outcome reported, not assumed | After the order: **filled or not, matched size, fill price, and PnL** — read from the order's `size_matched` |
|
||||||
|
| Zero per-click network overhead beyond the order itself | Price display and order parameters require **0 extra HTTP calls** at trade time |
|
||||||
|
| Usable across windows without operator work | New 5-minute market auto-discovered and re-warmed in the background |
|
||||||
|
|
||||||
|
## Non-goals (explicit cuts)
|
||||||
|
|
||||||
|
- **No variable order size** — fixed 10 shares per keystroke. Fewer keys to think about, and it caps fat-finger damage.
|
||||||
|
- **No order management** — no cancel/modify UI. GTD expiry does the cleanup.
|
||||||
|
- **No position dashboard, no charts** — the separate watch screen owns market display; this tool owns the trigger.
|
||||||
|
- **No multi-market support** — current window only.
|
||||||
|
|
||||||
|
## Functional requirements (the entire keyboard surface)
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `u` / `d` | Buy UP / DOWN, 10 shares, at market (ask + $0.02 buffer, capped at $0.99) |
|
||||||
|
| `Shift`+`1`–`6` | Resting limit "lottery" bids at $0.01 / $0.02 / $0.03 — cheap maker orders (`1`–`3` = UP, `4`–`6` = DOWN) |
|
||||||
|
| `i` / `o` | Sell UP / DOWN at best bid, most expensive lot first |
|
||||||
|
| `b` / `r` / `w` | Balance check · redeem settled tokens · force window refresh |
|
||||||
|
| `q` | Quit (restores terminal state) |
|
||||||
|
|
||||||
|
## Component structure
|
||||||
|
|
||||||
|
```
|
||||||
|
① PRICE FEED — always live in the background. THIS is why a click needs no network:
|
||||||
|
|
||||||
|
Polymarket WebSocket ──► collector ──► snapshots.jsonl ──► tail -F ──► Price panel
|
||||||
|
(chainlink price + (local file, the (live bid/ask,
|
||||||
|
CLOB book, real-time) collector streams it) countdown)
|
||||||
|
|
||||||
|
② HOT PATH — fires on ONE keystroke; it reads the current ask from the Price panel
|
||||||
|
above (a local read), so no network call is needed to price the order:
|
||||||
|
|
||||||
|
keystroke (raw tty, no Enter)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Key listener (select() loop)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Order router u/d = market · cheap limits · i/o = sell
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Preflight cache tick_size + neg_risk, warmed once per window
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Order builder + signer client-side signing; HTTP/1.1 keep-alive
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
REST post_order (GTD) ──► Polymarket CLOB
|
||||||
|
|
||||||
|
③ REPORTING — every order writes one line to a JSONL journal (the study's dataset):
|
||||||
|
|
||||||
|
• on submit: sign → ack latency (ms)
|
||||||
|
• at settle: poll order status (REST) ──► size_matched · fill price · PnL
|
||||||
|
|
||||||
|
Background: a window manager auto-detects each new 5-minute market, resolves its
|
||||||
|
token IDs (one REST call), and re-warms the Preflight cache before the window opens.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key decisions & tradeoffs
|
||||||
|
|
||||||
|
| Decision | Why | Cost accepted |
|
||||||
|
|---|---|---|
|
||||||
|
| Single keystroke, no confirmation | The confirmation dialog *is* the latency problem | Fat-finger risk — mitigated by fixed small size |
|
||||||
|
| Fixed 10-share size | Removes a decision (and keystrokes) from the hot path | No sizing flexibility |
|
||||||
|
| Prices from a local file tail, not REST | The collector already streams every tick to disk; reading it costs ~0ms and no rate limits | Depends on the collector running |
|
||||||
|
| Warm order params once per window | `tick_size`/`neg_risk` lookups are per-token constants; fetching them per order wastes a round trip inside the hot path | Stale cache on rare market config change |
|
||||||
|
| Market buys pay ask + $0.02 | In the last seconds, a missed fill costs more than 2¢ of price improvement | Slightly worse average entry |
|
||||||
|
| Sell most-expensive-lot-first | Realizes the best exit first when unwinding under time pressure | — |
|
||||||
|
| Terminal UI, not web | tty raw mode delivers a keypress in microseconds; a browser adds an event loop you don't control | No visual polish |
|
||||||
|
|
||||||
|
## The real cost center: the fill lifecycle
|
||||||
|
|
||||||
|
The interface optimizes the *submit* path — but a keystroke doesn't buy anything; it opens an order lifecycle:
|
||||||
|
|
||||||
|
```
|
||||||
|
quote (best ask) → post → { full fill | partial fill | no fill } → retry / cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
This is deliberately **not** managed in the tool (fixed size, GTD auto-expiry does the cleanup — see Non-goals), and that scoping choice is exactly where the honesty lives: **order management is the pain, not a feature I could polish away.** On a thin binary book, partial fills and no-fills are routine. Each retry chases an ask the maker has already moved, so you cross to a worse price (slippage) or miss entirely. The exit runs the same lifecycle as a ladder — post the ask → step to the bid → cross at market — and a latency circuit breaker halts trading when the round trip degrades. The full loop regularly takes longer than the edge exists. **Latency inside this lifecycle plus thin-book slippage consumes essentially the entire margin** — which is why the fast-submit interface, though necessary, was never going to be sufficient.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
The interface doubled as the study's **measurement instrument**: every order writes one line to a JSONL journal — sign-and-submit latency, whether it filled, matched size, fill price, PnL. That log is the raw dataset behind Pillar 3, turning "execution is slow" from a hunch into distributions you can read. Order latency became a **measured number printed after every trade** instead of a guess, and — together with the fill-lifecycle cost above — that is what makes Pillar 3's conclusion honest: even with the human reduced to a single keystroke and the software path stripped to its floor, the remaining round-trip is orders of magnitude slower than the market makers' millisecond repricing. The bottleneck was never the UI; it's the speed of light plus someone else's colo. **Knowing that changed the product question** — from "how do I trade this faster?" to "what game can retail actually win?" (see the study's conclusion in the [README](../README.md)).
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
{"strategy": "clob_volume", "ask": 0.59, "pm_a": 20.69, "left": 11.0, "rv_5m": 4.61, "rv_15m": 4.46, "sim_a_vol": 9.11, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.67, "pm_a": -50.95, "left": 35.0, "rv_5m": 4.01, "rv_15m": 3.75, "sim_a_vol": 11.33, "direction": "DOWN", "won": true, "pnl_5sh": 1.65}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": -52.0, "left": 140.0, "rv_5m": 6.03, "rv_15m": 5.75, "sim_a_vol": 14.16, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 53.71, "left": 140.0, "rv_5m": 2.35, "rv_15m": 1.85, "sim_a_vol": 4.8, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.92, "pm_a": -45.11, "left": 140.0, "rv_5m": 2.68, "rv_15m": 3.38, "sim_a_vol": 3.53, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.94, "pm_a": -63.11, "left": 119.0, "rv_5m": 3.32, "rv_15m": 5.03, "sim_a_vol": 22.97, "direction": "DOWN", "won": true, "pnl_5sh": 0.3}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": -9.31, "left": 250.0, "rv_5m": 2.52, "rv_15m": 2.85, "sim_a_vol": 9.98, "direction": "DOWN", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.29, "pm_a": 4.84, "left": 140.0, "rv_5m": 3.37, "rv_15m": 3.61, "sim_a_vol": 8.23, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -11.18, "left": 250.0, "rv_5m": 5.03, "rv_15m": 6.31, "sim_a_vol": 11.41, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": -75.22, "left": 140.0, "rv_5m": 2.24, "rv_15m": 1.78, "sim_a_vol": 13.75, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 86.39, "left": 140.0, "rv_5m": 3.81, "rv_15m": 3.81, "sim_a_vol": 6.44, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.64, "pm_a": 14.14, "left": 140.0, "rv_5m": 6.36, "rv_15m": 6.09, "sim_a_vol": 27.94, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.42, "pm_a": -2.44, "left": 205.0, "rv_5m": 11.23, "rv_15m": 10.22, "sim_a_vol": 42.3, "direction": "DOWN", "won": false, "pnl_5sh": -2.1}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": 2.65, "left": 233.0, "rv_5m": 2.41, "rv_15m": 3.0, "sim_a_vol": 1.56, "direction": "DOWN", "won": true, "pnl_5sh": 2.5}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.45, "pm_a": 11.5, "left": 248.0, "rv_5m": 4.82, "rv_15m": 5.41, "sim_a_vol": 7.82, "direction": "DOWN", "won": false, "pnl_5sh": -2.25}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.74, "pm_a": -15.31, "left": 140.0, "rv_5m": 5.0, "rv_15m": 4.18, "sim_a_vol": 6.08, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.13, "pm_a": -85.07, "left": 200.0, "rv_5m": 4.15, "rv_15m": 5.67, "sim_a_vol": 9.77, "direction": "UP", "won": true, "pnl_5sh": 4.35}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.6, "pm_a": 14.65, "left": 200.0, "rv_5m": 2.13, "rv_15m": 2.23, "sim_a_vol": 3.91, "direction": "UP", "won": true, "pnl_5sh": 2.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.63, "pm_a": -14.76, "left": 140.0, "rv_5m": 0.0, "rv_15m": 0.0, "sim_a_vol": 15.83, "direction": "DOWN", "won": true, "pnl_5sh": 1.85}
|
||||||
|
{"strategy": "lock", "ask": 0.96, "pm_a": -248.13, "left": 138.7, "rv_5m": 12.32, "rv_15m": 12.3, "sim_a_vol": 33.05, "direction": "DOWN", "won": true, "pnl_5sh": 0.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.7, "pm_a": 10.22, "left": 140.0, "rv_5m": 2.87, "rv_15m": 2.76, "sim_a_vol": 1.55, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_50", "ask": 0.94, "pm_a": 68.14, "left": 50.0, "rv_5m": 5.52, "rv_15m": 5.85, "sim_a_vol": 23.35, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 8.04, "left": 250.0, "rv_5m": 3.51, "rv_15m": 3.37, "sim_a_vol": 3.8, "direction": "UP", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "xgb_dir_10", "ask": 0.79, "pm_a": 35.43, "left": 15.0, "rv_5m": 5.75, "rv_15m": 5.87, "sim_a_vol": 19.52, "direction": "UP", "won": true, "pnl_5sh": 1.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.89, "pm_a": 67.42, "left": 140.0, "rv_5m": 5.81, "rv_15m": 5.78, "sim_a_vol": 17.41, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.3, "pm_a": -43.91, "left": 282.0, "rv_5m": 7.24, "rv_15m": 5.17, "sim_a_vol": 3.09, "direction": "UP", "won": false, "pnl_5sh": -1.5}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -6.45, "left": 202.0, "rv_5m": 1.85, "rv_15m": 2.59, "sim_a_vol": 1.88, "direction": "DOWN", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.85, "pm_a": -34.11, "left": 140.0, "rv_5m": 3.56, "rv_15m": 4.41, "sim_a_vol": 13.14, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.72, "pm_a": -23.78, "left": 106.9, "rv_5m": 20.01, "rv_15m": 20.55, "sim_a_vol": 14.69, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -84.1, "left": 129.0, "rv_5m": 3.76, "rv_15m": 4.0, "sim_a_vol": 10.65, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.61, "pm_a": -3.95, "left": 140.0, "rv_5m": 2.55, "rv_15m": 2.84, "sim_a_vol": 3.53, "direction": "DOWN", "won": false, "pnl_5sh": -3.05}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.55, "pm_a": -12.92, "left": 241.0, "rv_5m": 4.36, "rv_15m": 4.2, "sim_a_vol": 18.58, "direction": "DOWN", "won": true, "pnl_5sh": 2.25}
|
||||||
|
{"strategy": "clob_momentum", "ask": 0.51, "pm_a": 3.29, "left": 29.0, "rv_5m": 4.95, "rv_15m": 4.54, "sim_a_vol": 4.37, "direction": "DOWN", "won": true, "pnl_5sh": 2.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.53, "pm_a": 8.38, "left": 140.0, "rv_5m": 2.92, "rv_15m": 3.59, "sim_a_vol": 3.99, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.81, "pm_a": -18.39, "left": 85.0, "rv_5m": 2.57, "rv_15m": 3.01, "sim_a_vol": 2.57, "direction": "DOWN", "won": false, "pnl_5sh": -4.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.95, "pm_a": -106.34, "left": 140.0, "rv_5m": 7.68, "rv_15m": 5.92, "sim_a_vol": 12.93, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_hours", "ask": 0.45, "pm_a": 0.52, "left": 296.0, "rv_5m": 3.92, "rv_15m": 3.81, "sim_a_vol": 9.8, "direction": "DOWN", "won": true, "pnl_5sh": 2.75}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.24, "pm_a": -11.34, "left": 241.0, "rv_5m": 2.52, "rv_15m": 4.03, "sim_a_vol": 19.06, "direction": "UP", "won": false, "pnl_5sh": -1.2}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.54, "pm_a": -6.07, "left": 227.0, "rv_5m": 2.44, "rv_15m": 2.96, "sim_a_vol": 6.1, "direction": "UP", "won": true, "pnl_5sh": 2.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.69, "pm_a": -9.21, "left": 140.0, "rv_5m": 1.38, "rv_15m": 1.96, "sim_a_vol": 1.2, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "break", "ask": 0.53, "pm_a": -2.94, "left": 20.0, "rv_5m": 6.17, "rv_15m": 6.25, "sim_a_vol": 19.42, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.68, "pm_a": 18.4, "left": 140.0, "rv_5m": 4.45, "rv_15m": 4.08, "sim_a_vol": 16.32, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.72, "pm_a": -18.85, "left": 140.0, "rv_5m": 2.24, "rv_15m": 2.58, "sim_a_vol": 2.24, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.79, "pm_a": -27.73, "left": 140.0, "rv_5m": 3.32, "rv_15m": 3.41, "sim_a_vol": 2.01, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.09, "pm_a": -82.8, "left": 192.0, "rv_5m": 3.89, "rv_15m": 3.85, "sim_a_vol": 10.57, "direction": "UP", "won": false, "pnl_5sh": -0.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.82, "pm_a": 37.46, "left": 140.0, "rv_5m": 2.52, "rv_15m": 6.02, "sim_a_vol": 17.88, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.67, "pm_a": 19.86, "left": 140.0, "rv_5m": 4.54, "rv_15m": 5.1, "sim_a_vol": 1.51, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": -66.77, "left": 140.0, "rv_5m": 10.04, "rv_15m": 12.45, "sim_a_vol": 21.8, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.31, "pm_a": -54.9, "left": 241.0, "rv_5m": 9.16, "rv_15m": 9.23, "sim_a_vol": 17.78, "direction": "UP", "won": false, "pnl_5sh": -1.55}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.54, "pm_a": -3.98, "left": 140.0, "rv_5m": 1.7, "rv_15m": 2.57, "sim_a_vol": 5.58, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 2.2, "left": 40.0, "rv_5m": 9.41, "rv_15m": 7.88, "sim_a_vol": 29.84, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.71, "pm_a": -12.78, "left": 140.0, "rv_5m": 3.09, "rv_15m": 3.32, "sim_a_vol": 10.86, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.52, "pm_a": 13.07, "left": 250.0, "rv_5m": 7.37, "rv_15m": 9.29, "sim_a_vol": 9.98, "direction": "UP", "won": true, "pnl_5sh": 2.4}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": -73.56, "left": 140.0, "rv_5m": 5.39, "rv_15m": 6.2, "sim_a_vol": 9.81, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 6.54, "left": 241.0, "rv_5m": 4.19, "rv_15m": 3.42, "sim_a_vol": 24.27, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.64, "pm_a": -20.15, "left": 287.0, "rv_5m": 2.05, "rv_15m": 4.87, "sim_a_vol": 7.4, "direction": "DOWN", "won": true, "pnl_5sh": 1.8}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.4, "pm_a": 8.85, "left": 140.0, "rv_5m": 1.9, "rv_15m": 2.35, "sim_a_vol": 4.51, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.53, "pm_a": 4.47, "left": 248.0, "rv_5m": 4.98, "rv_15m": 4.43, "sim_a_vol": 61.24, "direction": "DOWN", "won": false, "pnl_5sh": -2.65}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.62, "pm_a": 7.37, "left": 164.0, "rv_5m": 4.4, "rv_15m": 3.92, "sim_a_vol": 10.27, "direction": "UP", "won": true, "pnl_5sh": 1.9}
|
||||||
|
{"strategy": "clob_rev", "ask": 0.22, "pm_a": -29.72, "left": 42.0, "rv_5m": 6.74, "rv_15m": 5.94, "sim_a_vol": 5.49, "direction": "UP", "won": false, "pnl_5sh": -1.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.91, "pm_a": 92.49, "left": 140.0, "rv_5m": 8.0, "rv_15m": 9.98, "sim_a_vol": 28.18, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.71, "pm_a": -11.56, "left": 140.0, "rv_5m": 1.39, "rv_15m": 1.45, "sim_a_vol": 3.67, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.67, "pm_a": 25.82, "left": 140.0, "rv_5m": 4.57, "rv_15m": 7.68, "sim_a_vol": 7.69, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "early_lock", "ask": 0.69, "pm_a": 50.23, "left": 226.0, "rv_5m": 5.84, "rv_15m": 6.62, "sim_a_vol": 22.05, "direction": "UP", "won": false, "pnl_5sh": -3.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.89, "pm_a": -43.39, "left": 140.0, "rv_5m": 4.57, "rv_15m": 3.32, "sim_a_vol": 5.57, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.42, "pm_a": 34.39, "left": 7.0, "rv_5m": 2.8, "rv_15m": 1.98, "sim_a_vol": 1.62, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.24, "pm_a": 54.35, "left": 230.0, "rv_5m": 25.95, "rv_15m": 19.8, "sim_a_vol": 22.84, "direction": "DOWN", "won": false, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "cb_confirm", "ask": 0.7, "pm_a": -20.59, "left": 271.0, "rv_5m": 4.23, "rv_15m": 5.58, "sim_a_vol": 6.98, "direction": "DOWN", "won": false, "pnl_5sh": -3.5}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.57, "pm_a": 18.31, "left": 227.0, "rv_5m": 4.91, "rv_15m": 4.26, "sim_a_vol": 9.89, "direction": "UP", "won": true, "pnl_5sh": 2.15}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.69, "pm_a": 28.01, "left": 137.0, "rv_5m": 3.16, "rv_15m": 4.21, "sim_a_vol": 5.96, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.36, "pm_a": -6.42, "left": 140.0, "rv_5m": 1.32, "rv_15m": 2.07, "sim_a_vol": 2.2, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.38, "pm_a": 0.16, "left": 292.1, "rv_5m": 3.89, "rv_15m": 4.47, "sim_a_vol": 43.83, "direction": "DOWN", "won": false, "pnl_5sh": -1.9}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.47, "pm_a": 1.82, "left": 214.0, "rv_5m": 2.18, "rv_15m": 2.3, "sim_a_vol": 1.22, "direction": "DOWN", "won": true, "pnl_5sh": 2.65}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 24.22, "left": 218.0, "rv_5m": 5.57, "rv_15m": 5.19, "sim_a_vol": 11.6, "direction": "UP", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "edge_reversal", "ask": 0.12, "pm_a": 31.61, "left": 24.0, "rv_5m": 5.24, "rv_15m": 5.96, "sim_a_vol": 10.64, "direction": "DOWN", "won": false, "pnl_5sh": -0.6}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -19.66, "left": 247.0, "rv_5m": 6.57, "rv_15m": 7.13, "sim_a_vol": 47.91, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.32, "pm_a": -37.68, "left": 245.5, "rv_5m": 5.52, "rv_15m": 8.52, "sim_a_vol": 33.3, "direction": "UP", "won": false, "pnl_5sh": -1.6}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.91, "pm_a": 46.12, "left": 140.0, "rv_5m": 2.68, "rv_15m": 3.23, "sim_a_vol": 5.09, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.9, "pm_a": 129.07, "left": 139.9, "rv_5m": 8.84, "rv_15m": 8.84, "sim_a_vol": 48.36, "direction": "UP", "won": false, "pnl_5sh": -4.5}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": -26.18, "left": 250.0, "rv_5m": 6.69, "rv_15m": 6.98, "sim_a_vol": 11.32, "direction": "DOWN", "won": true, "pnl_5sh": 2.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": -24.94, "left": 140.0, "rv_5m": 3.48, "rv_15m": 3.85, "sim_a_vol": 4.5, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.9, "pm_a": -38.15, "left": 140.0, "rv_5m": 3.3, "rv_15m": 2.48, "sim_a_vol": 5.45, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.99, "pm_a": -65.06, "left": 42.0, "rv_5m": 5.47, "rv_15m": 4.49, "sim_a_vol": 24.24, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": 94.07, "left": 140.0, "rv_5m": 4.87, "rv_15m": 6.24, "sim_a_vol": 9.43, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": 17.4, "left": 224.0, "rv_5m": 5.39, "rv_15m": 4.03, "sim_a_vol": 9.14, "direction": "DOWN", "won": false, "pnl_5sh": -2.5}
|
||||||
|
{"strategy": "lock", "ask": 0.99, "pm_a": -63.09, "left": 41.0, "rv_5m": 3.22, "rv_15m": 5.49, "sim_a_vol": 4.38, "direction": "DOWN", "won": true, "pnl_5sh": 0.05}
|
||||||
|
{"strategy": "lock", "ask": 0.95, "pm_a": 120.81, "left": 140.0, "rv_5m": 5.99, "rv_15m": 5.8, "sim_a_vol": 16.2, "direction": "UP", "won": true, "pnl_5sh": 0.25}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.25, "pm_a": -12.6, "left": 96.0, "rv_5m": 2.61, "rv_15m": 4.55, "sim_a_vol": 9.81, "direction": "UP", "won": true, "pnl_5sh": 3.75}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.59, "pm_a": 24.27, "left": 140.0, "rv_5m": 2.39, "rv_15m": 2.74, "sim_a_vol": 2.82, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.82, "pm_a": -51.98, "left": 140.0, "rv_5m": 5.03, "rv_15m": 6.73, "sim_a_vol": 3.49, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.78, "pm_a": 23.45, "left": 39.0, "rv_5m": 4.29, "rv_15m": 2.93, "sim_a_vol": 8.84, "direction": "UP", "won": true, "pnl_5sh": 1.1}
|
||||||
|
{"strategy": "xgb_dir_50", "ask": 0.83, "pm_a": -20.83, "left": 50.0, "rv_5m": 3.78, "rv_15m": 4.2, "sim_a_vol": 4.64, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.97, "pm_a": 166.57, "left": 140.0, "rv_5m": 8.75, "rv_15m": 6.94, "sim_a_vol": 14.67, "direction": "UP", "won": true, "pnl_5sh": 0.15}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.38, "pm_a": 4.51, "left": 20.5, "rv_5m": 4.29, "rv_15m": 6.86, "sim_a_vol": 2.34, "direction": "UP", "won": true, "pnl_5sh": 3.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.9, "pm_a": 39.74, "left": 140.0, "rv_5m": 2.19, "rv_15m": 3.95, "sim_a_vol": 6.95, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 41.06, "left": 169.0, "rv_5m": 7.36, "rv_15m": 5.67, "sim_a_vol": 7.46, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.11, "pm_a": -54.43, "left": 122.0, "rv_5m": 4.72, "rv_15m": 5.33, "sim_a_vol": 8.06, "direction": "UP", "won": false, "pnl_5sh": -0.55}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.48, "pm_a": -6.88, "left": 249.0, "rv_5m": 3.79, "rv_15m": 4.23, "sim_a_vol": 25.6, "direction": "DOWN", "won": true, "pnl_5sh": 2.6}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.57, "pm_a": -32.94, "left": 24.0, "rv_5m": 2.55, "rv_15m": 2.57, "sim_a_vol": 0.73, "direction": "DOWN", "won": true, "pnl_5sh": 2.15}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.44, "pm_a": -13.76, "left": 33.0, "rv_5m": 4.28, "rv_15m": 4.12, "sim_a_vol": 7.63, "direction": "UP", "won": true, "pnl_5sh": 2.8}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.63, "pm_a": -28.11, "left": 263.0, "rv_5m": 3.24, "rv_15m": 3.22, "sim_a_vol": 14.14, "direction": "DOWN", "won": true, "pnl_5sh": 1.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.54, "pm_a": 44.49, "left": 158.0, "rv_5m": 2.03, "rv_15m": 2.15, "sim_a_vol": 3.78, "direction": "UP", "won": true, "pnl_5sh": 2.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.59, "pm_a": -5.87, "left": 140.0, "rv_5m": 3.75, "rv_15m": 6.11, "sim_a_vol": 9.81, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": -27.18, "left": 250.0, "rv_5m": 4.43, "rv_15m": 4.06, "sim_a_vol": 30.14, "direction": "DOWN", "won": false, "pnl_5sh": -2.8}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.6, "pm_a": -13.09, "left": 284.0, "rv_5m": 3.74, "rv_15m": 3.61, "sim_a_vol": 8.84, "direction": "DOWN", "won": true, "pnl_5sh": 2.0}
|
||||||
|
{"strategy": "last7s", "ask": 0.92, "pm_a": 44.4, "left": 19.9, "rv_5m": 2.76, "rv_15m": 3.6, "sim_a_vol": 18.56, "direction": "UP", "won": true, "pnl_5sh": 0.4}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.62, "pm_a": 5.8, "left": 140.0, "rv_5m": 3.5, "rv_15m": 3.6, "sim_a_vol": 5.57, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.46, "pm_a": -8.81, "left": 157.0, "rv_5m": 2.68, "rv_15m": 4.47, "sim_a_vol": 5.53, "direction": "UP", "won": false, "pnl_5sh": -2.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.67, "pm_a": -23.21, "left": 140.0, "rv_5m": 3.11, "rv_15m": 3.38, "sim_a_vol": 12.06, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cb_flip", "ask": 0.52, "pm_a": -8.83, "left": 29.4, "rv_5m": 3.44, "rv_15m": 3.83, "sim_a_vol": 5.22, "direction": "UP", "won": false, "pnl_5sh": -3.6}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.92, "pm_a": 26.77, "left": 140.0, "rv_5m": 1.97, "rv_15m": 2.12, "sim_a_vol": 10.14, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 1.0, "pm_a": 0.0, "left": 299.8, "rv_5m": 8.96, "rv_15m": 7.51, "sim_a_vol": 28.92, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "last_flip", "ask": 0.51, "pm_a": 10.01, "left": 30.0, "rv_5m": 3.07, "rv_15m": 3.75, "sim_a_vol": 11.32, "direction": "UP", "won": true, "pnl_5sh": 2.45}
|
||||||
|
{"strategy": "last7s", "ask": 1.0, "pm_a": -192.28, "left": 19.7, "rv_5m": 5.05, "rv_15m": 4.69, "sim_a_vol": 15.06, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.93, "pm_a": -42.94, "left": 200.0, "rv_5m": 2.52, "rv_15m": 2.76, "sim_a_vol": 7.89, "direction": "DOWN", "won": true, "pnl_5sh": 0.35}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.51, "pm_a": 11.64, "left": 250.0, "rv_5m": 4.81, "rv_15m": 4.01, "sim_a_vol": 46.38, "direction": "UP", "won": false, "pnl_5sh": -2.55}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": -36.0, "left": 216.0, "rv_5m": 5.65, "rv_15m": 5.2, "sim_a_vol": 6.3, "direction": "DOWN", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.52, "pm_a": 8.75, "left": 241.0, "rv_5m": 4.01, "rv_15m": 4.73, "sim_a_vol": 9.34, "direction": "DOWN", "won": false, "pnl_5sh": -2.6}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.23, "pm_a": 27.45, "left": 208.0, "rv_5m": 6.44, "rv_15m": 5.74, "sim_a_vol": 14.46, "direction": "DOWN", "won": true, "pnl_5sh": 3.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": 43.63, "left": 250.0, "rv_5m": 5.44, "rv_15m": 5.59, "sim_a_vol": 25.44, "direction": "UP", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": -21.01, "left": 245.0, "rv_5m": 3.09, "rv_15m": 3.96, "sim_a_vol": 17.85, "direction": "DOWN", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.9, "pm_a": 49.13, "left": 140.0, "rv_5m": 3.76, "rv_15m": 4.85, "sim_a_vol": 10.32, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cb_confirm", "ask": 0.7, "pm_a": -7.87, "left": 226.0, "rv_5m": 3.47, "rv_15m": 4.29, "sim_a_vol": 6.68, "direction": "DOWN", "won": true, "pnl_5sh": 1.5}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.64, "pm_a": -8.19, "left": 140.0, "rv_5m": 1.43, "rv_15m": 1.81, "sim_a_vol": 1.49, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 1.0, "pm_a": 92.5, "left": 3.0, "rv_5m": 4.97, "rv_15m": 4.52, "sim_a_vol": 11.09, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.84, "pm_a": -51.05, "left": 140.0, "rv_5m": 6.17, "rv_15m": 10.72, "sim_a_vol": 10.26, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.12, "pm_a": 47.9, "left": 192.0, "rv_5m": 3.23, "rv_15m": 2.88, "sim_a_vol": 6.19, "direction": "DOWN", "won": false, "pnl_5sh": -0.6}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.86, "pm_a": 49.1, "left": 140.0, "rv_5m": 3.45, "rv_15m": 3.41, "sim_a_vol": 10.46, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.33, "pm_a": -32.75, "left": 269.0, "rv_5m": 5.47, "rv_15m": 6.9, "sim_a_vol": 26.04, "direction": "UP", "won": true, "pnl_5sh": 3.35}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.41, "pm_a": 16.39, "left": 239.0, "rv_5m": 2.76, "rv_15m": 2.97, "sim_a_vol": 6.87, "direction": "UP", "won": true, "pnl_5sh": 2.95}
|
||||||
|
{"strategy": "lock", "ask": 0.95, "pm_a": -62.38, "left": 116.0, "rv_5m": 5.25, "rv_15m": 4.53, "sim_a_vol": 8.02, "direction": "DOWN", "won": true, "pnl_5sh": 0.25}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.88, "pm_a": 60.78, "left": 140.0, "rv_5m": 2.54, "rv_15m": 2.6, "sim_a_vol": 11.37, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "last7s", "ask": 0.97, "pm_a": -52.32, "left": 19.3, "rv_5m": 4.25, "rv_15m": 4.78, "sim_a_vol": 7.37, "direction": "DOWN", "won": true, "pnl_5sh": 0.15}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.46, "pm_a": 3.37, "left": 140.0, "rv_5m": 3.09, "rv_15m": 3.37, "sim_a_vol": 2.82, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.97, "pm_a": 62.34, "left": 140.0, "rv_5m": 3.83, "rv_15m": 3.96, "sim_a_vol": 3.84, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.32, "pm_a": -57.48, "left": 261.0, "rv_5m": 5.12, "rv_15m": 5.09, "sim_a_vol": 48.61, "direction": "UP", "won": false, "pnl_5sh": -1.6}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.55, "pm_a": -5.25, "left": 244.0, "rv_5m": 4.03, "rv_15m": 4.13, "sim_a_vol": 20.91, "direction": "DOWN", "won": true, "pnl_5sh": 2.25}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 27.06, "left": 250.0, "rv_5m": 6.8, "rv_15m": 7.89, "sim_a_vol": 14.9, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.38, "pm_a": -17.38, "left": 123.0, "rv_5m": 6.62, "rv_15m": 4.51, "sim_a_vol": 13.36, "direction": "UP", "won": false, "pnl_5sh": -1.9}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.34, "pm_a": -24.0, "left": 279.0, "rv_5m": 5.08, "rv_15m": 7.41, "sim_a_vol": 24.13, "direction": "UP", "won": true, "pnl_5sh": 3.3}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.55, "pm_a": 7.16, "left": 238.0, "rv_5m": 4.14, "rv_15m": 3.85, "sim_a_vol": 9.44, "direction": "UP", "won": true, "pnl_5sh": 2.25}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.57, "pm_a": -19.52, "left": 250.0, "rv_5m": 6.91, "rv_15m": 6.26, "sim_a_vol": 17.41, "direction": "DOWN", "won": false, "pnl_5sh": -2.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": 7.09, "left": 138.0, "rv_5m": 4.95, "rv_15m": 5.03, "sim_a_vol": 2.85, "direction": "UP", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": -140.6, "left": 140.0, "rv_5m": 8.66, "rv_15m": 5.5, "sim_a_vol": 20.92, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.52, "pm_a": -8.79, "left": 140.0, "rv_5m": 5.97, "rv_15m": 4.72, "sim_a_vol": 13.12, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.94, "pm_a": 51.62, "left": 134.1, "rv_5m": 3.67, "rv_15m": 4.26, "sim_a_vol": 2.21, "direction": "UP", "won": true, "pnl_5sh": 0.3}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": 13.35, "left": 218.0, "rv_5m": 4.59, "rv_15m": 6.19, "sim_a_vol": 16.54, "direction": "UP", "won": true, "pnl_5sh": 2.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": -21.61, "left": 140.0, "rv_5m": 3.5, "rv_15m": 3.52, "sim_a_vol": 10.14, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.82, "pm_a": 36.24, "left": 140.0, "rv_5m": 4.22, "rv_15m": 4.39, "sim_a_vol": 7.2, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.35, "pm_a": 15.74, "left": 217.0, "rv_5m": 1.7, "rv_15m": 2.99, "sim_a_vol": 2.8, "direction": "DOWN", "won": false, "pnl_5sh": -1.75}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": 85.05, "left": 140.0, "rv_5m": 4.25, "rv_15m": 4.02, "sim_a_vol": 17.41, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.67, "pm_a": -38.08, "left": 140.0, "rv_5m": 9.7, "rv_15m": 8.05, "sim_a_vol": 12.57, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.63, "pm_a": 38.96, "left": 140.0, "rv_5m": 2.45, "rv_15m": 2.36, "sim_a_vol": 8.5, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 1.0, "pm_a": 0.0, "left": 300.0, "rv_5m": 7.04, "rv_15m": 6.54, "sim_a_vol": 14.05, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.51, "pm_a": -7.33, "left": 250.0, "rv_5m": 4.17, "rv_15m": 5.15, "sim_a_vol": 5.12, "direction": "DOWN", "won": true, "pnl_5sh": 2.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": -10.24, "left": 140.0, "rv_5m": 2.16, "rv_15m": 3.04, "sim_a_vol": 5.75, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.31, "pm_a": 27.2, "left": 199.0, "rv_5m": 1.11, "rv_15m": 1.14, "sim_a_vol": 4.56, "direction": "DOWN", "won": false, "pnl_5sh": -1.55}
|
||||||
|
{"strategy": "lock", "ask": 0.88, "pm_a": -62.91, "left": 116.0, "rv_5m": 5.1, "rv_15m": 7.54, "sim_a_vol": 15.18, "direction": "DOWN", "won": false, "pnl_5sh": -4.4}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.04, "pm_a": -149.41, "left": 218.0, "rv_5m": 4.2, "rv_15m": 3.31, "sim_a_vol": 47.4, "direction": "UP", "won": false, "pnl_5sh": -0.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.74, "pm_a": 12.57, "left": 140.0, "rv_5m": 1.73, "rv_15m": 2.7, "sim_a_vol": 5.85, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.54, "pm_a": -5.66, "left": 140.0, "rv_5m": 3.76, "rv_15m": 4.17, "sim_a_vol": 2.86, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 1.0, "pm_a": -8.46, "left": 136.0, "rv_5m": 12.65, "rv_15m": 20.59, "sim_a_vol": 19.67, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": 6.65, "left": 140.0, "rv_5m": 1.5, "rv_15m": 1.73, "sim_a_vol": 4.62, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.68, "pm_a": 10.42, "left": 140.0, "rv_5m": 2.46, "rv_15m": 2.56, "sim_a_vol": 6.18, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.29, "pm_a": 28.06, "left": 216.0, "rv_5m": 2.38, "rv_15m": 3.17, "sim_a_vol": 6.98, "direction": "DOWN", "won": false, "pnl_5sh": -1.45}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.32, "pm_a": -11.41, "left": 249.0, "rv_5m": 26.55, "rv_15m": 20.11, "sim_a_vol": 20.14, "direction": "DOWN", "won": false, "pnl_5sh": -1.6}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.47, "pm_a": -15.06, "left": 240.0, "rv_5m": 5.65, "rv_15m": 4.58, "sim_a_vol": 15.32, "direction": "UP", "won": true, "pnl_5sh": 2.65}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.65, "pm_a": -23.72, "left": 140.0, "rv_5m": 7.6, "rv_15m": 7.15, "sim_a_vol": 11.1, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.63, "pm_a": 21.58, "left": 260.0, "rv_5m": 4.03, "rv_15m": 3.83, "sim_a_vol": 30.22, "direction": "UP", "won": true, "pnl_5sh": 1.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.42, "pm_a": 4.28, "left": 186.0, "rv_5m": 3.14, "rv_15m": 3.23, "sim_a_vol": 2.95, "direction": "DOWN", "won": false, "pnl_5sh": -2.1}
|
||||||
|
{"strategy": "xgb_dir_250", "ask": 0.41, "pm_a": -7.53, "left": 260.0, "rv_5m": 3.58, "rv_15m": 4.07, "sim_a_vol": 30.11, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.55, "pm_a": 15.05, "left": 140.0, "rv_5m": 3.52, "rv_15m": 5.38, "sim_a_vol": 5.66, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.44, "pm_a": 4.34, "left": 198.0, "rv_5m": 3.0, "rv_15m": 2.72, "sim_a_vol": 3.82, "direction": "DOWN", "won": false, "pnl_5sh": -2.2}
|
||||||
|
{"strategy": "lock", "ask": 0.61, "pm_a": 60.01, "left": 134.0, "rv_5m": 19.73, "rv_15m": 18.16, "sim_a_vol": 25.04, "direction": "UP", "won": true, "pnl_5sh": 1.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.95, "pm_a": -97.38, "left": 140.0, "rv_5m": 13.62, "rv_15m": 9.36, "sim_a_vol": 12.35, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.31, "pm_a": -47.61, "left": 176.0, "rv_5m": 6.61, "rv_15m": 5.89, "sim_a_vol": 9.03, "direction": "UP", "won": true, "pnl_5sh": 3.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.38, "pm_a": 15.8, "left": 140.0, "rv_5m": 3.76, "rv_15m": 2.75, "sim_a_vol": 1.97, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.52, "pm_a": -0.96, "left": 242.0, "rv_5m": 3.9, "rv_15m": 3.5, "sim_a_vol": 18.65, "direction": "DOWN", "won": true, "pnl_5sh": 2.4}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.68, "pm_a": -35.32, "left": 140.0, "rv_5m": 3.86, "rv_15m": 3.17, "sim_a_vol": 2.88, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.95, "pm_a": 52.15, "left": 140.0, "rv_5m": 3.04, "rv_15m": 2.46, "sim_a_vol": 5.56, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.97, "pm_a": 94.41, "left": 140.0, "rv_5m": 4.82, "rv_15m": 4.28, "sim_a_vol": 12.62, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.4, "pm_a": 6.93, "left": 140.0, "rv_5m": 2.25, "rv_15m": 2.19, "sim_a_vol": 1.37, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.55, "pm_a": 12.35, "left": 294.5, "rv_5m": 9.04, "rv_15m": 7.46, "sim_a_vol": 52.54, "direction": "UP", "won": false, "pnl_5sh": -2.75}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.58, "pm_a": 14.97, "left": 294.4, "rv_5m": 4.86, "rv_15m": 5.13, "sim_a_vol": 46.91, "direction": "UP", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": -17.68, "left": 140.0, "rv_5m": 2.39, "rv_15m": 3.1, "sim_a_vol": 4.85, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.85, "pm_a": 30.31, "left": 140.0, "rv_5m": 3.22, "rv_15m": 3.24, "sim_a_vol": 3.93, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.52, "pm_a": 1.78, "left": 242.0, "rv_5m": 4.75, "rv_15m": 3.77, "sim_a_vol": 21.55, "direction": "UP", "won": true, "pnl_5sh": 2.4}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": -53.07, "left": 140.0, "rv_5m": 2.36, "rv_15m": 2.8, "sim_a_vol": 4.93, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": -21.33, "left": 240.0, "rv_5m": 3.77, "rv_15m": 3.78, "sim_a_vol": 31.56, "direction": "DOWN", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.79, "pm_a": 15.47, "left": 140.0, "rv_5m": 1.15, "rv_15m": 1.25, "sim_a_vol": 2.25, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.72, "pm_a": -37.52, "left": 200.0, "rv_5m": 5.35, "rv_15m": 6.24, "sim_a_vol": 23.14, "direction": "DOWN", "won": false, "pnl_5sh": -3.6}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.53, "pm_a": 11.38, "left": 216.0, "rv_5m": 1.85, "rv_15m": 1.33, "sim_a_vol": 3.29, "direction": "UP", "won": false, "pnl_5sh": -2.65}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": 19.97, "left": 140.0, "rv_5m": 3.63, "rv_15m": 2.87, "sim_a_vol": 4.57, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.58, "pm_a": -22.49, "left": 140.0, "rv_5m": 73.22, "rv_15m": 42.63, "sim_a_vol": 9.67, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.95, "pm_a": 71.62, "left": 140.0, "rv_5m": 4.25, "rv_15m": 3.68, "sim_a_vol": 9.76, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_100", "ask": 0.6, "pm_a": 9.25, "left": 110.0, "rv_5m": 3.93, "rv_15m": 3.45, "sim_a_vol": 5.4, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -29.75, "left": 109.0, "rv_5m": 2.89, "rv_15m": 3.26, "sim_a_vol": 5.59, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.49, "pm_a": -1.43, "left": 188.0, "rv_5m": 3.07, "rv_15m": 3.69, "sim_a_vol": 2.02, "direction": "UP", "won": true, "pnl_5sh": 2.55}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.95, "pm_a": -65.45, "left": 140.0, "rv_5m": 2.64, "rv_15m": 2.97, "sim_a_vol": 2.4, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.56, "pm_a": 7.66, "left": 140.0, "rv_5m": 2.4, "rv_15m": 2.61, "sim_a_vol": 3.33, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.43, "pm_a": 4.05, "left": 150.0, "rv_5m": 1.89, "rv_15m": 2.35, "sim_a_vol": 4.37, "direction": "UP", "won": false, "pnl_5sh": -2.15}
|
||||||
|
{"strategy": "lock", "ask": 0.98, "pm_a": -62.92, "left": 71.0, "rv_5m": 2.59, "rv_15m": 2.72, "sim_a_vol": 13.07, "direction": "DOWN", "won": true, "pnl_5sh": 0.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.84, "pm_a": 35.25, "left": 140.0, "rv_5m": 6.21, "rv_15m": 7.16, "sim_a_vol": 11.39, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_hours", "ask": 0.45, "pm_a": -5.08, "left": 288.0, "rv_5m": 5.56, "rv_15m": 4.68, "sim_a_vol": 22.92, "direction": "UP", "won": true, "pnl_5sh": 2.75}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.34, "pm_a": 14.56, "left": 146.0, "rv_5m": 4.14, "rv_15m": 4.14, "sim_a_vol": 22.06, "direction": "DOWN", "won": false, "pnl_5sh": -1.7}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.64, "pm_a": 2.72, "left": 140.0, "rv_5m": 4.3, "rv_15m": 4.02, "sim_a_vol": 8.18, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "bounce", "ask": 0.75, "pm_a": -27.6, "left": 50.0, "rv_5m": 2.3, "rv_15m": 2.73, "sim_a_vol": 4.12, "direction": "DOWN", "won": false, "pnl_5sh": -3.75}
|
||||||
|
{"strategy": "last_flip", "ask": 0.94, "pm_a": -0.95, "left": 3.0, "rv_5m": 5.19, "rv_15m": 4.58, "sim_a_vol": 3.14, "direction": "DOWN", "won": true, "pnl_5sh": 0.3}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": 7.05, "left": 235.0, "rv_5m": 4.08, "rv_15m": 4.15, "sim_a_vol": 16.94, "direction": "UP", "won": false, "pnl_5sh": -2.8}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.58, "pm_a": -8.1, "left": 140.0, "rv_5m": 4.02, "rv_15m": 3.82, "sim_a_vol": 8.66, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 1.0, "pm_a": 0.0, "left": 299.8, "rv_5m": 7.39, "rv_15m": 8.2, "sim_a_vol": 14.23, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.53, "pm_a": -12.41, "left": 244.0, "rv_5m": 8.19, "rv_15m": 9.64, "sim_a_vol": 107.62, "direction": "DOWN", "won": true, "pnl_5sh": 2.35}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 31.15, "left": 140.0, "rv_5m": 1.6, "rv_15m": 2.09, "sim_a_vol": 10.94, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.12, "pm_a": 49.56, "left": 154.0, "rv_5m": 3.43, "rv_15m": 3.91, "sim_a_vol": 4.96, "direction": "DOWN", "won": false, "pnl_5sh": -0.6}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 108.77, "left": 140.0, "rv_5m": 3.88, "rv_15m": 5.55, "sim_a_vol": 10.98, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.34, "pm_a": 33.6, "left": 242.0, "rv_5m": 4.02, "rv_15m": 3.65, "sim_a_vol": 33.63, "direction": "DOWN", "won": true, "pnl_5sh": 3.3}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": -12.21, "left": 214.0, "rv_5m": 2.15, "rv_15m": 2.55, "sim_a_vol": 4.11, "direction": "DOWN", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": -3.44, "left": 246.0, "rv_5m": 5.03, "rv_15m": 6.8, "sim_a_vol": 46.38, "direction": "UP", "won": true, "pnl_5sh": 2.2}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.43, "pm_a": 29.52, "left": 249.0, "rv_5m": 3.37, "rv_15m": 3.66, "sim_a_vol": 19.42, "direction": "UP", "won": true, "pnl_5sh": 2.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -20.04, "left": 248.0, "rv_5m": 5.1, "rv_15m": 5.64, "sim_a_vol": 34.78, "direction": "DOWN", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.55, "pm_a": 0.61, "left": 231.0, "rv_5m": 3.61, "rv_15m": 3.91, "sim_a_vol": 7.61, "direction": "UP", "won": false, "pnl_5sh": -2.75}
|
||||||
|
{"strategy": "cb_flip", "ask": 0.19, "pm_a": -34.98, "left": 28.0, "rv_5m": 10.45, "rv_15m": 9.32, "sim_a_vol": 34.81, "direction": "UP", "won": false, "pnl_5sh": -1.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": 92.48, "left": 140.0, "rv_5m": 6.86, "rv_15m": 6.63, "sim_a_vol": 14.82, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.66, "pm_a": -11.89, "left": 140.0, "rv_5m": 3.1, "rv_15m": 2.82, "sim_a_vol": 4.26, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": -56.6, "left": 140.0, "rv_5m": 4.88, "rv_15m": 3.98, "sim_a_vol": 3.82, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "bounce", "ask": 0.73, "pm_a": -21.14, "left": 43.0, "rv_5m": 3.97, "rv_15m": 5.16, "sim_a_vol": 11.34, "direction": "DOWN", "won": true, "pnl_5sh": 1.35}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.94, "pm_a": 57.89, "left": 140.0, "rv_5m": 2.97, "rv_15m": 4.14, "sim_a_vol": 9.94, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -9.31, "left": 209.0, "rv_5m": 3.14, "rv_15m": 4.28, "sim_a_vol": 2.03, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 53.65, "left": 140.0, "rv_5m": 3.65, "rv_15m": 3.9, "sim_a_vol": 4.69, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -13.1, "left": 248.0, "rv_5m": 3.98, "rv_15m": 3.87, "sim_a_vol": 20.66, "direction": "DOWN", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "lock", "ask": 0.93, "pm_a": 78.16, "left": 140.0, "rv_5m": 5.65, "rv_15m": 6.05, "sim_a_vol": 20.72, "direction": "UP", "won": true, "pnl_5sh": 0.35}
|
||||||
|
{"strategy": "xgb_dir_50", "ask": 0.96, "pm_a": -82.25, "left": 60.0, "rv_5m": 5.58, "rv_15m": 7.97, "sim_a_vol": 11.78, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.74, "pm_a": -26.96, "left": 140.0, "rv_5m": 5.51, "rv_15m": 5.98, "sim_a_vol": 6.41, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.55, "pm_a": -11.73, "left": 182.0, "rv_5m": 2.94, "rv_15m": 2.46, "sim_a_vol": 15.15, "direction": "DOWN", "won": true, "pnl_5sh": 2.25}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 137.52, "left": 140.0, "rv_5m": 6.15, "rv_15m": 5.6, "sim_a_vol": 19.23, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.16, "pm_a": 19.73, "left": 26.0, "rv_5m": 36.74, "rv_15m": 33.82, "sim_a_vol": 137.31, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.37, "pm_a": -10.59, "left": 52.0, "rv_5m": 3.89, "rv_15m": 4.03, "sim_a_vol": 15.96, "direction": "UP", "won": false, "pnl_5sh": -1.85}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.57, "pm_a": 5.84, "left": 245.0, "rv_5m": 3.07, "rv_15m": 2.62, "sim_a_vol": 21.57, "direction": "UP", "won": true, "pnl_5sh": 2.15}
|
||||||
|
{"strategy": "clob_rev", "ask": 0.25, "pm_a": -40.78, "left": 53.8, "rv_5m": 5.91, "rv_15m": 6.94, "sim_a_vol": 17.62, "direction": "UP", "won": false, "pnl_5sh": -1.25}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": -26.93, "left": 140.0, "rv_5m": 3.71, "rv_15m": 4.04, "sim_a_vol": 6.39, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.65, "pm_a": 15.96, "left": 140.0, "rv_5m": 1.96, "rv_15m": 2.73, "sim_a_vol": 3.86, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_hours", "ask": 0.44, "pm_a": 10.82, "left": 294.0, "rv_5m": 3.6, "rv_15m": 3.51, "sim_a_vol": 13.13, "direction": "DOWN", "won": false, "pnl_5sh": -2.2}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.51, "pm_a": -4.81, "left": 294.6, "rv_5m": 3.29, "rv_15m": 4.33, "sim_a_vol": 6.68, "direction": "UP", "won": false, "pnl_5sh": -2.55}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": 6.03, "left": 117.0, "rv_5m": 3.84, "rv_15m": 3.68, "sim_a_vol": 12.24, "direction": "DOWN", "won": false, "pnl_5sh": -2.5}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.62, "pm_a": 10.4, "left": 140.0, "rv_5m": 3.2, "rv_15m": 3.5, "sim_a_vol": 8.49, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 35.81, "left": 140.0, "rv_5m": 1.28, "rv_15m": 2.94, "sim_a_vol": 10.17, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.96, "pm_a": -60.06, "left": 58.0, "rv_5m": 4.8, "rv_15m": 4.53, "sim_a_vol": 11.29, "direction": "DOWN", "won": true, "pnl_5sh": 0.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.78, "pm_a": -17.36, "left": 140.0, "rv_5m": 2.36, "rv_15m": 2.59, "sim_a_vol": 4.73, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.59, "pm_a": 4.35, "left": 31.0, "rv_5m": 2.58, "rv_15m": 2.0, "sim_a_vol": 7.68, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -110.58, "left": 140.0, "rv_5m": 5.73, "rv_15m": 4.51, "sim_a_vol": 33.09, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": 52.36, "left": 140.0, "rv_5m": 4.02, "rv_15m": 4.49, "sim_a_vol": 10.15, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.59, "pm_a": -25.6, "left": 294.3, "rv_5m": 7.58, "rv_15m": 8.79, "sim_a_vol": 36.7, "direction": "DOWN", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.49, "pm_a": 2.02, "left": 124.0, "rv_5m": 2.79, "rv_15m": 3.68, "sim_a_vol": 1.1, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": 1.89, "left": 231.0, "rv_5m": 3.16, "rv_15m": 3.43, "sim_a_vol": 4.39, "direction": "UP", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.45, "pm_a": 3.75, "left": 247.0, "rv_5m": 4.16, "rv_15m": 4.09, "sim_a_vol": 25.64, "direction": "DOWN", "won": false, "pnl_5sh": -2.25}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.3, "pm_a": 47.99, "left": 268.0, "rv_5m": 5.4, "rv_15m": 5.4, "sim_a_vol": 14.55, "direction": "DOWN", "won": false, "pnl_5sh": -1.5}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.69, "pm_a": -29.64, "left": 280.0, "rv_5m": 2.69, "rv_15m": 4.97, "sim_a_vol": 7.35, "direction": "DOWN", "won": true, "pnl_5sh": 1.55}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 51.32, "left": 244.0, "rv_5m": 11.05, "rv_15m": 6.85, "sim_a_vol": 116.87, "direction": "UP", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "mid_flip_v2", "ask": 0.48, "pm_a": -9.47, "left": 125.0, "rv_5m": 4.3, "rv_15m": 3.82, "sim_a_vol": 8.14, "direction": "UP", "won": false, "pnl_5sh": -2.4}
|
||||||
|
{"strategy": "lock", "ask": 0.82, "pm_a": 73.96, "left": 131.0, "rv_5m": 7.55, "rv_15m": 9.95, "sim_a_vol": 9.58, "direction": "UP", "won": true, "pnl_5sh": 0.9}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.48, "pm_a": 4.58, "left": 136.0, "rv_5m": 3.09, "rv_15m": 5.57, "sim_a_vol": 9.97, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.58, "pm_a": -11.59, "left": 140.0, "rv_5m": 2.96, "rv_15m": 3.13, "sim_a_vol": 13.0, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.61, "pm_a": -10.89, "left": 140.0, "rv_5m": 4.71, "rv_15m": 3.91, "sim_a_vol": 10.59, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.37, "pm_a": 2.96, "left": 154.2, "rv_5m": 5.86, "rv_15m": 5.13, "sim_a_vol": 12.66, "direction": "DOWN", "won": false, "pnl_5sh": -1.85}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.91, "pm_a": -35.58, "left": 140.0, "rv_5m": 2.06, "rv_15m": 1.92, "sim_a_vol": 5.6, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": 14.01, "left": 140.0, "rv_5m": 2.29, "rv_15m": 3.49, "sim_a_vol": 8.51, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cb_flip", "ask": 0.98, "pm_a": 12.37, "left": 4.9, "rv_5m": 3.01, "rv_15m": 3.17, "sim_a_vol": 7.13, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.4, "pm_a": 40.24, "left": 227.0, "rv_5m": 9.14, "rv_15m": 7.59, "sim_a_vol": 18.0, "direction": "DOWN", "won": false, "pnl_5sh": -2.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.33, "pm_a": 23.86, "left": 140.0, "rv_5m": 5.48, "rv_15m": 5.45, "sim_a_vol": 13.91, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_50", "ask": 1.0, "pm_a": -161.29, "left": 60.0, "rv_5m": 4.97, "rv_15m": 4.63, "sim_a_vol": 16.01, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.53, "pm_a": -8.34, "left": 140.0, "rv_5m": 2.62, "rv_15m": 4.11, "sim_a_vol": 2.38, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": 54.19, "left": 140.0, "rv_5m": 2.66, "rv_15m": 3.04, "sim_a_vol": 10.48, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock_hedge", "ask": 0.09, "pm_a": -61.63, "left": 126.0, "rv_5m": 3.42, "rv_15m": 4.03, "sim_a_vol": 11.93, "direction": "UP", "won": false, "pnl_5sh": -0.45}
|
||||||
|
{"strategy": "lock", "ask": 0.94, "pm_a": 64.45, "left": 41.0, "rv_5m": 4.89, "rv_15m": 6.94, "sim_a_vol": 12.26, "direction": "UP", "won": true, "pnl_5sh": 0.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 89.14, "left": 140.0, "rv_5m": 3.11, "rv_15m": 3.54, "sim_a_vol": 7.99, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner_strict", "ask": 0.78, "pm_a": 15.99, "left": 59.0, "rv_5m": 2.9, "rv_15m": 2.89, "sim_a_vol": 4.24, "direction": "UP", "won": false, "pnl_5sh": -3.9}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.4, "pm_a": 7.16, "left": 187.0, "rv_5m": 4.05, "rv_15m": 3.76, "sim_a_vol": 1.53, "direction": "DOWN", "won": true, "pnl_5sh": 3.0}
|
||||||
|
{"strategy": "cheap_hours", "ask": 0.42, "pm_a": -31.8, "left": 295.0, "rv_5m": 2.6, "rv_15m": 2.71, "sim_a_vol": 11.8, "direction": "UP", "won": false, "pnl_5sh": -2.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.47, "pm_a": -6.78, "left": 139.0, "rv_5m": 4.58, "rv_15m": 5.8, "sim_a_vol": 8.76, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.31, "pm_a": -25.91, "left": 135.0, "rv_5m": 5.1, "rv_15m": 4.64, "sim_a_vol": 24.42, "direction": "UP", "won": false, "pnl_5sh": -1.55}
|
||||||
|
{"strategy": "lock", "ask": 0.92, "pm_a": -60.73, "left": 103.0, "rv_5m": 5.43, "rv_15m": 4.58, "sim_a_vol": 3.58, "direction": "DOWN", "won": true, "pnl_5sh": 0.4}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.72, "pm_a": 32.41, "left": 139.0, "rv_5m": 6.91, "rv_15m": 6.18, "sim_a_vol": 15.08, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.55, "pm_a": 15.94, "left": 294.1, "rv_5m": 5.74, "rv_15m": 6.94, "sim_a_vol": 11.23, "direction": "UP", "won": false, "pnl_5sh": -2.75}
|
||||||
|
{"strategy": "mid_flip_v2", "ask": 0.49, "pm_a": 2.44, "left": 124.0, "rv_5m": 3.74, "rv_15m": 4.28, "sim_a_vol": 5.19, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.47, "pm_a": -24.77, "left": 124.0, "rv_5m": 4.97, "rv_15m": 4.95, "sim_a_vol": 4.69, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.66, "pm_a": -32.25, "left": 140.0, "rv_5m": 6.57, "rv_15m": 8.06, "sim_a_vol": 8.83, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.18, "pm_a": 38.18, "left": 224.0, "rv_5m": 3.4, "rv_15m": 2.76, "sim_a_vol": 11.04, "direction": "DOWN", "won": true, "pnl_5sh": 4.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 87.33, "left": 140.0, "rv_5m": 4.27, "rv_15m": 3.18, "sim_a_vol": 7.13, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.27, "pm_a": 42.92, "left": 235.0, "rv_5m": 6.81, "rv_15m": 6.31, "sim_a_vol": 28.08, "direction": "DOWN", "won": false, "pnl_5sh": -1.35}
|
||||||
|
{"strategy": "cheap_winner_strict", "ask": 0.8, "pm_a": 24.45, "left": 39.0, "rv_5m": 1.63, "rv_15m": 2.52, "sim_a_vol": 1.57, "direction": "UP", "won": true, "pnl_5sh": 1.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.56, "pm_a": 9.5, "left": 140.0, "rv_5m": 5.01, "rv_15m": 5.72, "sim_a_vol": 3.76, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": 23.01, "left": 140.0, "rv_5m": 2.2, "rv_15m": 2.18, "sim_a_vol": 7.92, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -49.68, "left": 140.0, "rv_5m": 2.58, "rv_15m": 2.51, "sim_a_vol": 10.67, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.49, "pm_a": -3.7, "left": 140.0, "rv_5m": 6.48, "rv_15m": 16.93, "sim_a_vol": 20.65, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.8, "pm_a": 11.86, "left": 30.0, "rv_5m": 3.67, "rv_15m": 3.62, "sim_a_vol": 4.99, "direction": "UP", "won": true, "pnl_5sh": 1.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.61, "pm_a": 22.04, "left": 140.0, "rv_5m": 6.7, "rv_15m": 7.26, "sim_a_vol": 6.52, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.08, "pm_a": -32.04, "left": 127.0, "rv_5m": 1.99, "rv_15m": 2.52, "sim_a_vol": 8.44, "direction": "UP", "won": false, "pnl_5sh": -0.4}
|
||||||
|
{"strategy": "last7s", "ask": 1.0, "pm_a": 154.72, "left": 19.9, "rv_5m": 6.41, "rv_15m": 7.23, "sim_a_vol": 7.82, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "maker_bias", "ask": 0.58, "pm_a": -1.37, "left": 195.0, "rv_5m": 4.89, "rv_15m": 4.67, "sim_a_vol": 7.1, "direction": "DOWN", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.66, "pm_a": 11.48, "left": 140.0, "rv_5m": 2.43, "rv_15m": 2.43, "sim_a_vol": 7.56, "direction": "UP", "won": true, "pnl_5sh": 1.7}
|
||||||
|
{"strategy": "cheap_hours", "ask": 0.44, "pm_a": -3.33, "left": 202.0, "rv_5m": 6.71, "rv_15m": 7.61, "sim_a_vol": 6.54, "direction": "UP", "won": false, "pnl_5sh": -2.2}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.8, "pm_a": -37.96, "left": 140.0, "rv_5m": 4.41, "rv_15m": 4.07, "sim_a_vol": 7.29, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.63, "pm_a": -9.22, "left": 140.0, "rv_5m": 3.63, "rv_15m": 4.97, "sim_a_vol": 8.84, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.6, "pm_a": -11.54, "left": 294.6, "rv_5m": 6.26, "rv_15m": 6.25, "sim_a_vol": 15.22, "direction": "UP", "won": true, "pnl_5sh": 2.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": 0.28, "left": 107.0, "rv_5m": 2.37, "rv_15m": 2.74, "sim_a_vol": 6.3, "direction": "UP", "won": true, "pnl_5sh": 2.5}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.46, "pm_a": -12.19, "left": 118.0, "rv_5m": 2.29, "rv_15m": 2.86, "sim_a_vol": 7.2, "direction": "UP", "won": false, "pnl_5sh": -2.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.89, "pm_a": -47.47, "left": 140.0, "rv_5m": 4.27, "rv_15m": 3.95, "sim_a_vol": 4.04, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.83, "pm_a": -32.14, "left": 140.0, "rv_5m": 3.89, "rv_15m": 4.58, "sim_a_vol": 4.62, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.54, "pm_a": -0.87, "left": 194.0, "rv_5m": 7.68, "rv_15m": 5.92, "sim_a_vol": 16.82, "direction": "DOWN", "won": false, "pnl_5sh": -2.7}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.33, "pm_a": 18.55, "left": 284.0, "rv_5m": 5.72, "rv_15m": 5.14, "sim_a_vol": 9.54, "direction": "DOWN", "won": false, "pnl_5sh": -1.65}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.77, "pm_a": 15.49, "left": 140.0, "rv_5m": 2.03, "rv_15m": 2.21, "sim_a_vol": 7.88, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.66, "pm_a": 6.39, "left": 120.0, "rv_5m": 4.04, "rv_15m": 3.68, "sim_a_vol": 16.91, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner_v2", "ask": 0.58, "pm_a": -10.85, "left": 59.0, "rv_5m": 1.99, "rv_15m": 2.08, "sim_a_vol": 4.32, "direction": "DOWN", "won": false, "pnl_5sh": -2.9}
|
||||||
|
{"strategy": "mid_flip", "ask": 0.39, "pm_a": -9.21, "left": 149.0, "rv_5m": 3.89, "rv_15m": 3.75, "sim_a_vol": 17.49, "direction": "UP", "won": true, "pnl_5sh": 3.05}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.41, "pm_a": 11.7, "left": 140.0, "rv_5m": 1.5, "rv_15m": 1.3, "sim_a_vol": 1.43, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.38, "pm_a": 30.4, "left": 264.7, "rv_5m": 5.37, "rv_15m": 4.78, "sim_a_vol": 6.56, "direction": "DOWN", "won": false, "pnl_5sh": -1.9}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": -3.53, "left": 250.0, "rv_5m": 4.58, "rv_15m": 3.69, "sim_a_vol": 7.06, "direction": "DOWN", "won": false, "pnl_5sh": -2.5}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.16, "pm_a": -34.18, "left": 237.0, "rv_5m": 2.04, "rv_15m": 2.81, "sim_a_vol": 9.26, "direction": "UP", "won": false, "pnl_5sh": -0.8}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -27.67, "left": 250.0, "rv_5m": 9.12, "rv_15m": 7.31, "sim_a_vol": 99.56, "direction": "DOWN", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.55, "pm_a": -2.42, "left": 51.0, "rv_5m": 4.8, "rv_15m": 5.4, "sim_a_vol": 13.92, "direction": "DOWN", "won": false, "pnl_5sh": -2.75}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.66, "pm_a": 3.73, "left": 133.0, "rv_5m": 2.04, "rv_15m": 1.92, "sim_a_vol": 1.5, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": 5.17, "left": 140.0, "rv_5m": 1.48, "rv_15m": 2.11, "sim_a_vol": 1.04, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.82, "pm_a": 2.18, "left": 140.0, "rv_5m": 3.17, "rv_15m": 3.15, "sim_a_vol": 2.99, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.48, "pm_a": 9.72, "left": 140.0, "rv_5m": 2.73, "rv_15m": 4.78, "sim_a_vol": 2.85, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.49, "pm_a": 0.94, "left": 240.0, "rv_5m": 14.08, "rv_15m": 10.42, "sim_a_vol": 36.64, "direction": "UP", "won": false, "pnl_5sh": -2.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.84, "pm_a": -36.28, "left": 140.0, "rv_5m": 3.56, "rv_15m": 3.27, "sim_a_vol": 11.63, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.9, "pm_a": 50.31, "left": 124.1, "rv_5m": 4.54, "rv_15m": 4.19, "sim_a_vol": 9.85, "direction": "UP", "won": true, "pnl_5sh": 0.5}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.79, "pm_a": -28.78, "left": 140.0, "rv_5m": 3.67, "rv_15m": 2.82, "sim_a_vol": 9.95, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.62, "pm_a": -13.32, "left": 140.0, "rv_5m": 3.37, "rv_15m": 3.03, "sim_a_vol": 1.93, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.48, "pm_a": -2.72, "left": 237.0, "rv_5m": 2.01, "rv_15m": 1.91, "sim_a_vol": 5.52, "direction": "UP", "won": false, "pnl_5sh": -2.4}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.66, "pm_a": 70.12, "left": 295.0, "rv_5m": 11.42, "rv_15m": 12.44, "sim_a_vol": 50.14, "direction": "UP", "won": true, "pnl_5sh": 1.7}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": -21.25, "left": 250.0, "rv_5m": 7.0, "rv_15m": 5.11, "sim_a_vol": 77.42, "direction": "DOWN", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 90.89, "left": 140.0, "rv_5m": 2.61, "rv_15m": 3.25, "sim_a_vol": 10.8, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.38, "pm_a": 9.55, "left": 140.0, "rv_5m": 2.17, "rv_15m": 2.12, "sim_a_vol": 3.53, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.32, "pm_a": -11.21, "left": 286.0, "rv_5m": 4.43, "rv_15m": 4.27, "sim_a_vol": 13.98, "direction": "UP", "won": true, "pnl_5sh": 3.4}
|
||||||
|
{"strategy": "bounce", "ask": 0.77, "pm_a": -44.49, "left": 49.0, "rv_5m": 6.62, "rv_15m": 5.98, "sim_a_vol": 11.98, "direction": "DOWN", "won": true, "pnl_5sh": 1.15}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.93, "pm_a": 67.12, "left": 140.0, "rv_5m": 4.09, "rv_15m": 3.82, "sim_a_vol": 6.95, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.8, "pm_a": -11.2, "left": 140.0, "rv_5m": 2.33, "rv_15m": 2.52, "sim_a_vol": 3.99, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.76, "pm_a": -60.37, "left": 140.0, "rv_5m": 7.29, "rv_15m": 9.39, "sim_a_vol": 10.67, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.58, "pm_a": 17.97, "left": 250.0, "rv_5m": 4.54, "rv_15m": 4.21, "sim_a_vol": 12.78, "direction": "UP", "won": true, "pnl_5sh": 2.1}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.49, "pm_a": -1.2, "left": 232.0, "rv_5m": 6.5, "rv_15m": 7.46, "sim_a_vol": 14.67, "direction": "UP", "won": false, "pnl_5sh": -2.45}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.56, "pm_a": -11.81, "left": 140.0, "rv_5m": 2.0, "rv_15m": 2.52, "sim_a_vol": 3.83, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 1.0, "pm_a": -178.58, "left": 140.0, "rv_5m": 5.83, "rv_15m": 4.4, "sim_a_vol": 24.94, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.49, "pm_a": 11.86, "left": 30.0, "rv_5m": 6.12, "rv_15m": 8.63, "sim_a_vol": 6.35, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.81, "pm_a": 20.59, "left": 140.0, "rv_5m": 2.97, "rv_15m": 3.19, "sim_a_vol": 16.09, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.98, "pm_a": 52.44, "left": 42.3, "rv_5m": 4.76, "rv_15m": 4.12, "sim_a_vol": 5.17, "direction": "UP", "won": true, "pnl_5sh": 0.1}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.56, "pm_a": 8.01, "left": 140.0, "rv_5m": 3.42, "rv_15m": 3.26, "sim_a_vol": 5.59, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cb_confirm", "ask": 0.75, "pm_a": 29.74, "left": 241.0, "rv_5m": 4.64, "rv_15m": 4.48, "sim_a_vol": 12.16, "direction": "UP", "won": false, "pnl_5sh": -3.75}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.68, "pm_a": 34.25, "left": 140.0, "rv_5m": 6.67, "rv_15m": 6.5, "sim_a_vol": 8.99, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.81, "pm_a": -54.68, "left": 140.0, "rv_5m": 7.94, "rv_15m": 8.97, "sim_a_vol": 6.7, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "last7s", "ask": 1.0, "pm_a": 114.48, "left": 20.0, "rv_5m": 6.91, "rv_15m": 7.54, "sim_a_vol": 23.92, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.87, "pm_a": -52.5, "left": 140.0, "rv_5m": 6.93, "rv_15m": 6.36, "sim_a_vol": 7.74, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "cheap_winner", "ask": 0.63, "pm_a": 16.48, "left": 55.0, "rv_5m": 3.65, "rv_15m": 6.95, "sim_a_vol": 10.93, "direction": "UP", "won": false, "pnl_5sh": -3.15}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -128.98, "left": 140.0, "rv_5m": 6.21, "rv_15m": 5.87, "sim_a_vol": 42.82, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "hold7s", "ask": 0.81, "pm_a": 10.41, "left": 10.0, "rv_5m": 5.39, "rv_15m": 6.36, "sim_a_vol": 9.58, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 6.2, "left": 236.0, "rv_5m": 1.37, "rv_15m": 1.34, "sim_a_vol": 3.59, "direction": "UP", "won": false, "pnl_5sh": -2.95}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.63, "pm_a": 9.99, "left": 140.0, "rv_5m": 1.84, "rv_15m": 2.1, "sim_a_vol": 3.08, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.52, "pm_a": 2.43, "left": 139.0, "rv_5m": 6.39, "rv_15m": 6.13, "sim_a_vol": 36.51, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.67, "pm_a": -19.42, "left": 139.0, "rv_5m": 3.28, "rv_15m": 3.49, "sim_a_vol": 5.93, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": 9.48, "left": 235.0, "rv_5m": 2.75, "rv_15m": 3.53, "sim_a_vol": 7.66, "direction": "DOWN", "won": false, "pnl_5sh": -2.5}
|
||||||
|
{"strategy": "cb_flip", "ask": 0.03, "pm_a": -11.49, "left": 3.0, "rv_5m": 3.21, "rv_15m": 3.98, "sim_a_vol": 7.48, "direction": "UP", "won": false, "pnl_5sh": -1.15}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.79, "pm_a": -21.57, "left": 140.0, "rv_5m": 4.29, "rv_15m": 3.68, "sim_a_vol": 8.88, "direction": "DOWN", "won": false, "pnl_5sh": -3.95}
|
||||||
|
{"strategy": "cb_flip", "ask": 0.9, "pm_a": 18.44, "left": 30.0, "rv_5m": 3.17, "rv_15m": 3.19, "sim_a_vol": 4.3, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.94, "pm_a": 109.64, "left": 140.0, "rv_5m": 8.32, "rv_15m": 8.54, "sim_a_vol": 14.22, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.87, "pm_a": 37.88, "left": 140.0, "rv_5m": 2.02, "rv_15m": 2.12, "sim_a_vol": 2.55, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.91, "pm_a": -63.65, "left": 140.0, "rv_5m": 5.04, "rv_15m": 5.54, "sim_a_vol": 9.08, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.5, "pm_a": -8.03, "left": 193.0, "rv_5m": 3.16, "rv_15m": 3.38, "sim_a_vol": 7.87, "direction": "UP", "won": true, "pnl_5sh": 2.5}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -75.46, "left": 140.0, "rv_5m": 4.79, "rv_15m": 4.31, "sim_a_vol": 6.39, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.32, "pm_a": 33.71, "left": 210.0, "rv_5m": 3.7, "rv_15m": 3.67, "sim_a_vol": 7.78, "direction": "DOWN", "won": false, "pnl_5sh": -1.6}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.98, "pm_a": 123.27, "left": 140.0, "rv_5m": 5.13, "rv_15m": 5.67, "sim_a_vol": 16.64, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.54, "pm_a": -0.38, "left": 246.0, "rv_5m": 1.62, "rv_15m": 2.02, "sim_a_vol": 6.91, "direction": "DOWN", "won": true, "pnl_5sh": 2.3}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.64, "pm_a": 8.77, "left": 140.0, "rv_5m": 2.87, "rv_15m": 3.02, "sim_a_vol": 5.89, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.71, "pm_a": -8.54, "left": 140.0, "rv_5m": 1.97, "rv_15m": 2.13, "sim_a_vol": 1.57, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.53, "pm_a": 8.45, "left": 140.0, "rv_5m": 2.87, "rv_15m": 2.78, "sim_a_vol": 15.43, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.69, "pm_a": -41.2, "left": 140.0, "rv_5m": 4.05, "rv_15m": 3.8, "sim_a_vol": 4.74, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.9, "pm_a": -86.43, "left": 140.0, "rv_5m": 7.94, "rv_15m": 6.11, "sim_a_vol": 23.1, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.42, "pm_a": 30.98, "left": 212.0, "rv_5m": 10.25, "rv_15m": 8.73, "sim_a_vol": 17.37, "direction": "DOWN", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.59, "pm_a": 34.18, "left": 213.0, "rv_5m": 0.0, "rv_15m": 0.0, "sim_a_vol": 47.77, "direction": "UP", "won": true, "pnl_5sh": 2.05}
|
||||||
|
{"strategy": "open_hedge", "ask": 0.64, "pm_a": -21.08, "left": 294.2, "rv_5m": 3.97, "rv_15m": 4.62, "sim_a_vol": 10.83, "direction": "DOWN", "won": false, "pnl_5sh": -3.2}
|
||||||
|
{"strategy": "open_hedge", "ask": 1.0, "pm_a": 0.0, "left": 299.6, "rv_5m": 7.16, "rv_15m": 7.96, "sim_a_vol": 27.76, "direction": "DOWN", "won": false, "pnl_5sh": -5.0}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.34, "pm_a": -5.22, "left": 276.0, "rv_5m": 5.25, "rv_15m": 5.25, "sim_a_vol": 7.9, "direction": "UP", "won": false, "pnl_5sh": -1.7}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.34, "pm_a": 16.69, "left": 206.0, "rv_5m": 2.65, "rv_15m": 3.33, "sim_a_vol": 2.05, "direction": "DOWN", "won": false, "pnl_5sh": -1.7}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.99, "pm_a": -83.42, "left": 140.0, "rv_5m": 3.16, "rv_15m": 2.96, "sim_a_vol": 2.08, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "lock", "ask": 0.92, "pm_a": 71.87, "left": 20.0, "rv_5m": 3.84, "rv_15m": 4.02, "sim_a_vol": 17.33, "direction": "UP", "won": true, "pnl_5sh": 0.0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.56, "pm_a": 20.21, "left": 250.0, "rv_5m": 4.33, "rv_15m": 3.01, "sim_a_vol": 10.49, "direction": "UP", "won": false, "pnl_5sh": -2.8}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.49, "pm_a": -4.31, "left": 234.0, "rv_5m": 4.63, "rv_15m": 4.13, "sim_a_vol": 7.42, "direction": "DOWN", "won": true, "pnl_5sh": 2.55}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -40.58, "left": 140.0, "rv_5m": 1.24, "rv_15m": 1.77, "sim_a_vol": 11.79, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.74, "pm_a": -23.56, "left": 140.0, "rv_5m": 3.0, "rv_15m": 3.21, "sim_a_vol": 6.2, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.89, "pm_a": 41.94, "left": 140.0, "rv_5m": 1.98, "rv_15m": 1.66, "sim_a_vol": 1.89, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.7, "pm_a": -13.97, "left": 140.0, "rv_5m": 2.03, "rv_15m": 2.17, "sim_a_vol": 6.59, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.99, "pm_a": -72.04, "left": 140.0, "rv_5m": 3.55, "rv_15m": 3.3, "sim_a_vol": 4.58, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.94, "pm_a": 78.91, "left": 140.0, "rv_5m": 4.94, "rv_15m": 4.35, "sim_a_vol": 7.91, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.96, "pm_a": -73.86, "left": 140.0, "rv_5m": 3.24, "rv_15m": 3.82, "sim_a_vol": 7.07, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.58, "pm_a": -10.57, "left": 140.0, "rv_5m": 8.13, "rv_15m": 7.24, "sim_a_vol": 11.55, "direction": "DOWN", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": 32.47, "left": 140.0, "rv_5m": 4.23, "rv_15m": 4.16, "sim_a_vol": 5.56, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.73, "pm_a": 25.04, "left": 140.0, "rv_5m": 2.7, "rv_15m": 3.1, "sim_a_vol": 3.0, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.31, "pm_a": -25.81, "left": 174.0, "rv_5m": 5.15, "rv_15m": 7.72, "sim_a_vol": 12.96, "direction": "UP", "won": false, "pnl_5sh": -1.55}
|
||||||
|
{"strategy": "depth_cheap", "ask": 0.57, "pm_a": -4.6, "left": 250.0, "rv_5m": 6.08, "rv_15m": 5.83, "sim_a_vol": 46.0, "direction": "DOWN", "won": true, "pnl_5sh": 2.15}
|
||||||
|
{"strategy": "spread_arb", "ask": 0.34, "pm_a": 59.55, "left": 282.0, "rv_5m": 5.67, "rv_15m": 6.97, "sim_a_vol": 32.24, "direction": "DOWN", "won": false, "pnl_5sh": -1.7}
|
||||||
|
{"strategy": "xgb_dir_140", "ask": 0.75, "pm_a": 32.64, "left": 140.0, "rv_5m": 5.97, "rv_15m": 17.17, "sim_a_vol": 17.44, "direction": "UP", "won": false, "pnl_5sh": 0}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Train layered XGBoost direction models at different time points.
|
||||||
|
|
||||||
|
Layer 1: Predict UP/DOWN direction (no ask as feature)
|
||||||
|
Layer 2: Use confidence + ask to decide if trade is worth it
|
||||||
|
|
||||||
|
Saves models to models/direction_left{N}.json for use in paper strategy.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
cd ~/btc_15m_collab/rewrite
|
||||||
|
source .venv/bin/activate
|
||||||
|
python3 tools/xgb_direction_train.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import xgboost as xgb
|
||||||
|
from sklearn.metrics import roc_auc_score
|
||||||
|
from collections import defaultdict
|
||||||
|
import os
|
||||||
|
|
||||||
|
SNAPSHOT_FILE = "data/chainlink_predictor/snapshots.jsonl"
|
||||||
|
SUMMARY_FILE = "data/chainlink_predictor/window_summary.jsonl"
|
||||||
|
MODEL_DIR = "models"
|
||||||
|
|
||||||
|
FEATURE_NAMES = [
|
||||||
|
'abs_pm_a', 'pm_a_accel', 'cb_a_abs', 'bn_a_abs', 'edge',
|
||||||
|
'cb_same', 'bn_same', 'cb_stronger', 'bid_ratio',
|
||||||
|
'vol_ratio', 'total_vol', 'vol_growth', 'rv_5m', 'trades',
|
||||||
|
'open_vol_ratio', 'open_matches', 'diff_abs',
|
||||||
|
'pm_a_velocity', # pts per 10s — how fast pm_a built up
|
||||||
|
'pm_a_from_peak', # current |pm_a| / max |pm_a| this window (1.0=at peak, <1=dropping)
|
||||||
|
'ask_from_peak', # current ask / max ask this window (1.0=at peak, <1=dropping)
|
||||||
|
'direction_flips', # how many times pm_a flipped sign this window
|
||||||
|
'time_at_direction', # seconds pm_a has been in current direction
|
||||||
|
# --- BTC market state (cross-window) ---
|
||||||
|
'rv_ratio', # rv_5m / rv_15m — short-term vol expanding? >1 = heating up
|
||||||
|
'dvol', # Deribit implied vol — market fear/calm
|
||||||
|
'funding', # funding rate — long/short sentiment
|
||||||
|
'oi_chg', # open interest change — money flowing in/out
|
||||||
|
'cb_vol_60s', # CB spot volume last 60s
|
||||||
|
'vol_spike', # volume spike indicator
|
||||||
|
'flip_rate', # price flip rate — choppy vs trending
|
||||||
|
'left_sec', # seconds left in window — small pm_a at left=10 is valuable, not at left=140
|
||||||
|
'pm_a_z', # |pm_a| / rv_5m — volatility-normalized strength
|
||||||
|
'pm_a_vol_z', # |pm_a| / cb_vol_60s — volume-normalized strength
|
||||||
|
]
|
||||||
|
|
||||||
|
LEFT_TARGETS = [140, 50, 10] # Three models: trend (140s) + confirmation (50s) + last-second (10s)
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
outcomes = {}
|
||||||
|
with open(SUMMARY_FILE) as f:
|
||||||
|
for l in f:
|
||||||
|
try:
|
||||||
|
w = json.loads(l)
|
||||||
|
outcomes[str(w['window_id'])] = w.get('pm_direction', '')
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
print("Loading snapshots...")
|
||||||
|
windows = defaultdict(list)
|
||||||
|
with open(SNAPSHOT_FILE, 'rb') as f:
|
||||||
|
f.seek(0, 2)
|
||||||
|
sz = f.tell()
|
||||||
|
for offset in range(0, sz, 200_000_000):
|
||||||
|
f.seek(offset)
|
||||||
|
f.readline()
|
||||||
|
chunk = f.read(200_000_000)
|
||||||
|
for line in chunk.split(b'\n'):
|
||||||
|
if not line: continue
|
||||||
|
try:
|
||||||
|
s = json.loads(line)
|
||||||
|
wid = str(s.get('window_id', ''))
|
||||||
|
if wid not in outcomes: continue
|
||||||
|
clob = s.get('clob', {})
|
||||||
|
ob = s.get('ob', {})
|
||||||
|
cb_ob = ob.get('coinbase', {})
|
||||||
|
bn_ob = ob.get('binance', {})
|
||||||
|
windows[wid].append({
|
||||||
|
'left': s.get('left_sec', 0),
|
||||||
|
'pm_a': s.get('pm_a', 0),
|
||||||
|
'diff': s.get('diff', 0),
|
||||||
|
'rv_5m': s.get('rv_5m', 0),
|
||||||
|
'rv_15m': s.get('rv_15m', 0),
|
||||||
|
'up_ask': clob.get('up_ask', 0),
|
||||||
|
'down_ask': clob.get('down_ask', 0),
|
||||||
|
'up_vol': clob.get('up_buy_usdc', 0),
|
||||||
|
'down_vol': clob.get('down_buy_usdc', 0),
|
||||||
|
'trades': clob.get('trades', 0),
|
||||||
|
'cb_gap': cb_ob.get('gap', 0) if cb_ob else 0,
|
||||||
|
'cb_bid': cb_ob.get('bid', 0) if cb_ob else 0,
|
||||||
|
'cb_ask': cb_ob.get('ask', 0) if cb_ob else 0,
|
||||||
|
'bn_gap': bn_ob.get('gap', 0) if bn_ob else 0,
|
||||||
|
'dvol': s.get('dvol', 0),
|
||||||
|
'funding': s.get('funding', 0),
|
||||||
|
'oi_chg': s.get('oi_chg', 0),
|
||||||
|
'cb_vol_60s': s.get('cb_vol_60s', 0),
|
||||||
|
'vol_spike': s.get('vol_spike', 0),
|
||||||
|
'flip_rate': s.get('flip_rate', 0),
|
||||||
|
})
|
||||||
|
except: pass
|
||||||
|
print(f'Windows: {len(windows)}')
|
||||||
|
return windows, outcomes
|
||||||
|
|
||||||
|
|
||||||
|
def build_features(snaps, left_target, wid_int=0, prev_dirs=None):
|
||||||
|
obs = [s for s in snaps if abs(s['left'] - left_target) < 5]
|
||||||
|
if not obs: return None
|
||||||
|
s = obs[0]
|
||||||
|
|
||||||
|
if abs(s['pm_a']) < 2: return None
|
||||||
|
if s['up_ask'] <= 0 or s['down_ask'] <= 0: return None
|
||||||
|
|
||||||
|
direction = 1 if s['pm_a'] > 0 else -1
|
||||||
|
|
||||||
|
earlier = [x for x in snaps if abs(x['left'] - (s['left'] + 30)) < 5]
|
||||||
|
if earlier:
|
||||||
|
e = earlier[0]
|
||||||
|
pm_a_accel = abs(s['pm_a']) - abs(e['pm_a'])
|
||||||
|
e_up_vol = e['up_vol']
|
||||||
|
e_down_vol = e['down_vol']
|
||||||
|
else:
|
||||||
|
pm_a_accel = 0
|
||||||
|
e_up_vol = 0
|
||||||
|
e_down_vol = 0
|
||||||
|
|
||||||
|
total_vol = s['up_vol'] + s['down_vol']
|
||||||
|
our_vol = s['up_vol'] if direction == 1 else s['down_vol']
|
||||||
|
vol_ratio = our_vol / (total_vol + 1)
|
||||||
|
|
||||||
|
e_total = e_up_vol + e_down_vol
|
||||||
|
vol_growth = total_vol / (e_total + 1) if e_total > 10 else 0
|
||||||
|
|
||||||
|
cb_a = s['cb_gap']
|
||||||
|
bn_a = s['bn_gap']
|
||||||
|
edge = cb_a - s['pm_a']
|
||||||
|
|
||||||
|
cb_same = 1 if (cb_a > 0 and s['pm_a'] > 0) or (cb_a < 0 and s['pm_a'] < 0) else 0
|
||||||
|
bn_same = 1 if (bn_a > 0 and s['pm_a'] > 0) or (bn_a < 0 and s['pm_a'] < 0) else 0
|
||||||
|
|
||||||
|
cb_total = s['cb_bid'] + s['cb_ask']
|
||||||
|
bid_ratio = s['cb_bid'] / (cb_total + 0.01) if cb_total > 0.1 else 0.5
|
||||||
|
|
||||||
|
first_snaps = [x for x in snaps if x['left'] > 260]
|
||||||
|
if first_snaps:
|
||||||
|
f = first_snaps[-1]
|
||||||
|
open_vol_dir = 1 if f['up_vol'] > f['down_vol'] else (-1 if f['down_vol'] > f['up_vol'] else 0)
|
||||||
|
open_vol_ratio = max(f['up_vol'], f['down_vol']) / (min(f['up_vol'], f['down_vol']) + 1) if f['up_vol'] + f['down_vol'] > 50 else 0
|
||||||
|
open_matches = 1 if open_vol_dir == direction else 0
|
||||||
|
else:
|
||||||
|
open_vol_ratio = 0
|
||||||
|
open_matches = 0
|
||||||
|
|
||||||
|
# All snaps before observation point (for trajectory analysis)
|
||||||
|
snaps_by_time = sorted(snaps, key=lambda x: -x['left'])
|
||||||
|
before = [x for x in snaps_by_time if x['left'] >= s['left']]
|
||||||
|
|
||||||
|
# pm_a velocity: how fast did pm_a build up (pts per 10s)
|
||||||
|
pm_a_velocity = 0
|
||||||
|
current_pm_a = abs(s['pm_a'])
|
||||||
|
if current_pm_a >= 15:
|
||||||
|
start_left = None
|
||||||
|
for snap in before:
|
||||||
|
if snap['left'] <= s['left']: continue
|
||||||
|
if direction == 1 and snap['pm_a'] < 10:
|
||||||
|
start_left = snap['left']
|
||||||
|
break
|
||||||
|
elif direction == -1 and snap['pm_a'] > -10:
|
||||||
|
start_left = snap['left']
|
||||||
|
break
|
||||||
|
if start_left is not None:
|
||||||
|
build_time = start_left - s['left']
|
||||||
|
if build_time >= 5:
|
||||||
|
pm_a_velocity = (current_pm_a - 10) / build_time * 10
|
||||||
|
|
||||||
|
# pm_a from peak: is pm_a at its highest or dropping back?
|
||||||
|
if before:
|
||||||
|
max_abs_pma = max(abs(x['pm_a']) for x in before)
|
||||||
|
pm_a_from_peak = current_pm_a / (max_abs_pma + 0.1)
|
||||||
|
else:
|
||||||
|
pm_a_from_peak = 1.0
|
||||||
|
|
||||||
|
# ask from peak: is our ask at its highest or dropping?
|
||||||
|
our_ask_key = 'up_ask' if direction == 1 else 'down_ask'
|
||||||
|
our_asks = [x[our_ask_key] for x in before if x[our_ask_key] > 0]
|
||||||
|
if our_asks:
|
||||||
|
max_ask = max(our_asks)
|
||||||
|
current_ask = s[our_ask_key]
|
||||||
|
ask_from_peak = current_ask / (max_ask + 0.001) if max_ask > 0 else 1.0
|
||||||
|
else:
|
||||||
|
ask_from_peak = 1.0
|
||||||
|
|
||||||
|
# direction flips: how many times pm_a changed sign
|
||||||
|
direction_flips = 0
|
||||||
|
prev_sign = 0
|
||||||
|
for snap in before:
|
||||||
|
cur_sign = 1 if snap['pm_a'] > 0 else (-1 if snap['pm_a'] < 0 else 0)
|
||||||
|
if cur_sign != 0 and prev_sign != 0 and cur_sign != prev_sign:
|
||||||
|
direction_flips += 1
|
||||||
|
if cur_sign != 0:
|
||||||
|
prev_sign = cur_sign
|
||||||
|
|
||||||
|
# time at current direction: how long has pm_a been in current sign?
|
||||||
|
time_at_direction = 0
|
||||||
|
for snap in before:
|
||||||
|
if snap['left'] <= s['left']: continue
|
||||||
|
snap_dir = 1 if snap['pm_a'] > 0 else -1
|
||||||
|
if snap_dir == direction:
|
||||||
|
time_at_direction = snap['left'] - s['left']
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# rv ratio: short-term vol vs medium-term vol
|
||||||
|
rv_5 = s['rv_5m']
|
||||||
|
rv_15 = s.get('rv_15m', 0)
|
||||||
|
rv_ratio = rv_5 / (rv_15 + 0.01) if rv_15 > 0.1 else 1.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'abs_pm_a': abs(s['pm_a']),
|
||||||
|
'pm_a_accel': pm_a_accel,
|
||||||
|
'cb_a_abs': abs(cb_a),
|
||||||
|
'bn_a_abs': abs(bn_a),
|
||||||
|
'edge': edge * direction,
|
||||||
|
'cb_same': cb_same,
|
||||||
|
'bn_same': bn_same,
|
||||||
|
'cb_stronger': 1 if abs(cb_a) > abs(s['pm_a']) else 0,
|
||||||
|
'bid_ratio': bid_ratio,
|
||||||
|
'vol_ratio': vol_ratio,
|
||||||
|
'total_vol': total_vol,
|
||||||
|
'vol_growth': vol_growth,
|
||||||
|
'rv_5m': rv_5,
|
||||||
|
'trades': s['trades'],
|
||||||
|
'open_vol_ratio': open_vol_ratio,
|
||||||
|
'open_matches': open_matches,
|
||||||
|
'diff_abs': abs(s['diff']),
|
||||||
|
'pm_a_velocity': pm_a_velocity,
|
||||||
|
'pm_a_from_peak': pm_a_from_peak,
|
||||||
|
'ask_from_peak': ask_from_peak,
|
||||||
|
'direction_flips': direction_flips,
|
||||||
|
'time_at_direction': time_at_direction,
|
||||||
|
# BTC market state
|
||||||
|
'rv_ratio': rv_ratio,
|
||||||
|
'dvol': s.get('dvol', 0),
|
||||||
|
'funding': s.get('funding', 0),
|
||||||
|
'oi_chg': s.get('oi_chg', 0),
|
||||||
|
'cb_vol_60s': s.get('cb_vol_60s', 0),
|
||||||
|
'vol_spike': s.get('vol_spike', 0),
|
||||||
|
'flip_rate': s.get('flip_rate', 0),
|
||||||
|
'left_sec': s['left'],
|
||||||
|
'pm_a_z': abs(s['pm_a']) / (rv_5 + 0.1) if rv_5 > 0.5 else 0,
|
||||||
|
'pm_a_vol_z': abs(s['pm_a']) / (s.get('cb_vol_60s', 0) + 0.1) if s.get('cb_vol_60s', 0) > 0.5 else 0,
|
||||||
|
# Extra for paper trade logging (not used as feature)
|
||||||
|
'_up_ask': s['up_ask'],
|
||||||
|
'_down_ask': s['down_ask'],
|
||||||
|
'_pm_a': s['pm_a'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
windows, outcomes = load_data()
|
||||||
|
os.makedirs(MODEL_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Build consecutive-same-direction map
|
||||||
|
sorted_wids = sorted(windows.keys(), key=lambda x: int(x))
|
||||||
|
consec_map = {}
|
||||||
|
prev_outcome = ''
|
||||||
|
consec = 0
|
||||||
|
for wid in sorted_wids:
|
||||||
|
outcome = outcomes.get(wid, '')
|
||||||
|
if not outcome: continue
|
||||||
|
if outcome == prev_outcome:
|
||||||
|
consec += 1
|
||||||
|
else:
|
||||||
|
consec = 0
|
||||||
|
consec_map[wid] = consec
|
||||||
|
prev_outcome = outcome
|
||||||
|
|
||||||
|
# Build recent_flips map — how many of last 5 windows had pm_a sign change during window
|
||||||
|
flip_map = {}
|
||||||
|
recent_flips = []
|
||||||
|
for wid in sorted_wids:
|
||||||
|
snaps = windows.get(wid, [])
|
||||||
|
if not snaps: continue
|
||||||
|
# Check if pm_a changed sign during this window
|
||||||
|
signs = [1 if s['pm_a'] > 5 else (-1 if s['pm_a'] < -5 else 0) for s in snaps]
|
||||||
|
signs = [s for s in signs if s != 0]
|
||||||
|
had_flip = 0
|
||||||
|
if len(signs) > 5:
|
||||||
|
for i in range(1, len(signs)):
|
||||||
|
if signs[i] != signs[i-1]:
|
||||||
|
had_flip = 1
|
||||||
|
break
|
||||||
|
recent_flips.append(had_flip)
|
||||||
|
flip_map[wid] = sum(recent_flips[-5:])
|
||||||
|
|
||||||
|
# Define sample ranges for each model
|
||||||
|
# Model 140: sample from left 20-140 (every 20s)
|
||||||
|
# Model 50: sample from left 10-50 (every 10s)
|
||||||
|
sample_points = {
|
||||||
|
140: list(range(20, 141, 20)), # [20, 40, 60, 80, 100, 120, 140]
|
||||||
|
50: list(range(10, 51, 10)), # [10, 20, 30, 40, 50]
|
||||||
|
10: list(range(3, 16, 3)), # [3, 6, 9, 12, 15] — last 15s snapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
for left_target in LEFT_TARGETS:
|
||||||
|
rows = []
|
||||||
|
points = sample_points.get(left_target, [left_target])
|
||||||
|
for wid, snaps in windows.items():
|
||||||
|
outcome = outcomes.get(wid, '')
|
||||||
|
if not outcome: continue
|
||||||
|
for pt in points:
|
||||||
|
feats = build_features(snaps, pt, wid_int=int(wid))
|
||||||
|
if feats is None: continue
|
||||||
|
direction_label = 'UP' if feats['_pm_a'] > 0 else 'DOWN'
|
||||||
|
feats['won'] = 1 if direction_label == outcome else 0
|
||||||
|
rows.append(feats)
|
||||||
|
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
if len(df) < 100: continue
|
||||||
|
|
||||||
|
X = df[FEATURE_NAMES]
|
||||||
|
y = df['won']
|
||||||
|
|
||||||
|
# Train on all data for production
|
||||||
|
model = xgb.XGBClassifier(
|
||||||
|
n_estimators=200, max_depth=4, learning_rate=0.05,
|
||||||
|
subsample=0.8, colsample_bytree=0.7,
|
||||||
|
eval_metric='logloss', random_state=42,
|
||||||
|
)
|
||||||
|
model.fit(X, y, verbose=False)
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
split = int(len(df) * 0.7)
|
||||||
|
X_test = X.iloc[split:]
|
||||||
|
y_test = y.iloc[split:]
|
||||||
|
proba = model.predict_proba(X_test)[:, 1]
|
||||||
|
auc = roc_auc_score(y_test, proba)
|
||||||
|
|
||||||
|
model_path = f"{MODEL_DIR}/direction_left{left_target}.json"
|
||||||
|
model.save_model(model_path)
|
||||||
|
print(f'LEFT={left_target}s: {len(df)} samples, AUC={auc:.3f} → saved {model_path}')
|
||||||
|
|
||||||
|
# Save feature names
|
||||||
|
with open(f"{MODEL_DIR}/direction_features.json", "w") as f:
|
||||||
|
json.dump(FEATURE_NAMES, f)
|
||||||
|
print(f'\nFeature names saved. Models ready for paper strategy.')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Offline XGBoost inspection — load the REAL trained models and report them.
|
||||||
|
|
||||||
|
python ml/xgb_eval.py
|
||||||
|
|
||||||
|
Needs only `xgboost` + `numpy` + the committed model files under models/. No live
|
||||||
|
feeds, no keys, no 16 GB of ticks. Prints each model's feature-importance ranking,
|
||||||
|
and — if a held-out sample is present at ml/data/lock_sample.csv — the test AUC.
|
||||||
|
|
||||||
|
The point this makes, straight from the model: the strongest features are the
|
||||||
|
market maker's OWN quote (ask, ask_margin, ask_vel), not BTC's price dynamics.
|
||||||
|
The model taught itself that the best estimate of the outcome is what the maker is
|
||||||
|
already charging — which is exactly why the edge isn't capturable. And the AUC is
|
||||||
|
deliberately weak (~coin-flip-plus): that weakness IS the finding. See the README.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import xgboost as xgb
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ROOT = os.path.dirname(HERE)
|
||||||
|
|
||||||
|
|
||||||
|
def _importances(name: str, model_file: str, feat_file: str, top: int = 8) -> None:
|
||||||
|
# Native Booster API — no sklearn needed (XGBClassifier would require it).
|
||||||
|
try:
|
||||||
|
booster = xgb.Booster()
|
||||||
|
booster.load_model(os.path.join(ROOT, "models", model_file))
|
||||||
|
with open(os.path.join(ROOT, "models", feat_file)) as f:
|
||||||
|
names = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [{name}] could not load ({e})")
|
||||||
|
return
|
||||||
|
gain = booster.get_score(importance_type="gain") # {feature: score}, unused feats absent
|
||||||
|
# if the model stored generic f0/f1 names, remap them onto the real feature list
|
||||||
|
if gain and all(k[1:].isdigit() for k in gain if k.startswith("f")):
|
||||||
|
gain = {names[int(k[1:])]: v for k, v in gain.items()
|
||||||
|
if k.startswith("f") and int(k[1:]) < len(names)} or gain
|
||||||
|
total = sum(gain.values()) or 1.0
|
||||||
|
ranked = sorted(((n, gain.get(n, 0.0) / total) for n in names), key=lambda x: -x[1])
|
||||||
|
print(f"\n{name} ({model_file}, {len(names)} features)")
|
||||||
|
top1 = ranked[0][1] or 1.0
|
||||||
|
for feat, w in ranked[:top]:
|
||||||
|
bar = "█" * int(round(w / top1 * 28)) if w > 0 else "·"
|
||||||
|
print(f" {feat:<14} {w:6.3f} {bar}")
|
||||||
|
|
||||||
|
|
||||||
|
def _economics_from_log() -> None:
|
||||||
|
"""The 'high win-rate trap', straight from real paper trades.
|
||||||
|
|
||||||
|
A trade filled at ask A pays (1 - A) on a win and loses A on a loss. Paying up
|
||||||
|
for a high win rate hands the payoff to the maker — high WR, negative PnL.
|
||||||
|
"""
|
||||||
|
path = os.path.join(HERE, "data", "paper_trades_sample.jsonl")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return
|
||||||
|
rows = []
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
d = json.loads(line)
|
||||||
|
if d.get("ask") is not None and d.get("won") is not None and d.get("pnl_5sh") is not None:
|
||||||
|
rows.append(d)
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
wr_all = sum(r["won"] for r in rows) / len(rows)
|
||||||
|
pnl_all = sum(r["pnl_5sh"] for r in rows) / len(rows)
|
||||||
|
print("\n" + "=" * 68)
|
||||||
|
print(f"Real paper trades — no price bucket is profitable ({len(rows)} trades, ml/data/)")
|
||||||
|
print("=" * 68)
|
||||||
|
print(" fill at ask A → a win pays (1-A), a loss costs A. Split by entry price:\n")
|
||||||
|
print(f" {'ask range':<11} {'n':>4} {'win rate':>9} {'mean PnL / 5sh':>15}")
|
||||||
|
for lo, hi in [(0.0, 0.5), (0.5, 0.7), (0.7, 0.85), (0.85, 1.01)]:
|
||||||
|
b = [r for r in rows if lo <= r["ask"] < hi]
|
||||||
|
if not b:
|
||||||
|
continue
|
||||||
|
wr = sum(r["won"] for r in b) / len(b)
|
||||||
|
pnl = sum(r["pnl_5sh"] for r in b) / len(b)
|
||||||
|
print(f" {lo:.2f}-{hi:<6.2f} {len(b):>4} {wr:>8.0%} {pnl:>+15.2f}")
|
||||||
|
print(f" {'ALL':<11} {len(rows):>4} {wr_all:>8.0%} {pnl_all:>+15.2f}")
|
||||||
|
print("\n Mean PnL is negative in every bucket — cheap or expensive, high win rate or")
|
||||||
|
print(" low. Across this cross-section of 29 strategies there is no entry price that")
|
||||||
|
print(" comes out ahead: the maker's spread plus execution costs take the rest.")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 68)
|
||||||
|
print("XGBoost models — feature importances (what the model actually learned)")
|
||||||
|
print("=" * 68)
|
||||||
|
_importances("Lock model — will the current leader hold to settlement?",
|
||||||
|
"lock_xgb.json", "lock_xgb_features.json")
|
||||||
|
_importances("Direction model (left=140s)", "direction_left140.json", "direction_features.json")
|
||||||
|
_importances("Hold-7s model", "direction_hold7s.json", "hold7s_features.json")
|
||||||
|
_economics_from_log()
|
||||||
|
print("\n" + "-" * 68)
|
||||||
|
print("Read the top features: `ask`, `ask_margin`, `ask_vel` are the maker's own")
|
||||||
|
print("price. The best predictor of the outcome is what the maker already charges —")
|
||||||
|
print("you can't beat a signal that IS the counterparty. See README, Pillar 2.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Train XGBoost lock model V2 — pre-trigger features only (no data leakage).
|
||||||
|
|
||||||
|
Targets ask<0.85 trades where the real edge is.
|
||||||
|
Features computed ONLY from snapshots BEFORE trigger moment.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
cd ~/btc_15m_collab/rewrite
|
||||||
|
source .venv/bin/activate
|
||||||
|
python3 tools/xgb_lock_train_v2.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import xgboost as xgb
|
||||||
|
from sklearn.metrics import roc_auc_score
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
PAPER_FILE = "data/chainlink_predictor/paper_trades.jsonl"
|
||||||
|
SNAPSHOT_FILE = "data/chainlink_predictor/snapshots.jsonl"
|
||||||
|
MODEL_PATH = "models/lock_xgb.json"
|
||||||
|
FEATURES_PATH = "models/lock_xgb_features.json"
|
||||||
|
|
||||||
|
# New feature set — all computable from pre-trigger snapshots
|
||||||
|
FEATURE_NAMES = [
|
||||||
|
"ask", # our side's CLOB ask at trigger
|
||||||
|
"abs_pm_a", # |pm_a| at trigger
|
||||||
|
"left", # seconds remaining when triggered
|
||||||
|
"rv_5m", # 5-min realized volatility
|
||||||
|
"rv_15m", # 15-min realized volatility
|
||||||
|
"sim_a_vol", # exchange price divergence
|
||||||
|
"ask_margin", # our_ask - opp_ask (CLOB directional confidence)
|
||||||
|
"pm_a_accel", # |pm_a| growth in last 30s before trigger
|
||||||
|
"ask_vel", # ask price change rate before trigger
|
||||||
|
"vol_ratio", # our volume / total volume
|
||||||
|
"total_vol", # total CLOB volume (USDC)
|
||||||
|
"sign_changes", # pm_a sign flips before trigger (regime)
|
||||||
|
"pm_a_std", # pm_a standard deviation (stability)
|
||||||
|
"diff", # |sim - pm| at trigger (CB-PM divergence proxy)
|
||||||
|
"trades_count", # CLOB trade count
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_snapshot_index():
|
||||||
|
"""Index snapshots by window_id."""
|
||||||
|
print("Loading snapshots...")
|
||||||
|
snaps_by_wid = defaultdict(list)
|
||||||
|
with open(SNAPSHOT_FILE, "rb") as f:
|
||||||
|
f.seek(0, 2)
|
||||||
|
sz = f.tell()
|
||||||
|
for offset in range(0, sz, 200_000_000):
|
||||||
|
f.seek(offset)
|
||||||
|
f.readline()
|
||||||
|
chunk = f.read(200_000_000)
|
||||||
|
for line in chunk.split(b"\n"):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
s = json.loads(line)
|
||||||
|
wid = str(s.get("window_id", ""))
|
||||||
|
if not wid:
|
||||||
|
continue
|
||||||
|
clob = s.get("clob", {})
|
||||||
|
snaps_by_wid[wid].append({
|
||||||
|
"left": s.get("left_sec", 0),
|
||||||
|
"pm_a": s.get("pm_a", 0),
|
||||||
|
"rv_5m": s.get("rv_5m", 0),
|
||||||
|
"rv_15m": s.get("rv_15m", 0),
|
||||||
|
"diff": s.get("diff", 0),
|
||||||
|
"sim_a_vol": s.get("sim_a_vol", 0),
|
||||||
|
"up_ask": clob.get("up_ask", 0),
|
||||||
|
"down_ask": clob.get("down_ask", 0),
|
||||||
|
"up_vol": clob.get("up_buy_usdc", 0),
|
||||||
|
"down_vol": clob.get("down_buy_usdc", 0),
|
||||||
|
"trades": clob.get("trades", 0),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(f" {len(snaps_by_wid)} windows indexed")
|
||||||
|
return snaps_by_wid
|
||||||
|
|
||||||
|
|
||||||
|
def compute_pretrigger_features(snaps, trigger_left, direction):
|
||||||
|
"""Compute features using ONLY snapshots BEFORE trigger moment."""
|
||||||
|
before = [s for s in snaps if s["left"] > trigger_left]
|
||||||
|
if len(before) < 5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
our_ask_key = "up_ask" if direction == "UP" else "down_ask"
|
||||||
|
opp_ask_key = "down_ask" if direction == "UP" else "up_ask"
|
||||||
|
|
||||||
|
at_trig = before[-1] # closest to trigger
|
||||||
|
our_ask = at_trig[our_ask_key]
|
||||||
|
opp_ask = at_trig[opp_ask_key]
|
||||||
|
|
||||||
|
our_asks = [s[our_ask_key] for s in before if s[our_ask_key] > 0]
|
||||||
|
if not our_asks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pma_vals = [abs(s["pm_a"]) for s in before]
|
||||||
|
|
||||||
|
# pm_a acceleration (last 30s before trigger vs earlier)
|
||||||
|
recent = [s for s in before if s["left"] <= trigger_left + 35]
|
||||||
|
earlier = [s for s in before if s["left"] > trigger_left + 35]
|
||||||
|
if recent and earlier:
|
||||||
|
pm_a_accel = np.mean([abs(s["pm_a"]) for s in recent]) - np.mean([abs(s["pm_a"]) for s in earlier])
|
||||||
|
ask_vel = (our_asks[-1] - our_asks[0]) / max(len(our_asks), 1) if len(our_asks) > 3 else 0
|
||||||
|
else:
|
||||||
|
pm_a_accel = 0
|
||||||
|
ask_vel = 0
|
||||||
|
|
||||||
|
# Volume
|
||||||
|
our_vol = at_trig["up_vol"] if direction == "UP" else at_trig["down_vol"]
|
||||||
|
opp_vol = at_trig["down_vol"] if direction == "UP" else at_trig["up_vol"]
|
||||||
|
total_vol = our_vol + opp_vol
|
||||||
|
vol_ratio = our_vol / (total_vol + 1)
|
||||||
|
|
||||||
|
# Regime
|
||||||
|
signs = np.sign([s["pm_a"] for s in before])
|
||||||
|
sign_changes = int(np.sum(np.diff(signs) != 0))
|
||||||
|
pm_a_std = float(np.std(pma_vals))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ask_margin": our_ask - opp_ask,
|
||||||
|
"pm_a_accel": pm_a_accel,
|
||||||
|
"ask_vel": ask_vel,
|
||||||
|
"vol_ratio": vol_ratio,
|
||||||
|
"total_vol": total_vol,
|
||||||
|
"sign_changes": sign_changes,
|
||||||
|
"pm_a_std": pm_a_std,
|
||||||
|
"rv_5m": at_trig["rv_5m"],
|
||||||
|
"rv_15m": at_trig.get("rv_15m", 0),
|
||||||
|
"sim_a_vol": at_trig["sim_a_vol"],
|
||||||
|
"diff": abs(at_trig["diff"]),
|
||||||
|
"trades_count": at_trig["trades"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(snaps_by_wid):
|
||||||
|
"""Load lock paper trades and compute pre-trigger features."""
|
||||||
|
rows = []
|
||||||
|
with open(PAPER_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
d = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if d.get("strategy") != "lock" or "won" not in d:
|
||||||
|
continue
|
||||||
|
if abs(d.get("pm_a", 0)) < 60:
|
||||||
|
continue
|
||||||
|
|
||||||
|
wid = str(d.get("window_id", ""))
|
||||||
|
snaps = snaps_by_wid.get(wid, [])
|
||||||
|
trigger_left = d.get("left", 140)
|
||||||
|
direction = d.get("direction", "UP")
|
||||||
|
|
||||||
|
feats = compute_pretrigger_features(snaps, trigger_left, direction)
|
||||||
|
if feats is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"won": int(bool(d["won"])),
|
||||||
|
"ask": d["ask"],
|
||||||
|
"abs_pm_a": abs(d["pm_a"]),
|
||||||
|
"left": trigger_left,
|
||||||
|
}
|
||||||
|
row.update(feats)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
snaps = build_snapshot_index()
|
||||||
|
df = load_data(snaps)
|
||||||
|
print(f"\nLoaded {len(df)} lock trades (pm_a>=60), WR={df['won'].mean():.1%}")
|
||||||
|
|
||||||
|
# Show ask distribution
|
||||||
|
cheap = df[df["ask"] < 0.85]
|
||||||
|
expensive = df[df["ask"] >= 0.85]
|
||||||
|
print(f" ask<0.85: {len(cheap)} trades, WR={cheap['won'].mean():.1%}")
|
||||||
|
print(f" ask>=0.85: {len(expensive)} trades, WR={expensive['won'].mean():.1%}")
|
||||||
|
|
||||||
|
# Train on ALL trades (model learns ask is important feature)
|
||||||
|
X = df[FEATURE_NAMES]
|
||||||
|
y = df["won"]
|
||||||
|
|
||||||
|
# Time-based split
|
||||||
|
split = int(len(df) * 0.7)
|
||||||
|
X_train, X_test = X.iloc[:split], X.iloc[split:]
|
||||||
|
y_train, y_test = y.iloc[:split], y.iloc[split:]
|
||||||
|
print(f"\nTrain: {len(X_train)} (WR={y_train.mean():.1%})")
|
||||||
|
print(f"Test: {len(X_test)} (WR={y_test.mean():.1%})")
|
||||||
|
|
||||||
|
model = xgb.XGBClassifier(
|
||||||
|
n_estimators=200, max_depth=3, learning_rate=0.05,
|
||||||
|
subsample=0.8, colsample_bytree=0.7,
|
||||||
|
eval_metric="logloss", random_state=42,
|
||||||
|
)
|
||||||
|
model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
|
||||||
|
|
||||||
|
proba = model.predict_proba(X_test)[:, 1]
|
||||||
|
auc = roc_auc_score(y_test, proba)
|
||||||
|
print(f"\nOut-of-sample AUC: {auc:.3f}")
|
||||||
|
|
||||||
|
# Threshold analysis — focus on ask<0.85 trades in test set
|
||||||
|
print(f"\nThreshold analysis (test set, ask<0.85 only):")
|
||||||
|
cheap_mask = X_test["ask"] < 0.85
|
||||||
|
cheap_proba = proba[cheap_mask]
|
||||||
|
cheap_y = y_test[cheap_mask]
|
||||||
|
cheap_X = X_test[cheap_mask]
|
||||||
|
print(f" Total cheap trades in test: {cheap_mask.sum()}")
|
||||||
|
for thresh in [0.50, 0.60, 0.70, 0.75, 0.80, 0.85, 0.90]:
|
||||||
|
keep = cheap_proba >= thresh
|
||||||
|
if keep.sum() == 0:
|
||||||
|
continue
|
||||||
|
wr = cheap_y[keep].mean()
|
||||||
|
n = keep.sum()
|
||||||
|
block_n = (~keep).sum()
|
||||||
|
asks = cheap_X.loc[keep, "ask"]
|
||||||
|
pnl = sum((1 - asks.iloc[i]) * 5 if cheap_y[keep].iloc[i] else -asks.iloc[i] * 5 for i in range(n))
|
||||||
|
print(f" >={thresh:.2f}: KEEP {n:3d} WR={wr:.0%} PnL=${pnl:+.1f} | BLOCK {block_n:3d}")
|
||||||
|
|
||||||
|
# Feature importance
|
||||||
|
print(f"\nTop features:")
|
||||||
|
importance = sorted(zip(FEATURE_NAMES, model.feature_importances_), key=lambda x: -x[1])
|
||||||
|
for name, score in importance:
|
||||||
|
print(f" {name:>15s} {score:.3f}")
|
||||||
|
|
||||||
|
# Train final model on ALL data
|
||||||
|
print(f"\nTraining final model on all {len(df)} samples...")
|
||||||
|
model_final = xgb.XGBClassifier(
|
||||||
|
n_estimators=200, max_depth=3, learning_rate=0.05,
|
||||||
|
subsample=0.8, colsample_bytree=0.7,
|
||||||
|
eval_metric="logloss", random_state=42,
|
||||||
|
)
|
||||||
|
model_final.fit(X, y, verbose=False)
|
||||||
|
model_final.save_model(MODEL_PATH)
|
||||||
|
print(f"Model saved to {MODEL_PATH}")
|
||||||
|
|
||||||
|
with open(FEATURES_PATH, "w") as f:
|
||||||
|
json.dump(FEATURE_NAMES, f)
|
||||||
|
print(f"Feature names saved to {FEATURES_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
["abs_pm_a", "pm_a_accel", "cb_a_abs", "bn_a_abs", "edge", "cb_same", "bn_same", "cb_stronger", "bid_ratio", "vol_ratio", "total_vol", "vol_growth", "rv_5m", "trades", "open_vol_ratio", "open_matches", "diff_abs", "pm_a_velocity", "pm_a_from_peak", "ask_from_peak", "direction_flips", "time_at_direction", "rv_ratio", "dvol", "funding", "oi_chg", "cb_vol_60s", "vol_spike", "flip_rate", "left_sec", "pm_a_z", "pm_a_vol_z"]
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
["abs_pm_a", "z", "vz", "left_sec", "cb_same", "bn_same", "cb_a_abs", "bn_a_abs", "edge_signed", "rv_5m", "ask", "ask_ratio"]
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
["ask", "abs_pm_a", "left", "rv_5m", "rv_15m", "sim_a_vol", "ask_margin", "pm_a_accel", "ask_vel", "vol_ratio", "total_vol", "sign_changes", "pm_a_std", "diff", "trades_count"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# ── Paper / data-collection + XGBoost inference (all you need to run the study) ──
|
||||||
|
aiohttp
|
||||||
|
orjson
|
||||||
|
numpy
|
||||||
|
xgboost
|
||||||
|
scikit-learn # the live engine loads its XGBoost gates via XGBClassifier
|
||||||
|
|
||||||
|
# ── Live trading only (--trade). NOT needed for paper mode. ──
|
||||||
|
# The live executor uses a specific Polymarket CLOB client fork (py_clob_client_v2)
|
||||||
|
# that is not on PyPI. Install Polymarket's py-clob-client and adapt, or wire your
|
||||||
|
# own. Left commented so paper mode installs clean.
|
||||||
|
# python-dotenv
|
||||||
|
# web3
|
||||||
|
# httpx
|
||||||
|
# py-clob-client
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# LIVE TRADING ONLY. Paper / data-collection mode needs NONE of this.
|
||||||
|
#
|
||||||
|
# To trade live: copy this file to src/predictor/.env and fill in your own
|
||||||
|
# values. This places REAL orders with REAL funds on Polymarket. The code is
|
||||||
|
# provided as-is and UNAUDITED — audit it yourself and trade at your own risk.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
PREDICTOR_WALLET_KEY=
|
||||||
|
PREDICTOR_WALLET_ADDRESS=
|
||||||
|
POLYGON_RPC_URL=
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
"""Polymarket CLOB feed for predictor — discovers current window tokens and tracks prices."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BUCKET_SEC = 300 # 5-minute windows
|
||||||
|
BUCKET_15M_SEC = 900 # 15-minute windows
|
||||||
|
GAMMA_URL = "https://gamma-api.polymarket.com/events"
|
||||||
|
SLUG_PREFIX = "btc-updown-5m-"
|
||||||
|
SLUG_PREFIX_15M = "btc-updown-15m-"
|
||||||
|
CLOB_WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WindowTokens:
|
||||||
|
"""Current window's token IDs and CLOB prices."""
|
||||||
|
window_id: int = 0
|
||||||
|
slug: str = ""
|
||||||
|
token_up: str = ""
|
||||||
|
token_down: str = ""
|
||||||
|
up_bid: float = 0.0
|
||||||
|
up_ask: float = 0.0
|
||||||
|
down_bid: float = 0.0
|
||||||
|
down_ask: float = 0.0
|
||||||
|
last_update_mono: float = 0.0
|
||||||
|
# Volume tracking (cumulative per window, in USDC = size * price, BUY only)
|
||||||
|
up_buy_usdc: float = 0.0 # USDC spent buying UP tokens
|
||||||
|
down_buy_usdc: float = 0.0 # USDC spent buying DOWN tokens
|
||||||
|
trade_count: int = 0 # total BUY trade events this window
|
||||||
|
# Book depth (from latest snapshot)
|
||||||
|
up_bid_depth: float = 0.0 # total bid size for UP
|
||||||
|
up_ask_depth: float = 0.0 # total ask size for UP
|
||||||
|
down_bid_depth: float = 0.0
|
||||||
|
down_ask_depth: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# Shared state — accessible by strategy and executor
|
||||||
|
current_window = WindowTokens() # 5m current window
|
||||||
|
current_window_15m = WindowTokens() # 15m current window (parallel)
|
||||||
|
|
||||||
|
|
||||||
|
async def _discover_window(session: aiohttp.ClientSession) -> WindowTokens | None:
|
||||||
|
"""Find the current active window via Gamma API."""
|
||||||
|
now = time.time()
|
||||||
|
base = int(now // BUCKET_SEC) * BUCKET_SEC
|
||||||
|
candidates = [base, base + BUCKET_SEC, base - BUCKET_SEC]
|
||||||
|
|
||||||
|
for ts in candidates:
|
||||||
|
slug = f"{SLUG_PREFIX}{ts}"
|
||||||
|
try:
|
||||||
|
async with session.get(
|
||||||
|
GAMMA_URL, params={"slug": slug},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=5),
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
continue
|
||||||
|
data = await resp.json()
|
||||||
|
if not isinstance(data, list) or not data:
|
||||||
|
continue
|
||||||
|
event = data[0]
|
||||||
|
markets = event.get("markets", [])
|
||||||
|
if not markets:
|
||||||
|
continue
|
||||||
|
market = markets[0]
|
||||||
|
raw_ids = market.get("clobTokenIds")
|
||||||
|
if not raw_ids:
|
||||||
|
continue
|
||||||
|
if isinstance(raw_ids, str):
|
||||||
|
token_ids = json.loads(raw_ids)
|
||||||
|
else:
|
||||||
|
token_ids = raw_ids
|
||||||
|
if not token_ids or len(token_ids) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"[clob] discovered: {slug}, up={token_ids[0][:16]}..., down={token_ids[1][:16]}...")
|
||||||
|
return WindowTokens(
|
||||||
|
window_id=ts,
|
||||||
|
slug=slug,
|
||||||
|
token_up=token_ids[0],
|
||||||
|
token_down=token_ids[1],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[clob] discovery failed for {slug}: {e}")
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _update_prices(asset_id: str, token_up: str, token_down: str,
|
||||||
|
best_bid: float, best_ask: float) -> None:
|
||||||
|
"""Update current_window prices for a given asset."""
|
||||||
|
global current_window
|
||||||
|
if asset_id == token_up:
|
||||||
|
if best_bid > 0:
|
||||||
|
current_window.up_bid = best_bid
|
||||||
|
if best_ask > 0:
|
||||||
|
current_window.up_ask = best_ask
|
||||||
|
current_window.last_update_mono = time.monotonic()
|
||||||
|
elif asset_id == token_down:
|
||||||
|
if best_bid > 0:
|
||||||
|
current_window.down_bid = best_bid
|
||||||
|
if best_ask > 0:
|
||||||
|
current_window.down_ask = best_ask
|
||||||
|
current_window.last_update_mono = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_clob_msg(data, token_up: str, token_down: str) -> None:
|
||||||
|
"""Parse CLOB WS message and update current_window prices.
|
||||||
|
|
||||||
|
Message formats:
|
||||||
|
1. Book snapshot (list): [{"asset_id":..., "bids":[{"price":"0.86","size":"10"}], "asks":[...]}]
|
||||||
|
2. Price change (dict): {"price_changes": [{"asset_id":..., "best_bid":"0.86", "best_ask":"0.87"}]}
|
||||||
|
3. Last trade (dict): {"asset_id":..., "price":"0.86", "event_type":"last_trade_price"}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle list of book snapshots
|
||||||
|
items = d if isinstance(d, list) else [d]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Type 1: Book snapshot — has "bids" and "asks" arrays with "asset_id"
|
||||||
|
asset_id = str(item.get("asset_id", ""))
|
||||||
|
if asset_id and "bids" in item and "asks" in item:
|
||||||
|
bids = item["bids"]
|
||||||
|
asks = item["asks"]
|
||||||
|
best_bid = 0.0
|
||||||
|
best_ask = 0.0
|
||||||
|
bid_depth = 0.0
|
||||||
|
ask_depth = 0.0
|
||||||
|
if bids:
|
||||||
|
try:
|
||||||
|
best_bid = max(float(b["price"]) for b in bids if isinstance(b, dict))
|
||||||
|
bid_depth = sum(float(b.get("size", 0)) for b in bids if isinstance(b, dict))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if asks:
|
||||||
|
try:
|
||||||
|
best_ask = min(float(a["price"]) for a in asks if isinstance(a, dict))
|
||||||
|
ask_depth = sum(float(a.get("size", 0)) for a in asks if isinstance(a, dict))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_update_prices(asset_id, token_up, token_down, best_bid, best_ask)
|
||||||
|
# Update book depth
|
||||||
|
if asset_id == token_up:
|
||||||
|
current_window.up_bid_depth = bid_depth
|
||||||
|
current_window.up_ask_depth = ask_depth
|
||||||
|
elif asset_id == token_down:
|
||||||
|
current_window.down_bid_depth = bid_depth
|
||||||
|
current_window.down_ask_depth = ask_depth
|
||||||
|
|
||||||
|
# Type 2: Price changes — has "price_changes" array (includes trade size/side)
|
||||||
|
for pc in item.get("price_changes", []):
|
||||||
|
if not isinstance(pc, dict):
|
||||||
|
continue
|
||||||
|
pc_asset = str(pc.get("asset_id", ""))
|
||||||
|
if pc_asset not in (token_up, token_down):
|
||||||
|
continue
|
||||||
|
bb = float(pc.get("best_bid", 0))
|
||||||
|
ba = float(pc.get("best_ask", 0))
|
||||||
|
_update_prices(pc_asset, token_up, token_down, bb, ba)
|
||||||
|
# Note: price_changes are orderbook update events (new/cancel/fill),
|
||||||
|
# NOT actual trades. Real trade volume is tracked via last_trade_price below.
|
||||||
|
|
||||||
|
# Type 3: Last trade price — actual executed trade
|
||||||
|
# Polymarket only broadcasts UP token trades (DOWN inferred from UP inverse):
|
||||||
|
# UP BUY @price → someone bought UP tokens (paid price)
|
||||||
|
# UP SELL @price → someone sold UP tokens = someone bought DOWN @(1-price)
|
||||||
|
if item.get("event_type") == "last_trade_price" and asset_id:
|
||||||
|
px = float(item.get("price", 0))
|
||||||
|
size = float(item.get("size", 0))
|
||||||
|
side = item.get("side", "")
|
||||||
|
if px > 0:
|
||||||
|
_update_prices(asset_id, token_up, token_down, px, 0.0)
|
||||||
|
if size > 0 and asset_id == token_up:
|
||||||
|
current_window.trade_count += 1
|
||||||
|
if side == "BUY":
|
||||||
|
# Someone bought UP at px
|
||||||
|
current_window.up_buy_usdc += size * px
|
||||||
|
elif side == "SELL":
|
||||||
|
# Someone sold UP at px = someone bought DOWN at (1-px)
|
||||||
|
current_window.down_buy_usdc += size * (1.0 - px)
|
||||||
|
|
||||||
|
|
||||||
|
async def clob_feed_task(shutdown: asyncio.Event):
|
||||||
|
"""Main CLOB feed loop — discover windows and subscribe to price updates."""
|
||||||
|
global current_window
|
||||||
|
logger.info("[clob] feed starting")
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# Discover current window
|
||||||
|
info = await _discover_window(session)
|
||||||
|
if info is None:
|
||||||
|
logger.warning("[clob] no active window found, retrying in 5s")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_window = info
|
||||||
|
|
||||||
|
# Connect to CLOB WS
|
||||||
|
async with session.ws_connect(
|
||||||
|
CLOB_WS_URL,
|
||||||
|
heartbeat=20.0,
|
||||||
|
receive_timeout=40.0,
|
||||||
|
) as ws:
|
||||||
|
# Subscribe to both tokens
|
||||||
|
for token_id in [info.token_up, info.token_down]:
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "Market",
|
||||||
|
"assets_ids": [token_id],
|
||||||
|
})
|
||||||
|
logger.info(f"[clob] subscribed: {info.slug}")
|
||||||
|
|
||||||
|
# Read messages until window expires
|
||||||
|
boundary = info.window_id + BUCKET_SEC
|
||||||
|
while not shutdown.is_set() and time.time() < boundary + 5:
|
||||||
|
msg = await ws.receive(timeout=10)
|
||||||
|
if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
|
_parse_clob_msg(msg.data, info.token_up, info.token_down)
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(f"[clob] window expired: {info.slug}")
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[clob] error: {e}, retrying in 2s")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
logger.info("[clob] feed stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# 15-minute window CLOB feed (parallel WebSocket subscription)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _discover_window_15m(session: aiohttp.ClientSession) -> WindowTokens | None:
|
||||||
|
"""Find the current active 15m window via Gamma API."""
|
||||||
|
now = time.time()
|
||||||
|
base = int(now // BUCKET_15M_SEC) * BUCKET_15M_SEC
|
||||||
|
candidates = [base, base + BUCKET_15M_SEC, base - BUCKET_15M_SEC]
|
||||||
|
|
||||||
|
for ts in candidates:
|
||||||
|
slug = f"{SLUG_PREFIX_15M}{ts}"
|
||||||
|
try:
|
||||||
|
async with session.get(
|
||||||
|
GAMMA_URL, params={"slug": slug},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=5),
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
continue
|
||||||
|
data = await resp.json()
|
||||||
|
if not isinstance(data, list) or not data:
|
||||||
|
continue
|
||||||
|
event = data[0]
|
||||||
|
markets = event.get("markets", [])
|
||||||
|
if not markets:
|
||||||
|
continue
|
||||||
|
market = markets[0]
|
||||||
|
raw_ids = market.get("clobTokenIds")
|
||||||
|
if not raw_ids:
|
||||||
|
continue
|
||||||
|
token_ids = json.loads(raw_ids) if isinstance(raw_ids, str) else raw_ids
|
||||||
|
if not token_ids or len(token_ids) < 2:
|
||||||
|
continue
|
||||||
|
logger.info(f"[clob15] discovered: {slug}, up={token_ids[0][:16]}..., down={token_ids[1][:16]}...")
|
||||||
|
return WindowTokens(
|
||||||
|
window_id=ts, slug=slug,
|
||||||
|
token_up=token_ids[0], token_down=token_ids[1],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[clob15] discovery failed for {slug}: {e}")
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _update_prices_15m(asset_id: str, token_up: str, token_down: str,
|
||||||
|
best_bid: float, best_ask: float) -> None:
|
||||||
|
"""Update current_window_15m prices for a given asset."""
|
||||||
|
global current_window_15m
|
||||||
|
if asset_id == token_up:
|
||||||
|
if best_bid > 0: current_window_15m.up_bid = best_bid
|
||||||
|
if best_ask > 0: current_window_15m.up_ask = best_ask
|
||||||
|
current_window_15m.last_update_mono = time.monotonic()
|
||||||
|
elif asset_id == token_down:
|
||||||
|
if best_bid > 0: current_window_15m.down_bid = best_bid
|
||||||
|
if best_ask > 0: current_window_15m.down_ask = best_ask
|
||||||
|
current_window_15m.last_update_mono = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_clob_msg_15m(data, token_up: str, token_down: str) -> None:
|
||||||
|
"""Parse CLOB WS message and update current_window_15m prices."""
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
items = d if isinstance(d, list) else [d]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict): continue
|
||||||
|
asset_id = str(item.get("asset_id", ""))
|
||||||
|
|
||||||
|
# Type 1: Book snapshot
|
||||||
|
if asset_id and "bids" in item and "asks" in item:
|
||||||
|
bids = item["bids"]; asks = item["asks"]
|
||||||
|
best_bid = 0.0; best_ask = 0.0
|
||||||
|
bid_depth = 0.0; ask_depth = 0.0
|
||||||
|
if bids:
|
||||||
|
try:
|
||||||
|
best_bid = max(float(b["price"]) for b in bids if isinstance(b, dict))
|
||||||
|
bid_depth = sum(float(b.get("size", 0)) for b in bids if isinstance(b, dict))
|
||||||
|
except Exception: pass
|
||||||
|
if asks:
|
||||||
|
try:
|
||||||
|
best_ask = min(float(a["price"]) for a in asks if isinstance(a, dict))
|
||||||
|
ask_depth = sum(float(a.get("size", 0)) for a in asks if isinstance(a, dict))
|
||||||
|
except Exception: pass
|
||||||
|
_update_prices_15m(asset_id, token_up, token_down, best_bid, best_ask)
|
||||||
|
if asset_id == token_up:
|
||||||
|
current_window_15m.up_bid_depth = bid_depth
|
||||||
|
current_window_15m.up_ask_depth = ask_depth
|
||||||
|
elif asset_id == token_down:
|
||||||
|
current_window_15m.down_bid_depth = bid_depth
|
||||||
|
current_window_15m.down_ask_depth = ask_depth
|
||||||
|
|
||||||
|
# Type 2: Price changes
|
||||||
|
for pc in item.get("price_changes", []):
|
||||||
|
if not isinstance(pc, dict): continue
|
||||||
|
pc_asset = str(pc.get("asset_id", ""))
|
||||||
|
if pc_asset not in (token_up, token_down): continue
|
||||||
|
bb = float(pc.get("best_bid", 0))
|
||||||
|
ba = float(pc.get("best_ask", 0))
|
||||||
|
_update_prices_15m(pc_asset, token_up, token_down, bb, ba)
|
||||||
|
|
||||||
|
|
||||||
|
async def clob_feed_15m_task(shutdown: asyncio.Event):
|
||||||
|
"""Parallel CLOB feed for 15m windows — same pattern as 5m feed."""
|
||||||
|
global current_window_15m
|
||||||
|
logger.info("[clob15] feed starting")
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
info = await _discover_window_15m(session)
|
||||||
|
if info is None:
|
||||||
|
logger.warning("[clob15] no active 15m window, retry 5s")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
current_window_15m = info
|
||||||
|
async with session.ws_connect(
|
||||||
|
CLOB_WS_URL, heartbeat=20.0, receive_timeout=40.0,
|
||||||
|
) as ws:
|
||||||
|
for token_id in [info.token_up, info.token_down]:
|
||||||
|
await ws.send_json({"type": "Market", "assets_ids": [token_id]})
|
||||||
|
logger.info(f"[clob15] subscribed: {info.slug}")
|
||||||
|
boundary = info.window_id + BUCKET_15M_SEC
|
||||||
|
while not shutdown.is_set() and time.time() < boundary + 5:
|
||||||
|
msg = await ws.receive(timeout=10)
|
||||||
|
if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
|
_parse_clob_msg_15m(msg.data, info.token_up, info.token_down)
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
logger.info(f"[clob15] window expired: {info.slug}")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[clob15] error: {e}, retry 2s")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
logger.info("[clob15] feed stopped")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,341 @@
|
|||||||
|
"""Predictor Executor — places orders on Polymarket using the predictor's own wallet."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PredictorExecutor:
|
||||||
|
"""Simple executor for the predictor strategy.
|
||||||
|
|
||||||
|
Uses its own wallet (from src/predictor/.env).
|
||||||
|
Stateless per-window: place one order, track if filled, done.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Load predictor's own .env
|
||||||
|
env_path = os.path.join(os.path.dirname(__file__), ".env")
|
||||||
|
load_dotenv(env_path, override=True)
|
||||||
|
|
||||||
|
key = os.environ.get("PREDICTOR_WALLET_KEY", "")
|
||||||
|
address = os.environ.get("PREDICTOR_WALLET_ADDRESS", "")
|
||||||
|
rpc_url = os.environ.get("POLYGON_RPC_URL", "")
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
raise ValueError("PREDICTOR_WALLET_KEY not set in src/predictor/.env")
|
||||||
|
|
||||||
|
# Force HTTP/1.1
|
||||||
|
import httpx as _httpx
|
||||||
|
import py_clob_client_v2.http_helpers.helpers as _clob_helpers
|
||||||
|
_clob_helpers._http_client = _httpx.Client(http2=False, timeout=30)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
# Preflight caches (V2: fee_rate is computed by protocol at match time)
|
||||||
|
self._tick_size_cache: dict[str, str] = {}
|
||||||
|
self._neg_risk_cache: dict[str, bool] = {}
|
||||||
|
|
||||||
|
# Track current window's order
|
||||||
|
self._current_window_id: int = 0
|
||||||
|
self._order_placed_this_window: bool = False
|
||||||
|
self._last_order_id: str = ""
|
||||||
|
|
||||||
|
# Daily P&L tracking
|
||||||
|
self._daily_pnl: float = 0.0
|
||||||
|
self._daily_date: str = ""
|
||||||
|
|
||||||
|
logger.info(f"[executor] initialized: wallet={address}")
|
||||||
|
|
||||||
|
async def _resolve_preflight(self, token_id: str) -> tuple[str, bool]:
|
||||||
|
"""Cache tick_size, neg_risk per token."""
|
||||||
|
if token_id not in self._tick_size_cache:
|
||||||
|
self._tick_size_cache[token_id] = await asyncio.to_thread(
|
||||||
|
self._client.get_tick_size, token_id
|
||||||
|
)
|
||||||
|
if token_id not in self._neg_risk_cache:
|
||||||
|
self._neg_risk_cache[token_id] = await asyncio.to_thread(
|
||||||
|
self._client.get_neg_risk, token_id
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
self._tick_size_cache[token_id],
|
||||||
|
self._neg_risk_cache[token_id],
|
||||||
|
)
|
||||||
|
|
||||||
|
def new_window(self, window_id: int):
|
||||||
|
"""Called when a new window starts."""
|
||||||
|
if window_id != self._current_window_id:
|
||||||
|
self._current_window_id = window_id
|
||||||
|
self._order_placed_this_window = False
|
||||||
|
self._last_order_id = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_trade(self) -> bool:
|
||||||
|
"""Allow multiple trades per window (lock + cb_flip hedge)."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def place_buy(
|
||||||
|
self,
|
||||||
|
token_id: str,
|
||||||
|
price: float,
|
||||||
|
amount_usd: float,
|
||||||
|
expiration_sec: int = 30,
|
||||||
|
) -> str | None:
|
||||||
|
"""Place a BUY order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_id: UP or DOWN token to buy
|
||||||
|
price: Limit price (e.g. 0.72)
|
||||||
|
amount_usd: Dollar amount to spend
|
||||||
|
expiration_sec: GTD expiration in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
order_id if placed, None if failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Resolve preflight
|
||||||
|
tick_size, neg_risk = await self._resolve_preflight(token_id)
|
||||||
|
|
||||||
|
# Calculate quantity
|
||||||
|
qty = math.floor(amount_usd / price)
|
||||||
|
if qty < 1:
|
||||||
|
logger.warning(f"[executor] qty too small: {qty} (amount=${amount_usd}, price={price})")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build order
|
||||||
|
order_args = OrderArgs(
|
||||||
|
price=price,
|
||||||
|
size=float(qty),
|
||||||
|
side=BUY,
|
||||||
|
token_id=token_id,
|
||||||
|
expiration=str(int(time.time()) + expiration_sec),
|
||||||
|
)
|
||||||
|
|
||||||
|
options = PartialCreateOrderOptions(
|
||||||
|
tick_size=tick_size,
|
||||||
|
neg_risk=neg_risk,
|
||||||
|
)
|
||||||
|
|
||||||
|
signed_order = await asyncio.to_thread(
|
||||||
|
self._client.create_order, order_args, options
|
||||||
|
)
|
||||||
|
|
||||||
|
t0 = time.monotonic()
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
self._client.post_order, signed_order, OrderType.GTD
|
||||||
|
)
|
||||||
|
latency = (time.monotonic() - t0) * 1000
|
||||||
|
|
||||||
|
order_id = result.get("orderID", "")
|
||||||
|
self._order_placed_this_window = True
|
||||||
|
self._last_order_id = order_id
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[executor] ORDER PLACED: {order_id[:16]}... "
|
||||||
|
f"token={token_id[:16]}... price={price} qty={qty} "
|
||||||
|
f"cost=${qty * price:.2f} latency={latency:.0f}ms"
|
||||||
|
)
|
||||||
|
return order_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[executor] order failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_order_filled(self, order_id: str) -> dict:
|
||||||
|
"""Check order fill status. Returns actual avg fill price from trades.
|
||||||
|
Returns {'filled': shares_matched, 'limit_price': X, 'avg_fill_price': Y} or None on error.
|
||||||
|
avg_fill_price is the weighted average fill price (size-weighted across partial fills).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
order = await asyncio.to_thread(self._client.get_order, order_id)
|
||||||
|
logger.info(f"[executor] order {order_id[:16]}... raw response: {order}")
|
||||||
|
matched = float(order.get("size_matched", 0))
|
||||||
|
limit_price = float(order.get("price", 0))
|
||||||
|
avg_fill_price = limit_price # fallback
|
||||||
|
|
||||||
|
if matched > 0:
|
||||||
|
# Query actual trades to get true fill prices
|
||||||
|
try:
|
||||||
|
trades = await asyncio.to_thread(self._client.get_trades, {"id": order_id})
|
||||||
|
if trades:
|
||||||
|
total_cost = 0.0
|
||||||
|
total_size = 0.0
|
||||||
|
for t in trades:
|
||||||
|
# Each trade has size and price
|
||||||
|
tsize = float(t.get("size", 0))
|
||||||
|
tprice = float(t.get("price", 0))
|
||||||
|
if tsize > 0 and tprice > 0:
|
||||||
|
total_cost += tsize * tprice
|
||||||
|
total_size += tsize
|
||||||
|
if total_size > 0:
|
||||||
|
avg_fill_price = total_cost / total_size
|
||||||
|
logger.info(f"[executor] {order_id[:16]}... avg_fill={avg_fill_price:.4f} (limit={limit_price:.4f}, {len(trades)} fills)")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[executor] get_trades failed: {e}, using limit price as fallback")
|
||||||
|
|
||||||
|
return {"filled": matched, "limit_price": limit_price, "avg_fill_price": avg_fill_price}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[executor] check_order failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cancel_all(self) -> None:
|
||||||
|
"""Cancel all open orders."""
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(self._client.cancel_all)
|
||||||
|
logger.info("[executor] cancelled all orders")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[executor] cancel_all failed: {e}")
|
||||||
|
|
||||||
|
async def get_balance(self) -> float:
|
||||||
|
"""Get USDC balance."""
|
||||||
|
try:
|
||||||
|
from web3 import Web3
|
||||||
|
rpc = os.environ.get("POLYGON_RPC_URL", "")
|
||||||
|
address = os.environ.get("PREDICTOR_WALLET_ADDRESS", "")
|
||||||
|
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(address)).call()
|
||||||
|
return bal / 1e6
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[executor] balance check failed: {e}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
async def auto_redeem(self, slug: str) -> float:
|
||||||
|
"""Auto-redeem a settled window. Returns USDC gained."""
|
||||||
|
try:
|
||||||
|
from web3 import Web3
|
||||||
|
from web3.middleware import ExtraDataToPOAMiddleware
|
||||||
|
from eth_account import Account
|
||||||
|
import requests
|
||||||
|
|
||||||
|
rpc = os.environ.get("POLYGON_RPC_URL", "")
|
||||||
|
key = os.environ.get("PREDICTOR_WALLET_KEY", "")
|
||||||
|
address = os.environ.get("PREDICTOR_WALLET_ADDRESS", "")
|
||||||
|
|
||||||
|
w3 = Web3(Web3.HTTPProvider(rpc))
|
||||||
|
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
||||||
|
acct = Account.from_key(key)
|
||||||
|
eoa = Web3.to_checksum_address(address)
|
||||||
|
|
||||||
|
CTF_ADDR = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
||||||
|
USDC_ADDR = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||||
|
ZERO_PARENT = b"\x00" * 32
|
||||||
|
|
||||||
|
CTF_ABI = [
|
||||||
|
{"name": "redeemPositions", "type": "function", "stateMutability": "nonpayable",
|
||||||
|
"inputs": [
|
||||||
|
{"name": "collateralToken", "type": "address"},
|
||||||
|
{"name": "parentCollectionId", "type": "bytes32"},
|
||||||
|
{"name": "conditionId", "type": "bytes32"},
|
||||||
|
{"name": "indexSets", "type": "uint256[]"},
|
||||||
|
], "outputs": []},
|
||||||
|
{"name": "balanceOf", "type": "function", "stateMutability": "view",
|
||||||
|
"inputs": [
|
||||||
|
{"name": "account", "type": "address"},
|
||||||
|
{"name": "id", "type": "uint256"},
|
||||||
|
], "outputs": [{"type": "uint256"}]},
|
||||||
|
{"name": "payoutDenominator", "type": "function", "stateMutability": "view",
|
||||||
|
"inputs": [{"name": "conditionId", "type": "bytes32"}],
|
||||||
|
"outputs": [{"type": "uint256"}]},
|
||||||
|
]
|
||||||
|
ERC20_ABI = [
|
||||||
|
{"name": "balanceOf", "type": "function", "stateMutability": "view",
|
||||||
|
"inputs": [{"name": "account", "type": "address"}],
|
||||||
|
"outputs": [{"type": "uint256"}]},
|
||||||
|
]
|
||||||
|
|
||||||
|
ctf = w3.eth.contract(address=Web3.to_checksum_address(CTF_ADDR), abi=CTF_ABI)
|
||||||
|
usdc = w3.eth.contract(address=Web3.to_checksum_address(USDC_ADDR), abi=ERC20_ABI)
|
||||||
|
|
||||||
|
# Lookup market via events endpoint (markets endpoint doesn't find by slug)
|
||||||
|
r = await asyncio.to_thread(
|
||||||
|
requests.get, "https://gamma-api.polymarket.com/events",
|
||||||
|
params={"slug": slug}, timeout=10,
|
||||||
|
)
|
||||||
|
data = r.json()
|
||||||
|
if not data:
|
||||||
|
logger.warning(f"[redeem] event not found: {slug}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
event = data[0]
|
||||||
|
markets = event.get("markets", [])
|
||||||
|
if not markets:
|
||||||
|
logger.warning(f"[redeem] no markets in event: {slug}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
market = markets[0]
|
||||||
|
cid = market.get("conditionId") or market.get("conditionID")
|
||||||
|
if not cid:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
condition_b32 = bytes.fromhex(cid.removeprefix("0x"))
|
||||||
|
collateral = Web3.to_checksum_address(USDC_ADDR)
|
||||||
|
|
||||||
|
# Check if resolved
|
||||||
|
den = ctf.functions.payoutDenominator(condition_b32).call()
|
||||||
|
if den == 0:
|
||||||
|
logger.info(f"[redeem] {slug}: not resolved yet")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Check USDC before
|
||||||
|
usdc_before = usdc.functions.balanceOf(eoa).call()
|
||||||
|
|
||||||
|
# Build redeem tx
|
||||||
|
nonce = w3.eth.get_transaction_count(eoa, "pending")
|
||||||
|
latest = w3.eth.get_block("latest")
|
||||||
|
base_fee = latest.get("baseFeePerGas", 30_000_000_000)
|
||||||
|
max_priority = 50_000_000_000
|
||||||
|
max_fee = base_fee * 2 + max_priority
|
||||||
|
|
||||||
|
tx = ctf.functions.redeemPositions(
|
||||||
|
collateral, ZERO_PARENT, condition_b32, [1, 2]
|
||||||
|
).build_transaction({
|
||||||
|
"from": eoa,
|
||||||
|
"nonce": nonce,
|
||||||
|
"maxFeePerGas": max_fee,
|
||||||
|
"maxPriorityFeePerGas": max_priority,
|
||||||
|
"gas": 300_000,
|
||||||
|
"chainId": 137,
|
||||||
|
"type": 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
signed = acct.sign_transaction(tx)
|
||||||
|
txh = w3.eth.send_raw_transaction(signed.raw_transaction)
|
||||||
|
receipt = await asyncio.to_thread(
|
||||||
|
w3.eth.wait_for_transaction_receipt, txh, timeout=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
if receipt["status"] != 1:
|
||||||
|
logger.warning(f"[redeem] {slug}: tx reverted")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
usdc_after = usdc.functions.balanceOf(eoa).call()
|
||||||
|
delta = (usdc_after - usdc_before) / 1e6
|
||||||
|
logger.info(f"[redeem] {slug}: +${delta:.2f} (balance: ${usdc_after/1e6:.2f})")
|
||||||
|
return delta
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[redeem] failed: {e}")
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
"""Multi-source WebSocket price feeds for chainlink predictor."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from src.predictor.models import SourceName, SourceTick
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generic helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _ws_loop(
|
||||||
|
name: SourceName,
|
||||||
|
url: str,
|
||||||
|
on_connect: Callable | None,
|
||||||
|
parse_msg: Callable,
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
ping_interval: float = 20.0,
|
||||||
|
reconnect_base: float = 1.0,
|
||||||
|
reconnect_max: float = 30.0,
|
||||||
|
):
|
||||||
|
"""Generic reconnecting WebSocket loop."""
|
||||||
|
delay = reconnect_base
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.ws_connect(
|
||||||
|
url,
|
||||||
|
heartbeat=ping_interval,
|
||||||
|
receive_timeout=ping_interval * 2,
|
||||||
|
) as ws:
|
||||||
|
logger.info(f"[{name.value}] connected: {url}")
|
||||||
|
delay = reconnect_base
|
||||||
|
|
||||||
|
if on_connect:
|
||||||
|
await on_connect(ws)
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
msg = await ws.receive(timeout=ping_interval)
|
||||||
|
if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
|
ticks = parse_msg(msg.data)
|
||||||
|
for t in ticks:
|
||||||
|
try:
|
||||||
|
tick_queue.put_nowait(t)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[{name.value}] error: {e}, reconnecting in {delay:.0f}s")
|
||||||
|
|
||||||
|
if shutdown.is_set():
|
||||||
|
break
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
delay = min(delay * 2, reconnect_max)
|
||||||
|
|
||||||
|
logger.info(f"[{name.value}] stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Coinbase
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def coinbase_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "subscribe",
|
||||||
|
"channels": [{"name": "ticker", "product_ids": ["BTC-USD"]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if d.get("type") != "ticker":
|
||||||
|
return []
|
||||||
|
px = float(d["price"])
|
||||||
|
vol = float(d.get("last_size", 0))
|
||||||
|
return [SourceTick(
|
||||||
|
source=SourceName.COINBASE, price=px,
|
||||||
|
source_ts_ns=recv_ns, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.COINBASE,
|
||||||
|
"wss://ws-feed.exchange.coinbase.com",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kraken
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def kraken_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"event": "subscribe",
|
||||||
|
"pair": ["XBT/USD"],
|
||||||
|
"subscription": {"name": "trade"},
|
||||||
|
})
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
# Kraken trade format: [channelID, [[price, volume, time, side, type, misc], ...], "trade", "XBT/USD"]
|
||||||
|
if not isinstance(d, list) or len(d) < 4:
|
||||||
|
return []
|
||||||
|
if d[-1] != "XBT/USD" or d[-2] != "trade":
|
||||||
|
return []
|
||||||
|
trades = d[1]
|
||||||
|
if not trades:
|
||||||
|
return []
|
||||||
|
# Take last trade
|
||||||
|
last = trades[-1]
|
||||||
|
px = float(last[0])
|
||||||
|
vol = float(last[1])
|
||||||
|
src_ts = int(float(last[2]) * 1e9)
|
||||||
|
return [SourceTick(
|
||||||
|
source=SourceName.KRAKEN, price=px,
|
||||||
|
source_ts_ns=src_ts, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.KRAKEN,
|
||||||
|
"wss://ws.kraken.com",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bitstamp
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def bitstamp_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"event": "bts:subscribe",
|
||||||
|
"data": {"channel": "live_trades_btcusd"},
|
||||||
|
})
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if d.get("event") != "trade":
|
||||||
|
return []
|
||||||
|
td = d.get("data", {})
|
||||||
|
px = float(td["price"])
|
||||||
|
vol = float(td.get("amount", 0))
|
||||||
|
src_ts = int(td.get("timestamp", 0)) * 1_000_000_000 or recv_ns
|
||||||
|
return [SourceTick(
|
||||||
|
source=SourceName.BITSTAMP, price=px,
|
||||||
|
source_ts_ns=src_ts, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.BITSTAMP,
|
||||||
|
"wss://ws.bitstamp.net",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CryptoCompare
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def cryptocompare_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
api_key: str = "",
|
||||||
|
poll_interval: float = 1.0,
|
||||||
|
):
|
||||||
|
"""Poll CryptoCompare REST API (WS requires paid key)."""
|
||||||
|
url = "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD"
|
||||||
|
if api_key:
|
||||||
|
url += f"&api_key={api_key}"
|
||||||
|
|
||||||
|
logger.info(f"[cryptocompare] polling started: {url}")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=3)) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
d = await r.json()
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
px = float(d["USD"])
|
||||||
|
tick = SourceTick(
|
||||||
|
source=SourceName.CRYPTOCOMPARE, price=px,
|
||||||
|
source_ts_ns=recv_ns, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tick_queue.put_nowait(tick)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[cryptocompare] poll error: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(shutdown.wait(), timeout=poll_interval)
|
||||||
|
break
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("[cryptocompare] stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gemini (REST polling — no free WS trade feed without auth)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def gemini_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
poll_interval: float = 1.0,
|
||||||
|
):
|
||||||
|
"""Poll Gemini REST API every second."""
|
||||||
|
url = "https://api.gemini.com/v1/pubticker/btcusd"
|
||||||
|
logger.info(f"[gemini] polling started: {url}")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=3)) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
d = await r.json()
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
px = float(d["last"])
|
||||||
|
tick = SourceTick(
|
||||||
|
source=SourceName.GEMINI, price=px,
|
||||||
|
source_ts_ns=recv_ns, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tick_queue.put_nowait(tick)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[gemini] poll error: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(shutdown.wait(), timeout=poll_interval)
|
||||||
|
break
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("[gemini] stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OKX
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def okx_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": [{"channel": "trades", "instId": "BTC-USDT"}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if "data" not in d:
|
||||||
|
return []
|
||||||
|
ticks = []
|
||||||
|
for trade in d["data"]:
|
||||||
|
px = float(trade["px"])
|
||||||
|
vol = float(trade.get("sz", 0))
|
||||||
|
src_ts = int(trade.get("ts", 0)) * 1_000_000 or recv_ns
|
||||||
|
ticks.append(SourceTick(
|
||||||
|
source=SourceName.OKX, price=px,
|
||||||
|
source_ts_ns=src_ts, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
))
|
||||||
|
return ticks[-1:] if ticks else [] # last trade only
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.OKX,
|
||||||
|
"wss://ws.okx.com:8443/ws/v5/public",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bybit
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def bybit_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": ["publicTrade.BTCUSDT"],
|
||||||
|
})
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if d.get("topic") != "publicTrade.BTCUSDT":
|
||||||
|
return []
|
||||||
|
trades = d.get("data", [])
|
||||||
|
if not trades:
|
||||||
|
return []
|
||||||
|
last = trades[-1]
|
||||||
|
px = float(last["p"])
|
||||||
|
vol = float(last.get("v", 0))
|
||||||
|
src_ts = int(last.get("T", 0)) * 1_000_000 or recv_ns
|
||||||
|
return [SourceTick(
|
||||||
|
source=SourceName.BYBIT, price=px,
|
||||||
|
source_ts_ns=src_ts, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.BYBIT,
|
||||||
|
"wss://stream.bybit.com/v5/public/spot",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Binance
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def binance_feed(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
):
|
||||||
|
async def on_connect(ws):
|
||||||
|
pass # No subscription needed, URL contains the stream
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if "p" not in d:
|
||||||
|
return []
|
||||||
|
px = float(d["p"])
|
||||||
|
vol = float(d.get("q", 0))
|
||||||
|
src_ts = int(d.get("T", 0)) * 1_000_000 or recv_ns
|
||||||
|
return [SourceTick(
|
||||||
|
source=SourceName.BINANCE, price=px,
|
||||||
|
source_ts_ns=src_ts, recv_mono_ns=mono_ns, recv_ts_ns=recv_ns,
|
||||||
|
volume=vol,
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await _ws_loop(
|
||||||
|
SourceName.BINANCE,
|
||||||
|
"wss://stream.binance.com:9443/ws/btcusdt@trade",
|
||||||
|
on_connect, parse, tick_queue, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Feed registry
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ALL_FEEDS = {
|
||||||
|
SourceName.COINBASE: coinbase_feed,
|
||||||
|
SourceName.KRAKEN: kraken_feed,
|
||||||
|
SourceName.BITSTAMP: bitstamp_feed,
|
||||||
|
SourceName.CRYPTOCOMPARE: cryptocompare_feed,
|
||||||
|
SourceName.GEMINI: gemini_feed,
|
||||||
|
SourceName.OKX: okx_feed,
|
||||||
|
SourceName.BYBIT: bybit_feed,
|
||||||
|
SourceName.BINANCE: binance_feed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Data models for chainlink predictor."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class SourceName(str, Enum):
|
||||||
|
COINBASE = "coinbase"
|
||||||
|
KRAKEN = "kraken"
|
||||||
|
BITSTAMP = "bitstamp"
|
||||||
|
CRYPTOCOMPARE = "cryptocompare"
|
||||||
|
GEMINI = "gemini"
|
||||||
|
OKX = "okx"
|
||||||
|
BYBIT = "bybit"
|
||||||
|
BINANCE = "binance"
|
||||||
|
PM = "pm" # Polymarket / Chainlink reference
|
||||||
|
|
||||||
|
|
||||||
|
# Sources that trade in USDT (not USD) — have a persistent premium vs Chainlink
|
||||||
|
USDT_SOURCES = {SourceName.BINANCE, SourceName.OKX, SourceName.BYBIT}
|
||||||
|
USD_SOURCES = {SourceName.COINBASE, SourceName.KRAKEN, SourceName.BITSTAMP, SourceName.CRYPTOCOMPARE, SourceName.GEMINI}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class SourceTick:
|
||||||
|
"""A single price tick from a data source."""
|
||||||
|
source: SourceName
|
||||||
|
price: float
|
||||||
|
source_ts_ns: int # source-reported timestamp (ns)
|
||||||
|
recv_mono_ns: int # local monotonic (ns) — for lead-lag
|
||||||
|
recv_ts_ns: int # local wall clock (ns) — for logging
|
||||||
|
volume: float = 0.0 # trade volume (in BTC) — 0 if not available
|
||||||
|
# Extra fields from PM audit (only set for PM ticks)
|
||||||
|
pm_ptb: float = 0.0 # PM price at 5min bucket start (= 5m market threshold)
|
||||||
|
pm_ptb_15m: float = 0.0 # PM price at 15min bucket start (= 15m market threshold)
|
||||||
|
pm_a: float = 0.0 # PM movement from bucket start
|
||||||
|
left_sec: float = 0.0 # seconds left in window
|
||||||
|
bucket_start_sec: int = 0 # window ID from trading system
|
||||||
|
estimated_onchain_ptb: float = 0.0 # estimated on-chain CL price at window start
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OrderBookLevel:
|
||||||
|
"""A single price level in the order book."""
|
||||||
|
price: float
|
||||||
|
size: float # BTC
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OrderBookState:
|
||||||
|
"""Order book state for a source."""
|
||||||
|
source: SourceName
|
||||||
|
bids: list[OrderBookLevel] = field(default_factory=list) # sorted high→low
|
||||||
|
asks: list[OrderBookLevel] = field(default_factory=list) # sorted low→high
|
||||||
|
last_update_mono_ns: int = 0
|
||||||
|
|
||||||
|
def volume_between(self, price_a: float, price_b: float) -> tuple[float, float]:
|
||||||
|
"""
|
||||||
|
Calculate bid/ask volume between two prices.
|
||||||
|
Returns (bid_volume, ask_volume) in BTC.
|
||||||
|
"""
|
||||||
|
lo = min(price_a, price_b)
|
||||||
|
hi = max(price_a, price_b)
|
||||||
|
bid_vol = sum(l.size for l in self.bids if lo <= l.price <= hi)
|
||||||
|
ask_vol = sum(l.size for l in self.asks if lo <= l.price <= hi)
|
||||||
|
return bid_vol, ask_vol
|
||||||
|
|
||||||
|
|
||||||
|
# Sources that have order book feeds
|
||||||
|
ORDERBOOK_SOURCES = {SourceName.COINBASE, SourceName.KRAKEN, SourceName.BINANCE, SourceName.OKX}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class SourceState:
|
||||||
|
"""Current state of a data source."""
|
||||||
|
source: SourceName
|
||||||
|
price: float = 0.0
|
||||||
|
last_recv_mono_ns: int = 0
|
||||||
|
tick_count: int = 0
|
||||||
|
window_volume: float = 0.0 # cumulative volume this window (BTC)
|
||||||
|
disconnect_count: int = 0
|
||||||
|
last_disconnect_mono_ns: int = 0
|
||||||
|
stale_tick_count: int = 0 # ticks with interval > 2s
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class WindowSummary:
|
||||||
|
"""Per-window summary for dashboard."""
|
||||||
|
window_id: str
|
||||||
|
window_start: str
|
||||||
|
pm_settle_price: float = 0.0
|
||||||
|
pm_ptb: float = 0.0
|
||||||
|
pm_direction: str = ""
|
||||||
|
pm_delta: float = 0.0
|
||||||
|
sim_price_at_settle: float = 0.0
|
||||||
|
sim_error: float = 0.0
|
||||||
|
sim_direction_correct: bool = False
|
||||||
|
leading_source: str = ""
|
||||||
|
lead_direction_correct: bool = False
|
||||||
|
lead_avg_seconds: float = 0.0
|
||||||
|
source_prices_at_settle: dict = field(default_factory=dict)
|
||||||
|
source_lead_pct: dict = field(default_factory=dict)
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
"""Order book WebSocket feeds for Coinbase, Kraken, Binance, and OKX."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from src.predictor.models import SourceName, OrderBookState, OrderBookLevel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared state: order books accessible by collector
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
order_books: dict[SourceName, OrderBookState] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_book(source: SourceName) -> OrderBookState | None:
|
||||||
|
return order_books.get(source)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generic reconnecting WS loop
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _ob_ws_loop(
|
||||||
|
name: SourceName,
|
||||||
|
url: str,
|
||||||
|
on_connect,
|
||||||
|
on_message,
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
ping_interval: float = 20.0,
|
||||||
|
):
|
||||||
|
delay = 1.0
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.ws_connect(
|
||||||
|
url, heartbeat=ping_interval, receive_timeout=ping_interval * 2,
|
||||||
|
) as ws:
|
||||||
|
logger.info(f"[{name.value}_ob] connected: {url}")
|
||||||
|
delay = 1.0
|
||||||
|
if on_connect:
|
||||||
|
await on_connect(ws)
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
msg = await ws.receive(timeout=ping_interval)
|
||||||
|
if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
|
on_message(msg.data)
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[{name.value}_ob] error: {e}, reconnecting in {delay:.0f}s")
|
||||||
|
|
||||||
|
if shutdown.is_set():
|
||||||
|
break
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
delay = min(delay * 2, 30.0)
|
||||||
|
|
||||||
|
logger.info(f"[{name.value}_ob] stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Coinbase — level2 channel (top 50 levels)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def coinbase_orderbook_feed(shutdown: asyncio.Event):
|
||||||
|
book = OrderBookState(source=SourceName.COINBASE)
|
||||||
|
order_books[SourceName.COINBASE] = book
|
||||||
|
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"type": "subscribe",
|
||||||
|
"channels": [{"name": "level2_batch", "product_ids": ["BTC-USD"]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_message(data):
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
msg_type = d.get("type")
|
||||||
|
|
||||||
|
if msg_type == "snapshot":
|
||||||
|
book.bids = [OrderBookLevel(float(p), float(s)) for p, s in d.get("bids", [])[:50]]
|
||||||
|
book.asks = [OrderBookLevel(float(p), float(s)) for p, s in d.get("asks", [])[:50]]
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
|
||||||
|
elif msg_type == "l2update":
|
||||||
|
for side, price_str, size_str in d.get("changes", []):
|
||||||
|
price = float(price_str)
|
||||||
|
size = float(size_str)
|
||||||
|
if side == "buy":
|
||||||
|
# Update or remove bid
|
||||||
|
book.bids = [l for l in book.bids if l.price != price]
|
||||||
|
if size > 0:
|
||||||
|
book.bids.append(OrderBookLevel(price, size))
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.bids = book.bids[:50]
|
||||||
|
else:
|
||||||
|
book.asks = [l for l in book.asks if l.price != price]
|
||||||
|
if size > 0:
|
||||||
|
book.asks.append(OrderBookLevel(price, size))
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.asks = book.asks[:50]
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await _ob_ws_loop(
|
||||||
|
SourceName.COINBASE,
|
||||||
|
"wss://ws-feed.exchange.coinbase.com",
|
||||||
|
on_connect, on_message, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kraken — book channel (top 25 levels)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def kraken_orderbook_feed(shutdown: asyncio.Event):
|
||||||
|
book = OrderBookState(source=SourceName.KRAKEN)
|
||||||
|
order_books[SourceName.KRAKEN] = book
|
||||||
|
|
||||||
|
def _parse_levels(raw: list) -> list[OrderBookLevel]:
|
||||||
|
levels = []
|
||||||
|
for item in raw:
|
||||||
|
if len(item) >= 2:
|
||||||
|
levels.append(OrderBookLevel(float(item[0]), float(item[1])))
|
||||||
|
return levels
|
||||||
|
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"event": "subscribe",
|
||||||
|
"pair": ["XBT/USD"],
|
||||||
|
"subscription": {"name": "book", "depth": 25},
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_message(data):
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
|
||||||
|
# Kraken book format: [channelID, {"as":[], "bs":[]}, "book-25", "XBT/USD"]
|
||||||
|
# or update: [channelID, {"a":[]}, "book-25", "XBT/USD"]
|
||||||
|
if not isinstance(d, list) or len(d) < 4:
|
||||||
|
return
|
||||||
|
if "XBT/USD" not in str(d[-1]):
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = d[1]
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Snapshot
|
||||||
|
if "as" in payload and "bs" in payload:
|
||||||
|
book.asks = _parse_levels(payload["as"])
|
||||||
|
book.bids = _parse_levels(payload["bs"])
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Updates — can have "a" and/or "b" in payload or d[2] for second part
|
||||||
|
for part in ([d[1]] if isinstance(d[1], dict) else []) + ([d[2]] if len(d) > 4 and isinstance(d[2], dict) else []):
|
||||||
|
if "a" in part:
|
||||||
|
for item in part["a"]:
|
||||||
|
price = float(item[0])
|
||||||
|
size = float(item[1])
|
||||||
|
book.asks = [l for l in book.asks if l.price != price]
|
||||||
|
if size > 0:
|
||||||
|
book.asks.append(OrderBookLevel(price, size))
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.asks = book.asks[:25]
|
||||||
|
|
||||||
|
if "b" in part:
|
||||||
|
for item in part["b"]:
|
||||||
|
price = float(item[0])
|
||||||
|
size = float(item[1])
|
||||||
|
book.bids = [l for l in book.bids if l.price != price]
|
||||||
|
if size > 0:
|
||||||
|
book.bids.append(OrderBookLevel(price, size))
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.bids = book.bids[:25]
|
||||||
|
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await _ob_ws_loop(
|
||||||
|
SourceName.KRAKEN,
|
||||||
|
"wss://ws.kraken.com",
|
||||||
|
on_connect, on_message, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Binance — depth stream (top 20 levels, 100ms updates)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def binance_orderbook_feed(shutdown: asyncio.Event):
|
||||||
|
book = OrderBookState(source=SourceName.BINANCE)
|
||||||
|
order_books[SourceName.BINANCE] = book
|
||||||
|
|
||||||
|
async def on_connect(ws):
|
||||||
|
pass # stream is in the URL
|
||||||
|
|
||||||
|
def on_message(data):
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
if "bids" not in d and "asks" not in d:
|
||||||
|
return
|
||||||
|
|
||||||
|
book.bids = [OrderBookLevel(float(p), float(s)) for p, s in d.get("bids", [])[:20]]
|
||||||
|
book.asks = [OrderBookLevel(float(p), float(s)) for p, s in d.get("asks", [])[:20]]
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await _ob_ws_loop(
|
||||||
|
SourceName.BINANCE,
|
||||||
|
"wss://stream.binance.com:9443/ws/btcusdt@depth20@100ms",
|
||||||
|
on_connect, on_message, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OKX — books5 channel (top 5 levels, real-time, public, no auth)
|
||||||
|
# Spot BTC-USDT (matches okx_feed price feed). USDT-pegged source.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def okx_orderbook_feed(shutdown: asyncio.Event):
|
||||||
|
book = OrderBookState(source=SourceName.OKX)
|
||||||
|
order_books[SourceName.OKX] = book
|
||||||
|
|
||||||
|
async def on_connect(ws):
|
||||||
|
await ws.send_json({
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": [{"channel": "books5", "instId": "BTC-USDT"}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_message(data):
|
||||||
|
try:
|
||||||
|
d = orjson.loads(data) if isinstance(data, (bytes, str)) else data
|
||||||
|
# Skip subscribe ack and other event messages
|
||||||
|
if d.get("event"):
|
||||||
|
return
|
||||||
|
arg = d.get("arg") or {}
|
||||||
|
if arg.get("channel") != "books5":
|
||||||
|
return
|
||||||
|
payload_list = d.get("data") or []
|
||||||
|
if not payload_list:
|
||||||
|
return
|
||||||
|
payload = payload_list[0]
|
||||||
|
|
||||||
|
# Each level: [price, size, _liquidations, n_orders] — strings
|
||||||
|
asks_raw = payload.get("asks", [])
|
||||||
|
bids_raw = payload.get("bids", [])
|
||||||
|
book.asks = [OrderBookLevel(float(lv[0]), float(lv[1])) for lv in asks_raw[:20]]
|
||||||
|
book.bids = [OrderBookLevel(float(lv[0]), float(lv[1])) for lv in bids_raw[:20]]
|
||||||
|
book.asks.sort(key=lambda x: x.price)
|
||||||
|
book.bids.sort(key=lambda x: x.price, reverse=True)
|
||||||
|
book.last_update_mono_ns = time.monotonic_ns()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await _ob_ws_loop(
|
||||||
|
SourceName.OKX,
|
||||||
|
"wss://ws.okx.com:8443/ws/v5/public",
|
||||||
|
on_connect, on_message, shutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# All order book feeds
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ALL_ORDERBOOK_FEEDS = {
|
||||||
|
SourceName.COINBASE: coinbase_orderbook_feed,
|
||||||
|
SourceName.KRAKEN: kraken_orderbook_feed,
|
||||||
|
SourceName.BINANCE: binance_orderbook_feed,
|
||||||
|
SourceName.OKX: okx_orderbook_feed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""Direct PM/Chainlink WebSocket feed — independent of trading system."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from src.predictor.models import SourceName, SourceTick
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 5-minute window
|
||||||
|
WINDOW_SEC = 300
|
||||||
|
|
||||||
|
|
||||||
|
class PMDirectFeed:
|
||||||
|
"""
|
||||||
|
Connect directly to Polymarket's Chainlink price stream.
|
||||||
|
Tracks window boundaries (PTB, pm_a, left_sec) internally.
|
||||||
|
Also estimates on-chain PTB from price stability analysis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._ptb: float = 0.0
|
||||||
|
self._bucket_start: int = 0
|
||||||
|
self._ptb_15m: float = 0.0 # PM price at 15min bucket start (= 15m market threshold)
|
||||||
|
self._bucket_15m_start: int = 0
|
||||||
|
self._first_15m_skipped: bool = False
|
||||||
|
self._seq: int = 0
|
||||||
|
self._first_bucket_skipped: bool = False # skip first window if started mid-window
|
||||||
|
# On-chain PTB estimation: ring buffer of (source_ts, price) for last 30s
|
||||||
|
from collections import deque
|
||||||
|
self._price_hist: deque = deque(maxlen=300) # ~30s at ~10 ticks/s
|
||||||
|
self._estimated_onchain_ptb: float = 0.0 # estimated on-chain CL price at window start
|
||||||
|
|
||||||
|
def _get_bucket(self, ts_sec: float) -> int:
|
||||||
|
t = int(ts_sec)
|
||||||
|
return t - (t % WINDOW_SEC)
|
||||||
|
|
||||||
|
def _estimate_onchain_ptb(self, source_ts: float) -> float:
|
||||||
|
"""Estimate on-chain CL price at window boundary.
|
||||||
|
|
||||||
|
On-chain Chainlink updates only when price changes > ~0.5% or heartbeat timeout.
|
||||||
|
If price moved fast in last few seconds, on-chain may still show the old "stable" price.
|
||||||
|
Use median of prices from 3-5 seconds before window boundary as estimate.
|
||||||
|
"""
|
||||||
|
if not self._price_hist:
|
||||||
|
return 0.0
|
||||||
|
# Get prices from 3-10 seconds before current time
|
||||||
|
stable_prices = [px for ts, px in self._price_hist if source_ts - 10 <= ts <= source_ts - 3]
|
||||||
|
if not stable_prices:
|
||||||
|
# Fall back to any prices from 1-5 seconds ago
|
||||||
|
stable_prices = [px for ts, px in self._price_hist if source_ts - 5 <= ts <= source_ts - 1]
|
||||||
|
if not stable_prices:
|
||||||
|
return 0.0
|
||||||
|
stable_prices.sort()
|
||||||
|
return stable_prices[len(stable_prices) // 2] # median
|
||||||
|
|
||||||
|
def _process_price(self, price: float, source_ts_ns: int) -> SourceTick:
|
||||||
|
recv_ns = time.time_ns()
|
||||||
|
mono_ns = time.monotonic_ns()
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
# Window management — use oracle source timestamp for bucketing (not wall clock)
|
||||||
|
# Polymarket settles based on oracle timestamps, not our receive time
|
||||||
|
source_ts = source_ts_ns / 1e9 if source_ts_ns > 0 else now
|
||||||
|
|
||||||
|
# Track price history for on-chain estimation
|
||||||
|
self._price_hist.append((source_ts, price))
|
||||||
|
|
||||||
|
# Track 15min bucket (same logic, 900s instead of 300s) — for 15m market threshold
|
||||||
|
t = int(source_ts)
|
||||||
|
bucket_15m = t - (t % 900)
|
||||||
|
if bucket_15m != self._bucket_15m_start:
|
||||||
|
elapsed_15m = source_ts - bucket_15m
|
||||||
|
if not self._first_15m_skipped and elapsed_15m > 10:
|
||||||
|
self._first_15m_skipped = True
|
||||||
|
self._bucket_15m_start = bucket_15m
|
||||||
|
self._ptb_15m = 0.0
|
||||||
|
else:
|
||||||
|
self._first_15m_skipped = True
|
||||||
|
self._bucket_15m_start = bucket_15m
|
||||||
|
self._ptb_15m = price
|
||||||
|
|
||||||
|
bucket = self._get_bucket(source_ts)
|
||||||
|
if bucket != self._bucket_start:
|
||||||
|
elapsed_in_window = source_ts - bucket
|
||||||
|
# On first window after startup, check if we missed the window-open tick
|
||||||
|
if not self._first_bucket_skipped and elapsed_in_window > 10:
|
||||||
|
# Started mid-window — PTB would be wrong. Skip this window entirely.
|
||||||
|
self._first_bucket_skipped = True
|
||||||
|
self._bucket_start = bucket
|
||||||
|
self._ptb = 0.0 # mark as invalid; will set correctly at next window
|
||||||
|
self._estimated_onchain_ptb = 0.0
|
||||||
|
logger.warning(
|
||||||
|
f"[pm_direct] startup mid-window {bucket} (elapsed={elapsed_in_window:.0f}s), "
|
||||||
|
f"SKIPPING this window for PTB. Waiting for next window boundary."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._first_bucket_skipped = True
|
||||||
|
self._bucket_start = bucket
|
||||||
|
self._ptb = price
|
||||||
|
# Estimate what on-chain CL was at window boundary
|
||||||
|
self._estimated_onchain_ptb = self._estimate_onchain_ptb(source_ts)
|
||||||
|
ptb_diff = self._estimated_onchain_ptb - price if self._estimated_onchain_ptb > 0 else 0
|
||||||
|
logger.info(
|
||||||
|
f"[pm_direct] new window {bucket}, ptb=${price:,.2f}, "
|
||||||
|
f"est_onchain=${self._estimated_onchain_ptb:,.2f} (diff=${ptb_diff:+.1f}) "
|
||||||
|
f"(source_ts lag={now - source_ts:.1f}s)"
|
||||||
|
)
|
||||||
|
if abs(ptb_diff) > 30:
|
||||||
|
logger.warning(
|
||||||
|
f"[pm_direct] ⚠️ PTB vs on-chain estimate differs by ${ptb_diff:+.1f}! "
|
||||||
|
f"On-chain may not have updated. CLOB sanity check should catch this."
|
||||||
|
)
|
||||||
|
|
||||||
|
# If PTB is invalid (skipped window), pm_a is 0 and downstream strategies should treat as no signal
|
||||||
|
pm_a = (price - self._ptb) if self._ptb > 0 else 0.0
|
||||||
|
# Use source_ts for left_sec to be consistent with bucketing
|
||||||
|
left_sec = float(self._bucket_start + WINDOW_SEC) - source_ts
|
||||||
|
|
||||||
|
self._seq += 1
|
||||||
|
|
||||||
|
return SourceTick(
|
||||||
|
source=SourceName.PM,
|
||||||
|
price=price,
|
||||||
|
source_ts_ns=source_ts_ns,
|
||||||
|
recv_mono_ns=mono_ns,
|
||||||
|
recv_ts_ns=recv_ns,
|
||||||
|
pm_ptb=self._ptb,
|
||||||
|
pm_ptb_15m=self._ptb_15m,
|
||||||
|
pm_a=pm_a,
|
||||||
|
left_sec=left_sec,
|
||||||
|
bucket_start_sec=self._bucket_start,
|
||||||
|
estimated_onchain_ptb=self._estimated_onchain_ptb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def pm_direct_task(
|
||||||
|
tick_queue: asyncio.Queue[SourceTick],
|
||||||
|
shutdown: asyncio.Event,
|
||||||
|
url: str = "wss://ws-live-data.polymarket.com",
|
||||||
|
symbol: str = "btc/usd",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Connect directly to Polymarket Chainlink stream.
|
||||||
|
No dependency on trading system.
|
||||||
|
"""
|
||||||
|
feed = PMDirectFeed()
|
||||||
|
reconnect_delay = 0.5
|
||||||
|
max_delay = 5.0
|
||||||
|
|
||||||
|
logger.info(f"[pm_direct] starting: {url}, symbol={symbol}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.ws_connect(
|
||||||
|
url,
|
||||||
|
heartbeat=20.0,
|
||||||
|
receive_timeout=40.0,
|
||||||
|
) as ws:
|
||||||
|
# Subscribe
|
||||||
|
subscribe_msg = {
|
||||||
|
"action": "subscribe",
|
||||||
|
"subscriptions": [{
|
||||||
|
"topic": "crypto_prices_chainlink",
|
||||||
|
"type": "*",
|
||||||
|
"filters": f'{{"symbol":"{symbol}"}}'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
await ws.send_json(subscribe_msg)
|
||||||
|
logger.info(f"[pm_direct] connected: {url}")
|
||||||
|
reconnect_delay = 0.5
|
||||||
|
last_data_mono = time.monotonic()
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
# Staleness check: force reconnect if no data for 3 seconds
|
||||||
|
if time.monotonic() - last_data_mono > 3.0:
|
||||||
|
logger.warning("[pm_direct] stale 3s, forcing reconnect")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = await asyncio.wait_for(ws.receive(), timeout=2.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
|
last_data_mono = time.monotonic()
|
||||||
|
data = msg.data
|
||||||
|
try:
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
parsed = orjson.loads(data)
|
||||||
|
else:
|
||||||
|
parsed = orjson.loads(data.encode())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
payload = parsed.get("payload") or {}
|
||||||
|
|
||||||
|
# Batch format
|
||||||
|
data_list = payload.get("data")
|
||||||
|
if isinstance(data_list, list) and data_list:
|
||||||
|
items = [data_list[-1]]
|
||||||
|
elif payload.get("symbol") and payload.get("value") is not None:
|
||||||
|
items = [payload]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
px_raw = item.get("value")
|
||||||
|
if px_raw is None:
|
||||||
|
continue
|
||||||
|
px = float(px_raw)
|
||||||
|
|
||||||
|
source_ts_raw = item.get("timestamp")
|
||||||
|
if source_ts_raw:
|
||||||
|
source_ts_raw = int(source_ts_raw)
|
||||||
|
# Detect if timestamp is seconds vs milliseconds
|
||||||
|
# Seconds: ~1.7e9, Milliseconds: ~1.7e12
|
||||||
|
if source_ts_raw < 1e11: # seconds
|
||||||
|
source_ts_ns = source_ts_raw * 1_000_000_000
|
||||||
|
else: # milliseconds
|
||||||
|
source_ts_ns = source_ts_raw * 1_000_000
|
||||||
|
else:
|
||||||
|
source_ts_ns = time.time_ns()
|
||||||
|
|
||||||
|
tick = feed._process_price(px, source_ts_ns)
|
||||||
|
try:
|
||||||
|
tick_queue.put_nowait(tick)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[pm_direct] error: {e}, reconnecting in {reconnect_delay:.0f}s")
|
||||||
|
|
||||||
|
if shutdown.is_set():
|
||||||
|
break
|
||||||
|
await asyncio.sleep(reconnect_delay)
|
||||||
|
reconnect_delay = min(reconnect_delay * 2, max_delay)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("[pm_direct] stopped")
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""P&L tracker for predictor trading system."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Trade:
|
||||||
|
"""A single trade record."""
|
||||||
|
ts: int # unix timestamp
|
||||||
|
window_id: str
|
||||||
|
direction: str # UP or DOWN
|
||||||
|
entry_price: float # what we paid per share
|
||||||
|
shares: int # number of shares
|
||||||
|
cost: float # total cost
|
||||||
|
token_id: str
|
||||||
|
order_id: str
|
||||||
|
# Signal data
|
||||||
|
sim_a: float = 0.0
|
||||||
|
pm_a: float = 0.0
|
||||||
|
source_agreement: int = 0
|
||||||
|
ob_depth_btc: float = 0.0
|
||||||
|
reason: str = ""
|
||||||
|
# Settlement (filled in later)
|
||||||
|
settled: bool = False
|
||||||
|
settle_price: float = 0.0 # 0 or 1
|
||||||
|
pnl: float = 0.0
|
||||||
|
won: bool = False
|
||||||
|
filled: bool = False # whether order actually got filled (vs NFIL)
|
||||||
|
direction_correct: bool = False # whether trade direction matched chain settlement
|
||||||
|
chain_direction: str = "" # actual settlement direction from chain
|
||||||
|
|
||||||
|
|
||||||
|
class PnLTracker:
|
||||||
|
"""Track trades and P&L, write to JSONL."""
|
||||||
|
|
||||||
|
def __init__(self, output_dir: str = "data/chainlink_predictor"):
|
||||||
|
self.output_dir = output_dir
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
self._file_path = os.path.join(output_dir, "predictor_trades.jsonl")
|
||||||
|
|
||||||
|
# Cumulative stats
|
||||||
|
self.total_trades: int = 0
|
||||||
|
self.wins: int = 0
|
||||||
|
self.losses: int = 0
|
||||||
|
self.total_pnl: float = 0.0
|
||||||
|
self.pending_trades: list[Trade] = []
|
||||||
|
|
||||||
|
# Restore state from existing file
|
||||||
|
self._restore_from_file()
|
||||||
|
|
||||||
|
self._f = open(self._file_path, "a")
|
||||||
|
|
||||||
|
def _restore_from_file(self):
|
||||||
|
"""Restore pending trades and cumulative stats from existing JSONL file."""
|
||||||
|
if not os.path.exists(self._file_path):
|
||||||
|
return
|
||||||
|
seen_settled = set() # (window_id, direction, order_id) — dedup by order_id
|
||||||
|
all_entries = []
|
||||||
|
try:
|
||||||
|
with open(self._file_path) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
d = orjson.loads(line)
|
||||||
|
all_entries.append(d)
|
||||||
|
if d.get("settled"):
|
||||||
|
key = (d.get("window_id"), d.get("direction"), d.get("order_id", ""))
|
||||||
|
if key in seen_settled:
|
||||||
|
continue # already counted, skip duplicate settle records
|
||||||
|
seen_settled.add(key)
|
||||||
|
if d.get("won"):
|
||||||
|
self.wins += 1
|
||||||
|
else:
|
||||||
|
self.losses += 1
|
||||||
|
self.total_pnl += d.get("pnl", 0)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
# Find unsettled entries that were never settled (match by order_id)
|
||||||
|
settled_order_ids = {k[2] for k in seen_settled if k[2]}
|
||||||
|
for d in all_entries:
|
||||||
|
if not d.get("settled") and d.get("order_id", "") not in settled_order_ids:
|
||||||
|
trade = Trade(
|
||||||
|
ts=d["ts"], window_id=d["window_id"], direction=d["direction"],
|
||||||
|
entry_price=d["entry_price"], shares=d["shares"], cost=d["cost"],
|
||||||
|
token_id=d.get("token_id", ""), order_id=d.get("order_id", ""),
|
||||||
|
sim_a=d.get("sim_a", 0), pm_a=d.get("pm_a", 0),
|
||||||
|
reason=d.get("reason", ""),
|
||||||
|
)
|
||||||
|
self.pending_trades.append(trade)
|
||||||
|
self.total_trades += 1
|
||||||
|
logger.info(
|
||||||
|
f"[pnl] Restored: {self.wins}W/{self.losses}L pnl=${self.total_pnl:+.2f} "
|
||||||
|
f"pending={len(self.pending_trades)}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[pnl] Restore failed: {e}")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._f.close()
|
||||||
|
|
||||||
|
def record_entry(self, trade: Trade):
|
||||||
|
"""Record a new trade entry."""
|
||||||
|
self.pending_trades.append(trade)
|
||||||
|
self.total_trades += 1
|
||||||
|
self._write(trade)
|
||||||
|
logger.info(
|
||||||
|
f"[pnl] ENTRY: {trade.direction} {trade.shares}@{trade.entry_price:.2f} "
|
||||||
|
f"cost=${trade.cost:.2f} window={trade.window_id} reason={trade.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def record_settlement(self, window_id: str, direction: str, won: bool, order_id: str | None = None):
|
||||||
|
"""Record settlement for a single specific trade.
|
||||||
|
If order_id provided, matches exactly. Otherwise matches first window+direction unsettled."""
|
||||||
|
for trade in self.pending_trades:
|
||||||
|
if trade.settled:
|
||||||
|
continue
|
||||||
|
if trade.window_id != window_id or trade.direction != direction:
|
||||||
|
continue
|
||||||
|
if order_id and trade.order_id != order_id:
|
||||||
|
continue
|
||||||
|
# Found the trade — settle just this one
|
||||||
|
trade.settled = True
|
||||||
|
trade.settle_price = 1.0 if won else 0.0
|
||||||
|
if won:
|
||||||
|
trade.pnl = trade.shares * (1.0 - trade.entry_price)
|
||||||
|
trade.won = True
|
||||||
|
self.wins += 1
|
||||||
|
else:
|
||||||
|
trade.pnl = -trade.cost
|
||||||
|
trade.won = False
|
||||||
|
self.losses += 1
|
||||||
|
self.total_pnl += trade.pnl
|
||||||
|
self._write(trade)
|
||||||
|
|
||||||
|
color = "\033[92m" if won else "\033[91m"
|
||||||
|
rst = "\033[0m"
|
||||||
|
w = "WIN" if won else "LOSS"
|
||||||
|
total = self.wins + self.losses
|
||||||
|
wr = f"{self.wins/total*100:.0f}%" if total > 0 else "-"
|
||||||
|
logger.info(
|
||||||
|
f"[pnl] {color}{w}{rst}: {trade.direction} {trade.shares}@{trade.entry_price:.2f} "
|
||||||
|
f"pnl={trade.pnl:+.2f} cum={self.total_pnl:+.2f} | W:{self.wins} L:{self.losses} WR:{wr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up settled trades
|
||||||
|
self.pending_trades = [t for t in self.pending_trades if not t.settled]
|
||||||
|
return trade.pnl # return the actual pnl
|
||||||
|
|
||||||
|
return None # not found
|
||||||
|
|
||||||
|
def _write(self, trade: Trade):
|
||||||
|
"""Write trade to JSONL file."""
|
||||||
|
self._f.write(orjson.dumps(asdict(trade)).decode() + "\n")
|
||||||
|
self._f.flush()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def win_rate(self) -> float:
|
||||||
|
total = self.wins + self.losses
|
||||||
|
return self.wins / total if total > 0 else 0.0
|
||||||
|
|
||||||
|
def summary(self) -> str:
|
||||||
|
total = self.wins + self.losses
|
||||||
|
wr = self.win_rate * 100
|
||||||
|
return (
|
||||||
|
f"Trades: {total} | W:{self.wins} L:{self.losses} | "
|
||||||
|
f"WR:{wr:.1f}% | PnL: ${self.total_pnl:+.2f}"
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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