Skip to content

Tennis Raptor: live tennis event-state scout (observe-only) - #7

Merged
mbordash merged 2 commits into
mbordash:mainfrom
bensynapse:add-tennis-raptor
Aug 16, 2026
Merged

Tennis Raptor: live tennis event-state scout (observe-only)#7
mbordash merged 2 commits into
mbordash:mainfrom
bensynapse:add-tennis-raptor

Conversation

@bensynapse

Copy link
Copy Markdown
Contributor

Addresses #6 — the tennis event-state Raptor we discussed, built on the Sports Raptor pattern as you suggested.

What it does

  • src/raptors/tennis.rs — venue-neutral, observe-only, shaped like sports.rs: keyed on env LIVETENNIS_API_KEY, parks idle with a neutral snapshot when the key is absent, polls GET /matches?status=live (free tier), and broadcasts a Copy TennisSnapshot over a watch channel + telemetry under a fixed "tennis" health-map key.
  • Signal: sets, current-set games, in-game points, serving side, tiebreak flag, and a derived break-point flag (receiver at AD, or receiver at 40 vs a server below 40 — never in a tiebreak; unknown server/points read as false, never a guess). It tracks one match at a time — sticky on the tracked id while it stays live, otherwise the freshest score — so the state reads as one continuous match, same idea as the Sports Raptor's per-event drift tracking.
  • Feed health: every score carries the API's own last-change timestamp. When the tracked score is older than TENNIS_SCORE_STALENESS_SECS (default 600s — tennis pauses legitimately at changeovers/set breaks), the raptor reports tennis_connected = false plus feed_age_secs, so a consumer treats a stale feed exactly like a missing one — widen/pull, never hold. Zero live matches is reported as healthy-neutral (num_live = 0), not a failure; tennis has quiet hours every day.
  • Wiring: spawned beside the Sports Raptor on the intl and us_retail paths; SquadronRaptors gains an optional tennis receiver, attached post-construction (the same way the US general wing attaches its sports feed) so no constructor signatures changed and all three venue builds stay compatible. Telemetry sampler de-duplicates it like the Sports feed, with a shorter heartbeat so staleness stays visible on the chart. Config block added to all three profile templates, observe-only and inert without a key. No Viper consumes it — that bar stays yours.

Honest cadence / budget

Disclosure again: I run the Live Tennis API. The free tier is 30 req/min and 100 req/day. TENNIS_POLL_SECS defaults to 900 so all-day polling fits inside the free cap (≤96 req/day); dropping it to ~60s gives near point-level tracking but burns the free day cap in about 100 minutes — fine for develop-and-test or following a few matches, sustained fast polling needs a paid tier. The doc comments say all of this. The top tier's push WebSocket and model win-probability fields are not used — v1 is free-tier REST only; a WS lane can come later as a separate config-gated option if the raptor earns it.

Tests

Fields are read against the API's published OpenAPI spec (docs.livetennisapi.com/openapi.yaml), including its documented quirks (null point entries, empty games arrays on completed matches). Parsing is factored into pure functions with 11 inline unit tests: full parse, a break-point truth table, tiebreaks, null/empty score fields, tracked-match stickiness and rotation, staleness flagging, missing timestamps, empty live list, API error bodies, malformed JSON, and URL/tour-filter building.

Locally, per CONTRIBUTING:

cargo test                                        → 142 passed, 0 failed (1 pre-existing ignore)
cargo check                                       → clean (only the 5 pre-existing lib warnings on main)
cargo check --no-default-features --features us_retail  → clean
cargo check --no-default-features --features kalshi     → clean
cargo clippy --all-targets                        → zero findings in the new/changed code

Happy to sign the CLA as soon as the bot posts. Thanks for the quick green light and the pointer at the odds-raptor pattern — it made this a very direct build. If the raptor-via-UI roadmap lands and you'd rather this migrate to that surface, I'm glad to follow up.

🤖 Generated with Claude Code

…-only)

