Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ RUST_LOG=info,dradis=info
# run the Sports Raptor idle/observe-only (neutral snapshot, "Sports Raptor" pill shows offline).
# ODDS_API_KEY=your_the_odds_api_key_here

# ── Optional: Live Tennis API key for the Tennis Raptor ──────────────────────
# Powers the venue-neutral Tennis Raptor's live event-state feed — one tracked
# live match reduced to sets/games/points, the serving side, and a derived
# break-point flag, with feed-staleness reporting. Free tier at
# https://livetennisapi.com (30 req/min, 100 req/day; the default 900s poll
# stays inside the day cap and the raptor logs remaining budget each poll).
# Leave unset to run the Tennis Raptor idle/observe-only (neutral snapshot,
# tennis telemetry reports offline).
# LIVETENNIS_API_KEY=your_livetennisapi_key_here

# ── Optional: LLM Advisor provider ────────────────────────────────────────────
# Default is local/remote Ollama (no key). Set LLM_PROVIDER to use a hosted API:
# ollama — OLLAMA_URL / OLLAMA_MODEL (defaults from config.rs)
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# DRADIS

> **Direct Reaction And Dynamic Intelligence System** — Low-latency Rust prediction-market trading bot for Kalshi & Polymarket. Nine autonomous Viper strategies, a Raptor recon layer (Price, Funding, Derivatives, Tide "Institutional Pulse", Horizon "TradFi Velocity", and a venue-neutral Sports line-movement scout), a Squadron deployment framework, a CAG async dispatch layer with concurrent multi-asset support, a real-time Next.js Control Tower, and an LLM Advisor (Ollama local/remote, OpenAI-compatible, or Anthropic) that delivers optimization recommendations via Telegram & OpenClaw — and can propose or autonomously apply live config changes under a tiered, guard-railed autonomy policy.
> **Direct Reaction And Dynamic Intelligence System** — Low-latency Rust prediction-market trading bot for Kalshi & Polymarket. Nine autonomous Viper strategies, a Raptor recon layer (Price, Funding, Derivatives, Tide "Institutional Pulse", Horizon "TradFi Velocity", a venue-neutral Sports line-movement scout, and a venue-neutral Tennis event-state scout), a Squadron deployment framework, a CAG async dispatch layer with concurrent multi-asset support, a real-time Next.js Control Tower, and an LLM Advisor (Ollama local/remote, OpenAI-compatible, or Anthropic) that delivers optimization recommendations via Telegram & OpenClaw — and can propose or autonomously apply live config changes under a tiered, guard-railed autonomy policy.

![Rust](https://img.shields.io/badge/Rust-1.95+-orange?logo=rust&logoColor=white)
![Tokio](https://img.shields.io/badge/Tokio-async%20runtime-darkgreen?logo=rust&logoColor=white)
Expand Down Expand Up @@ -323,6 +323,7 @@ Raptors are intentionally dumb: **fetch, normalize, broadcast** — no trading l
| **Tide Raptor** | Alpaca IEX + synthetic iNAV | "Institutional Pulse" + coherence from spot-BTC-ETF (IBIT/FBTC/ARKB) premium vs iNAV — BTC-only, US-hours | `src/raptors/tide.rs` |
| **Horizon Raptor** | Alpaca IEX (shared) | TradFi velocity (SPY/QQQ), macro coherence (BTC↔QQQ), VIX proxy (UVXY) — BTC-only, US-hours | `src/raptors/horizon.rs` |
| **Sports Raptor** | The Odds API (h2h) | Vig-free consensus probability, line drift, book dispersion — venue-neutral (US + intl), **observe-only** | `src/raptors/sports.rs` |
| **Tennis Raptor** | Live Tennis API (REST) | Live tennis event state: score, serving side, break-point flag, feed staleness — venue-neutral, **observe-only** | `src/raptors/tennis.rs` |
| *(future)* **Politics Raptor** | Polling aggregators | Approval drift, event probability shifts | — |

When multiple Raptors are active, the GBoost Viper fuses every signal as model features (funding, OI/CVD, institutional pulse/coherence, TradFi velocity/VIX); Basis, Momentum and TrendCapture use them as confirmation gates; Maker and TrendCapture consume the Horizon macro signal as preventative gates (VIX-spike / coherent-TradFi-flow quote suppression, fade veto — observe-first, enforcement behind config flags); and the **Convergence** Viper opens directional positions only when the institutional + derivatives stack agrees. No single Raptor has veto power alone.
Expand All @@ -331,6 +332,8 @@ The **Tide** and **Horizon** Raptors share a single Alpaca IEX WebSocket connect

The **Sports Raptor** is the first non-crypto scout: a single venue-neutral instance shared by both the US and intl pipelines. It polls The Odds API (keyed on `ODDS_API_KEY`), reduces the nearest-commencing event's cross-book moneyline to a vig-free consensus, and broadcasts line drift + book dispersion. It runs **observe-only** — it publishes telemetry but no Viper consumes it for sizing yet — and degrades silently to a neutral snapshot when no API key is set.

The **Tennis Raptor** reads the event itself rather than the betting line: it polls the Live Tennis API's live-match endpoint (keyed on `LIVETENNIS_API_KEY`), tracks one live match (sticky by id, otherwise the freshest score), and broadcasts sets/games/points, the serving side, and a derived break-point flag (receiver at AD, or receiver at 40 vs a server below 40 — never in tiebreaks). Feed health follows the same stale-feed-reads-as-disconnected rule as the other scouts: a score older than `TENNIS_SCORE_STALENESS_SECS` reports `tennis_connected = false` alongside its age, so a consumer widens or pulls, never holds. Honest tier facts: the free tier is 30 req/min / 100 req/day — the default `TENNIS_POLL_SECS = 900` fits all-day polling inside the free cap, while ~60s polling gives near point-level tracking for only ~100 minutes/day (develop-and-test, or following a few matches; sustained fast polling needs a paid tier). The provider's push WebSocket and model win-probability fields are top-tier features and are **not** used — this raptor is free-tier REST only, observe-only, and degrades silently to a neutral snapshot without a key.

---

## ✈️ Viper Wing (`src/vipers/`)
Expand Down
114 changes: 113 additions & 1 deletion src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,43 @@ pub struct AssetRaptorHealth {
/// Comma-separated bookmaker titles in the consensus (e.g. "DraftKings, FanDuel").
#[serde(default)]
pub sports_books: String,

// ── Live Tennis Raptor signal snapshot (Live Tennis API event state) ─────
/// Tennis Raptor's last poll succeeded AND the tracked score is fresh
/// (observe-only). False on failure OR staleness — a stale feed must read
/// as disconnected so a consumer widens/pulls, never holds on it.
pub tennis_connected: bool,
/// Live matches in the sample (0 = no data / nothing on court — neutral).
pub tennis_num_live: Decimal,
/// Sets won by player 1 / player 2 in the tracked match.
pub tennis_sets_p1: Decimal,
pub tennis_sets_p2: Decimal,
/// Games won in the tracked match's current set.
pub tennis_games_p1: Decimal,
pub tennis_games_p2: Decimal,
/// Serving side of the tracked match (1/2; 0 = unknown).
pub tennis_server: Decimal,
/// Receiver holds a break point (never true in a tiebreak).
pub tennis_break_point: bool,
/// The tracked match's current game is a tiebreak.
pub tennis_is_tiebreak: bool,
/// Age (seconds) of the tracked score's API timestamp (-1 = unknown).
pub tennis_feed_age_secs: Decimal,
/// Tracked match label, e.g. "C. Alcaraz vs J. Sinner".
#[serde(default)]
pub tennis_match: String,
/// Tournament name from the feed, e.g. "Cincinnati Open".
#[serde(default)]
pub tennis_tournament: String,
/// Tour of the tracked match ("atp"/"wta"/…); empty when unstated.
#[serde(default)]
pub tennis_tour: String,
/// In-game points as tennis strings, e.g. "30–40" or "AD–40".
#[serde(default)]
pub tennis_points: String,
/// ISO-8601 UTC timestamp of the tracked score (last score change).
#[serde(default)]
pub tennis_score_at: String,
}

// ─── Telemetry ring buffer ────────────────────────────────────────────────────
Expand Down Expand Up @@ -229,6 +266,28 @@ pub struct TelemetrySample {
#[serde(default)]
pub sports_books: String,

// ── Tennis Raptor (live event state) ──
pub tennis_connected: bool,
pub tennis_num_live: Decimal,
pub tennis_sets_p1: Decimal,
pub tennis_sets_p2: Decimal,
pub tennis_games_p1: Decimal,
pub tennis_games_p2: Decimal,
pub tennis_server: Decimal,
pub tennis_break_point: bool,
pub tennis_is_tiebreak: bool,
pub tennis_feed_age_secs: Decimal,
#[serde(default)]
pub tennis_match: String,
#[serde(default)]
pub tennis_tournament: String,
#[serde(default)]
pub tennis_tour: String,
#[serde(default)]
pub tennis_points: String,
#[serde(default)]
pub tennis_score_at: String,

// ── Horizon Raptor (TradFi velocity / VIX proxy) ──
pub horizon_connected: bool,
pub horizon_market_open: bool,
Expand All @@ -255,6 +314,14 @@ const SPORTS_HISTORY_CAP: usize = 1440;
/// series keeps advancing in time and the most-recent point stays reasonably fresh.
/// 1440 points × 30 min ≈ 30 days of retained, readable movement.
const SPORTS_TELEMETRY_HEARTBEAT_SECS: i64 = 1800;
/// The Tennis Raptor is another slow poller (`config::TENNIS_POLL_SECS`, 900s
/// default), so it gets the same change-or-heartbeat de-duplication as the
/// Sports feed, with the same heartbeat: nothing can change between polls, so
/// a heartbeat shorter than the poll interval would only re-store identical
/// points and shrink the retained window. 1440 points × ≥30 min spans the same
/// ~30 days of readable movement as the Sports feed.
const TENNIS_HISTORY_CAP: usize = 1440;
const TENNIS_TELEMETRY_HEARTBEAT_SECS: i64 = 1800;

/// Background task — every `TELEMETRY_SAMPLE_SECS`, snapshot the current Raptor
/// signal values into the per-asset ring buffer. Spawned once by
Expand Down Expand Up @@ -300,6 +367,32 @@ async fn run_telemetry_sampler(
}
}

// Same treatment for the slow Tennis feed: keep a point only when the
// event state actually changes, or once per heartbeat.
if asset == "tennis" {
let changed = match buf.back() {
Some(last) => {
last.tennis_sets_p1 != h.tennis_sets_p1
|| last.tennis_sets_p2 != h.tennis_sets_p2
|| last.tennis_games_p1 != h.tennis_games_p1
|| last.tennis_games_p2 != h.tennis_games_p2
|| last.tennis_points != h.tennis_points
|| last.tennis_server != h.tennis_server
|| last.tennis_break_point != h.tennis_break_point
|| last.tennis_num_live != h.tennis_num_live
|| last.tennis_match != h.tennis_match
|| last.tennis_connected != h.tennis_connected
}
None => true,
};
let heartbeat_due = buf.back()
.map(|last| now - last.t >= TENNIS_TELEMETRY_HEARTBEAT_SECS * 1000)
.unwrap_or(true);
if !changed && !heartbeat_due {
continue;
}
}

buf.push_back(TelemetrySample {
t: now,
oracle_price: h.oracle_price,
Expand Down Expand Up @@ -332,6 +425,21 @@ async fn run_telemetry_sampler(
sports_sport: h.sports_sport.clone(),
sports_commence: h.sports_commence.clone(),
sports_books: h.sports_books.clone(),
tennis_connected: h.tennis_connected,
tennis_num_live: h.tennis_num_live,
tennis_sets_p1: h.tennis_sets_p1,
tennis_sets_p2: h.tennis_sets_p2,
tennis_games_p1: h.tennis_games_p1,
tennis_games_p2: h.tennis_games_p2,
tennis_server: h.tennis_server,
tennis_break_point: h.tennis_break_point,
tennis_is_tiebreak: h.tennis_is_tiebreak,
tennis_feed_age_secs: h.tennis_feed_age_secs,
tennis_match: h.tennis_match.clone(),
tennis_tournament: h.tennis_tournament.clone(),
tennis_tour: h.tennis_tour.clone(),
tennis_points: h.tennis_points.clone(),
tennis_score_at: h.tennis_score_at.clone(),
horizon_connected: h.horizon_connected,
horizon_market_open: h.tradfi_velocity != Decimal::ZERO || h.vix_proxy != Decimal::ZERO,
tradfi_velocity: h.tradfi_velocity,
Expand All @@ -340,7 +448,11 @@ async fn run_telemetry_sampler(
vix_velocity: h.vix_velocity,
});
let len = buf.len();
let cap = if asset == "sports" { SPORTS_HISTORY_CAP } else { TELEMETRY_HISTORY_CAP };
let cap = match asset.as_str() {
"sports" => SPORTS_HISTORY_CAP,
"tennis" => TENNIS_HISTORY_CAP,
_ => TELEMETRY_HISTORY_CAP,
};
if len > cap {
buf.drain(0..len - cap);
}
Expand Down
24 changes: 14 additions & 10 deletions src/cag/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,26 @@ where
yes_fee_bps: hourly_yes_fee_rate,
no_fee_bps: hourly_no_fee_rate,
};
let mut squadron_raptors = SquadronRaptors::full(
raptor_signals.oracle.clone(),
raptor_signals.velocity.clone(),
raptor_signals.drift.clone(),
raptor_signals.funding.clone().expect("funding raptor always present"),
raptor_signals.derivatives.clone().expect("derivatives raptor always present"),
raptor_signals.tide.clone(),
raptor_signals.horizon.clone(),
raptor_signals.sports.clone(),
);
// The observe-only Tennis Raptor feed rides along when deployed
// (attached post-construction so `full()`'s signature stays stable).
squadron_raptors.tennis = raptor_signals.tennis.clone();
let mut squadron = Squadron::new(
patrol_ctx.crypto_filter.parse::<CryptoAsset>().unwrap_or(CryptoAsset::Btc),
SquadronConfig::full_wing(
format!("Full Wing — {}", patrol_ctx.crypto_filter.to_uppercase())
),
hourly_market_config_for_squadron,
SquadronRaptors::full(
raptor_signals.oracle.clone(),
raptor_signals.velocity.clone(),
raptor_signals.drift.clone(),
raptor_signals.funding.clone().expect("funding raptor always present"),
raptor_signals.derivatives.clone().expect("derivatives raptor always present"),
raptor_signals.tide.clone(),
raptor_signals.horizon.clone(),
raptor_signals.sports.clone(),
),
squadron_raptors,
);

// Load squadron-scoped config, preserving operator edits made via the
Expand Down
30 changes: 30 additions & 0 deletions src/config.aggressive.rs.example
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,36 @@ pub const SPORTS_ODDS_REGIONS: &str = "us";
pub const SPORTS_POLL_SECS: u64 = 300;
pub const SPORTS_ODDS_LOW_BUDGET_WARN: i64 = 50;

// ── Tennis Raptor (Live Tennis API live event-state feed) ────────────────────
//
/// Env var carrying the Live Tennis API (livetennisapi.com) key. Absent ⇒ the
/// Tennis Raptor runs idle/observe-only: it seeds a neutral snapshot, reports
/// `tennis_connected = false`, and publishes no event-state signal.
pub const TENNIS_API_KEY_ENV: &str = "LIVETENNIS_API_KEY";

/// Optional tour filter for the live-match poll: `atp`, `wta`, `challenger`,
/// `itf` or `juniors`. Empty ⇒ all tours (the raptor then tracks the live match
/// with the freshest score). Narrow this to the tour whose markets you trade.
pub const TENNIS_TOUR: &str = "";

/// Seconds between Tennis Raptor polls. Honest budget note — the free tier
/// allows 30 req/min and 100 req/day: the 900s default fits ALL-DAY polling
/// inside the free cap (≤96 req/day); ~60s gives near point-level tracking but
/// burns the free day cap in ~100 minutes (fine for develop-and-test or a few
/// tracked matches; sustained all-day fast polling needs a paid tier). The top
/// tier's push WebSocket is NOT used by this raptor — v1 is REST-only, a WS
/// lane would arrive later as a separate config-gated option.
pub const TENNIS_POLL_SECS: u64 = 900;

/// Max age (seconds) of the tracked match's score timestamp before the feed is
/// reported stale (`tennis_connected = false`, so consumers widen/pull rather
/// than hold). Tennis pauses legitimately — changeovers ~90s, set breaks
/// ~120s+ — so this sits well above those to avoid flapping.
pub const TENNIS_SCORE_STALENESS_SECS: u64 = 600;

/// Warn when the API's X-RateLimit remaining-request count drops to this.
pub const TENNIS_LOW_BUDGET_WARN: i64 = 20;


// ============================================================================
// BASIS STRATEGY MAKER ORDER PARAMETERS
Expand Down
30 changes: 30 additions & 0 deletions src/config.balanced.rs.example
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,36 @@ pub const SPORTS_ODDS_REGIONS: &str = "us";
pub const SPORTS_POLL_SECS: u64 = 300;
pub const SPORTS_ODDS_LOW_BUDGET_WARN: i64 = 50;

// ── Tennis Raptor (Live Tennis API live event-state feed) ────────────────────
//
/// Env var carrying the Live Tennis API (livetennisapi.com) key. Absent ⇒ the
/// Tennis Raptor runs idle/observe-only: it seeds a neutral snapshot, reports
/// `tennis_connected = false`, and publishes no event-state signal.
pub const TENNIS_API_KEY_ENV: &str = "LIVETENNIS_API_KEY";

/// Optional tour filter for the live-match poll: `atp`, `wta`, `challenger`,
/// `itf` or `juniors`. Empty ⇒ all tours (the raptor then tracks the live match
/// with the freshest score). Narrow this to the tour whose markets you trade.
pub const TENNIS_TOUR: &str = "";

/// Seconds between Tennis Raptor polls. Honest budget note — the free tier
/// allows 30 req/min and 100 req/day: the 900s default fits ALL-DAY polling
/// inside the free cap (≤96 req/day); ~60s gives near point-level tracking but
/// burns the free day cap in ~100 minutes (fine for develop-and-test or a few
/// tracked matches; sustained all-day fast polling needs a paid tier). The top
/// tier's push WebSocket is NOT used by this raptor — v1 is REST-only, a WS
/// lane would arrive later as a separate config-gated option.
pub const TENNIS_POLL_SECS: u64 = 900;

/// Max age (seconds) of the tracked match's score timestamp before the feed is
/// reported stale (`tennis_connected = false`, so consumers widen/pull rather
/// than hold). Tennis pauses legitimately — changeovers ~90s, set breaks
/// ~120s+ — so this sits well above those to avoid flapping.
pub const TENNIS_SCORE_STALENESS_SECS: u64 = 600;

/// Warn when the API's X-RateLimit remaining-request count drops to this.
pub const TENNIS_LOW_BUDGET_WARN: i64 = 20;


// ============================================================================
// BASIS STRATEGY MAKER ORDER PARAMETERS
Expand Down
Loading
Loading