A second venue-neutral macro Raptor beside the Sports Raptor, built on the
same pattern (issue mbordash#6): polls the Live Tennis API free-tier REST endpoint
GET /matches?status=live (Bearer key from LIVETENNIS_API_KEY), tracks one
live match (sticky by id, else freshest score), and broadcasts a Copy
TennisSnapshot — sets, current-set games, serving side, tiebreak flag, and
a derived break-point flag (receiver at AD, or receiver at 40 vs server
below 40; never in tiebreaks) — over a watch channel, with telemetry under
the fixed "tennis" health-map key.

Feed health mirrors the other scouts' stale-feed-reads-as-disconnected
rule: each score carries the API's own last-change timestamp, and when the
tracked score is older than TENNIS_SCORE_STALENESS_SECS the raptor reports
tennis_connected = false plus feed_age_secs, so a consumer widens or pulls
rather than holding. Zero live matches is a healthy neutral state (tennis
has quiet hours), and a missing key parks the raptor idle exactly like the
Sports Raptor.

Honest budget defaults in all three config templates: the free tier is
30 req/min / 100 req/day, so TENNIS_POLL_SECS defaults to 900 (all-day
safe); ~60s polling is documented as develop-and-test / few-match cadence.
The provider's push WebSocket and model win-probability are top-tier
features and are NOT used — v1 is free-tier REST only, observe-only, not
consumed by any Viper sizing.

Wiring: spawned beside the Sports Raptor on the intl and us_retail paths;
SquadronRaptors gains an optional tennis receiver attached
post-construction so no constructor signature changes; telemetry sampler
de-duplicates the slow feed like the Sports feed. 11 unit tests cover
parsing, break-point edge cases, tiebreaks, null/empty score fields,
tracked-match stickiness/rotation, staleness, and error bodies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

✅ All contributors have signed the CLA.
Posted by the CLA Assistant Lite bot.

@bensynapse

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@bensynapse

Copy link
Copy Markdown
Contributor Author

recheck

@bensynapse

Copy link
Copy Markdown
Contributor Author

Heads-up on the red cla check: I posted the signature phrase, but the workflow errors with Branch cla-signatures not found — the storage branch the action writes signatures/cla.json to does not exist in this repo yet (and the CLA_SIGNATURES_TOKEN apparently cannot create it). Once that branch exists (an empty commit is enough), a recheck comment should record my signature and go green. Happy to recheck whenever.

@mbordash

Copy link
Copy Markdown
Owner

recheck

1 similar comment
@mbordash

Copy link
Copy Markdown
Owner

recheck

github-actions Bot added a commit that referenced this pull request Aug 16, 2026
@mbordash

Copy link
Copy Markdown
Owner

ok all good now, thanks for helping me debug. I'll review the PR today.

@mbordash

mbordash commented Aug 16, 2026

Copy link
Copy Markdown
Owner

one thing -- your PR is forcing me in a good way to create raptor setup panel cards in the UI as an initial start -- right now raptors use the .env to setup keys which is kind of ugly. I'm going to take care of that today.

@bensynapse

Copy link
Copy Markdown
Contributor Author

Glad the CLA hunt paid off, and setup-panel cards for raptor keys sounds like exactly the right call — .env works but a panel makes the whole raptor ecosystem feel first-class. When it lands I'm happy to adapt the tennis raptor's config surface to it (should be a small change; everything key-related is already isolated in the config layer). No rush on the review — the PR will keep.

@mbordash mbordash left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really solid work. The break-point tests and the malformed-body ones are exactly what I'd want to see, and thanks for adding the constants to all three profile templates — that one trips people up a lot, since config.rs is gitignored and CI builds off the balanced example.

Pulled it down locally, builds clean and all 11 of your tests pass.

Three small things inline. One other gap: LIVETENNIS_API_KEY does still need an entry in .env.example in case users want to run in headless mode without the UI. Look at ODDS_API_KEY at line 210 for a good template.

Really glad to have this in, it's the first contributed raptor and a good template for the next one.

Comment thread src/api/server.rs Outdated
Comment thread src/main.rs Outdated
Comment thread src/raptors/tennis.rs
…mple

Address PR mbordash#7 review:

- Telemetry heartbeat 300s → 1800s to match the Sports feed. The raptor
  polls every 900s, so nothing can change between polls — the shorter
  heartbeat only re-stored identical points and cut retained history to
  ~5 days instead of ~30. Comment rewritten to state the real rationale.
- US build: thread the tennis receiver through run_us_trader → run_wing →
  trade_one_market → register_us_squadron, attached to both wings'
  SquadronRaptors exactly like us_sports_rx, so the channel stays live for
  the first consumer instead of relying on a scope-held binding.
- tennis.rs header doc: state up front that the tracked match is sticky on
  the previous id, else freshest score, and is NOT yet tied to a specific
  venue market (left to the first consumer).
- .env.example: LIVETENNIS_API_KEY entry per the ODDS_API_KEY template,
  for headless runs without the UI.

cargo test green (142 passed), us_retail + kalshi feature checks clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bensynapse

Copy link
Copy Markdown
Contributor Author

All four addressed in c68e1b3 — the three inline items (heartbeat → 1800 with an honest comment, tennis receiver threaded through the US trader like us_sports_rx, tracked-match/no-market-linkage doc up front) plus the .env.example entry for LIVETENNIS_API_KEY, following the ODDS_API_KEY template with the same shape: what it powers, free-tier numbers, and what "leave unset" does.

Re-ran the gate after the changes: cargo test 142 passed / 0 failed, cargo check clean on default + us_retail + kalshi, clippy still zero findings in the new code.

Thanks for pulling it down and running the tests yourself — and for the fast, precise review. Glad this can serve as the template for the next raptor.

@mbordash

Copy link
Copy Markdown
Owner

Thank you-- I ended up creating the new UI panels for Raptors this morning and will wire your's up when I merge everything together.

@mbordash
mbordash merged commit da089ae into mbordash:main Aug 16, 2026
4 checks passed
@bensynapse

Copy link
Copy Markdown
Contributor Author

Merged same-day — thank you! And glad the raptor ended up pulling the setup-panel work forward; that's the best kind of side effect. When the panel wiring lands, ping me (or just open an issue) and I'll adapt the tennis raptor's config surface to it — everything key-related is isolated in one place, so it should be a small, clean change.

Happy to be raptor #1 in the nest.

mbordash pushed a commit that referenced this pull request Aug 16, 2026
* feat(raptors): Tennis Raptor — live tennis event-state scout (observe-only)

A second venue-neutral macro Raptor beside the Sports Raptor, built on the
same pattern (issue #6): polls the Live Tennis API free-tier REST endpoint
GET /matches?status=live (Bearer key from LIVETENNIS_API_KEY), tracks one
live match (sticky by id, else freshest score), and broadcasts a Copy
TennisSnapshot — sets, current-set games, serving side, tiebreak flag, and
a derived break-point flag (receiver at AD, or receiver at 40 vs server
below 40; never in tiebreaks) — over a watch channel, with telemetry under
the fixed "tennis" health-map key.

Feed health mirrors the other scouts' stale-feed-reads-as-disconnected
rule: each score carries the API's own last-change timestamp, and when the
tracked score is older than TENNIS_SCORE_STALENESS_SECS the raptor reports
tennis_connected = false plus feed_age_secs, so a consumer widens or pulls
rather than holding. Zero live matches is a healthy neutral state (tennis
has quiet hours), and a missing key parks the raptor idle exactly like the
Sports Raptor.

Honest budget defaults in all three config templates: the free tier is
30 req/min / 100 req/day, so TENNIS_POLL_SECS defaults to 900 (all-day
safe); ~60s polling is documented as develop-and-test / few-match cadence.
The provider's push WebSocket and model win-probability are top-tier
features and are NOT used — v1 is free-tier REST only, observe-only, not
consumed by any Viper sizing.

Wiring: spawned beside the Sports Raptor on the intl and us_retail paths;
SquadronRaptors gains an optional tennis receiver attached
post-construction so no constructor signature changes; telemetry sampler
de-duplicates the slow feed like the Sports feed. 11 unit tests cover
parsing, break-point edge cases, tiebreaks, null/empty score fields,
tracked-match stickiness/rotation, staleness, and error bodies.


* review: telemetry heartbeat, US-build tennis receiver, doc + .env.example

Address PR #7 review:

- Telemetry heartbeat 300s → 1800s to match the Sports feed. The raptor
  polls every 900s, so nothing can change between polls — the shorter
  heartbeat only re-stored identical points and cut retained history to
  ~5 days instead of ~30. Comment rewritten to state the real rationale.
- US build: thread the tennis receiver through run_us_trader → run_wing →
  trade_one_market → register_us_squadron, attached to both wings'
  SquadronRaptors exactly like us_sports_rx, so the channel stays live for
  the first consumer instead of relying on a scope-held binding.
- tennis.rs header doc: state up front that the tracked match is sticky on
  the previous id, else freshest score, and is NOT yet tied to a specific
  venue market (left to the first consumer).
- .env.example: LIVETENNIS_API_KEY entry per the ODDS_API_KEY template,
  for headless runs without the UI.

cargo test green (142 passed), us_retail + kalshi feature checks clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants