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
13 changes: 11 additions & 2 deletions .github/workflows/package-contents.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Package contents

# Plants untracked files in the tree, then fails unless every file in the
# crate is a tracked source file or metadata cargo generates while packaging.
# The check is an allowlist on purpose: anything else is a failure.
# crate is a tracked source file or metadata cargo generates while packaging,
# and unless every tracked source file is in the crate. The check is an
# allowlist on purpose: anything else is a failure.
on:
pull_request:
push:
Expand All @@ -18,6 +19,8 @@ jobs:
mkdir -p .scratch/probe && echo stray > .scratch/probe/stray.rs
echo stray > examples/stray.rs
echo stray > src/stray.txt
echo stray > src/stray.rs
echo stray > src/resources/stray.rs
- name: The crate contains only tracked files and cargo metadata
run: |
set -euo pipefail
Expand All @@ -27,3 +30,9 @@ jobs:
bad=$(comm -23 /tmp/crate.txt /tmp/allowed.txt)
if [ -n "$bad" ]; then echo "::error::unexpected crate files: $bad"; exit 1; fi
grep -q '^src/lib.rs$' /tmp/crate.txt || { echo "::error::crate has no src/lib.rs"; exit 1; }
- name: Every tracked source file is in the crate
run: |
set -euo pipefail
git ls-files 'src/*.rs' | sort > /tmp/sources.txt
missing=$(comm -23 /tmp/sources.txt /tmp/crate.txt)
if [ -n "$missing" ]; then echo "::error::tracked source files missing from the crate (add them to include in Cargo.toml): $missing"; exit 1; fi
9 changes: 4 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Tests (internal only, not published)
tests/*
!tests/coverage_contract.rs
!tests/rh_lighter_and_positions.rs
!tests/surface_parity.rs
# Local-only test programs, never committed. Every other file under tests/ is
# tracked. Tests do not ship in the crate either way: Cargo.toml's include
# lists what is published.
tests/client_tests.rs
examples/test_all.rs

# Build output
Expand Down
249 changes: 224 additions & 25 deletions CHANGELOG.md

Large diffs are not rendered by default.

43 changes: 40 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,43 @@ keywords = ["market-data", "hyperliquid", "lighterxyz", "orderbook", "trading"]
categories = ["api-bindings", "finance"]
readme = "README.md"
# An allowlist of what ships. Cargo ignores `exclude` once `include` is set, and
# `include` does not consult .gitignore, so examples are named one by one: a
# directory pattern would also pick up untracked local files.
# `include` does not consult .gitignore, so every file is named one by one: a
# glob such as `/src/**/*.rs` would also pick up untracked local files. A new
# source file must be added here; the package-contents workflow fails when a
# tracked source file is missing from the crate.
include = [
"/src/**/*.rs",
"/src/client.rs",
"/src/error.rs",
"/src/exchanges.rs",
"/src/http.rs",
"/src/l4_reconstructor.rs",
"/src/lib.rs",
"/src/orderbook_reconstructor.rs",
"/src/resources/breadth.rs",
"/src/resources/candles.rs",
"/src/resources/cvd.rs",
"/src/resources/data_quality.rs",
"/src/resources/funding.rs",
"/src/resources/instruments.rs",
"/src/resources/l2_orderbook.rs",
"/src/resources/l3_orderbook.rs",
"/src/resources/l4_orderbook.rs",
"/src/resources/liquidations.rs",
"/src/resources/mod.rs",
"/src/resources/open_interest.rs",
"/src/resources/oracle.rs",
"/src/resources/orderbook.rs",
"/src/resources/orders.rs",
"/src/resources/positions.rs",
"/src/resources/spot.rs",
"/src/resources/trades.rs",
"/src/resources/wallets.rs",
"/src/resources/web3.rs",
"/src/resources/webhooks.rs",
"/src/types.rs",
"/src/webhook_signature.rs",
"/src/ws/client.rs",
"/src/ws/mod.rs",
"/examples/basic.rs",
"/examples/levels_smoke.rs",
"/examples/pagination.rs",
Expand Down Expand Up @@ -81,4 +114,8 @@ name = "websocket"
path = "examples/websocket.rs"
required-features = ["websocket"]

[package.metadata.docs.rs]
# Document the `websocket` feature (`oxarchive::ws`) on docs.rs.
all-features = true

[workspace]
901 changes: 609 additions & 292 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async fn main() -> oxarchive::Result<()> {
let oi = client.hyperliquid.open_interest.current("BTC").await?;
println!("BTC open interest: {}", oi.open_interest);

// Lighter.xyz
// Lighter
let lighter = client.lighter.instruments.list().await?;
println!("\nLighter: {} instruments", lighter.len());

Expand Down
38 changes: 24 additions & 14 deletions examples/levels_smoke.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,58 @@
//! Quick live smoke test for the price-level, candle and coverage endpoints.
//! Price levels and liquidations: hourly candles, projected liquidation
//! levels and their history, HIP-3 trigger levels, and the liquidations of
//! one wallet.
//!
//! Run: OXARCHIVE_API_KEY=... cargo run --example levels_smoke

use oxarchive::resources::candles::CandleHistoryParams;
use oxarchive::resources::liquidations::LiquidationsByUserParams;
use oxarchive::types::{CandleInterval, Timestamp};
use oxarchive::{LevelsHistoryParams, LiquidationLevelsParams, OxArchive, TriggerLevelsParams};

const HOUR_MS: i64 = 3_600_000;
const DAY_MS: i64 = 24 * HOUR_MS;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = std::env::var("OXARCHIVE_API_KEY")?;
let client = OxArchive::new(&key)?;
let now = chrono::Utc::now().timestamp_millis();

// C13 regression: candles previously failed on every call (String vs number)
// Hourly BTC candles for the last six hours.
let candles = client
.hyperliquid
.candles
.history(
"BTC",
CandleHistoryParams {
start: Timestamp::Millis(now - 6 * 3_600_000),
start: Timestamp::Millis(now - 6 * HOUR_MS),
end: Timestamp::Millis(now),
cursor: None,
limit: Some(5),
interval: Some(CandleInterval::OneHour),
},
)
.await?;
println!("candles: {} rows, first close {}", candles.data.len(), candles.data[0].close);
println!(
"candles: {} rows, first close {:?}",
candles.data.len(),
candles.data.first().map(|c| &c.close)
);

// New: liquidation levels
// Projected liquidation levels, grouped into 12 price buckets.
let liq = client
.hyperliquid
.liquidations
.levels("BTC", LiquidationLevelsParams { buckets: Some(12), ..Default::default() })
.await?;
println!(
"liq levels: snapshot {} buckets {} total_long {:.0}",
"liquidation levels: snapshot {}, {} buckets, total long {:.0}",
liq.snapshot_ts,
liq.levels.len(),
liq.total_long
);

// New: liquidation levels history (summary)
// Liquidation level history in summary form (bucket detail left out).
let hist = client
.hyperliquid
.liquidations
Expand All @@ -52,37 +62,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)
.await?;
println!(
"liq history: {} items, cursor {:?}, levels omitted: {}",
"liquidation level history: {} snapshots, next cursor {:?}, levels omitted: {}",
hist.data.len(),
hist.next_cursor,
hist.data[0].levels.is_none()
hist.data.iter().all(|item| item.levels.is_none())
);

// New: hip3 trigger levels
// Pending HIP-3 trigger orders (stop-loss and take-profit), by price bucket.
let trig = client
.hyperliquid
.hip3
.orders
.trigger_levels("xyz:TSLA", TriggerLevelsParams { buckets: Some(10), ..Default::default() })
.await?;
println!("hip3 trigger: as_of {} buckets {}", trig.as_of, trig.levels.len());
println!("HIP-3 trigger levels: as of {}, {} buckets", trig.as_of, trig.levels.len());

// C12 regression: by_user previously hit the wrong path
// Liquidations of one wallet over the last seven days.
let by_user = client
.hyperliquid
.liquidations
.by_user(
"0x32fe14732b5b54dc08c6eee46b9ba319ca38e9f4",
LiquidationsByUserParams {
start: Timestamp::Millis(now - 200 * 86_400_000),
start: Timestamp::Millis(now - 7 * DAY_MS),
end: Timestamp::Millis(now),
coin: None,
cursor: None,
limit: Some(5),
},
)
.await?;
println!("by_user: {} rows", by_user.data.len());
println!("liquidations by user: {} rows", by_user.data.len());

Ok(())
}
18 changes: 11 additions & 7 deletions examples/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ async fn main() -> oxarchive::Result<()> {
let api_key = std::env::var("OXARCHIVE_API_KEY").expect("Set OXARCHIVE_API_KEY");
let client = OxArchive::new(api_key)?;

// Paginate through BTC trades for a 24-hour window
// Paginate through BTC trades for the last ten minutes
let end = chrono::Utc::now().timestamp_millis();
let start = end - 10 * 60 * 1000;
let mut all_trades = vec![];
let mut cursor = None;
let mut page = 0u32;
Expand All @@ -15,13 +17,14 @@ async fn main() -> oxarchive::Result<()> {
let result = client
.hyperliquid
.trades
.list(
.history(
"BTC",
GetTradesParams {
start: 1704067200000_i64.into(), // 2024-01-01 00:00 UTC
end: 1704153600000_i64.into(), // 2024-01-02 00:00 UTC
start: start.into(),
end: end.into(),
cursor,
limit: Some(1000),
side: None,
},
)
.await?;
Expand All @@ -31,13 +34,14 @@ async fn main() -> oxarchive::Result<()> {
all_trades.extend(result.data);
println!("Page {page}: fetched {count} trades (total: {})", all_trades.len());

cursor = result.next_cursor;
if cursor.is_none() {
// Follow next_cursor, with the same filters, while has_more is true.
if !result.has_more {
break;
}
cursor = result.next_cursor;
}

println!("\nTotal BTC trades in 24h window: {}", all_trades.len());
println!("\nTotal BTC trades in the last ten minutes: {}", all_trades.len());

Ok(())
}
24 changes: 17 additions & 7 deletions examples/spot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ async fn main() -> oxarchive::Result<()> {
let pairs = client.hyperliquid.spot.pairs.list().await?;
println!("Spot: {} pairs", pairs.len());
for p in pairs.iter().take(5) {
println!(" {} (mark={:?}, mid={:?})", p.symbol, p.mark_price, p.mid_price);
println!(
" {}: base={:?}, quote={:?}, wire={:?}",
p.symbol, p.base, p.quote, p.wire_symbol
);
}

// Get a single pair detail.
let hype = client.hyperliquid.spot.pairs.get("HYPE-USDC").await?;
println!("\nHYPE-USDC: base={:?}, quote={:?}", hype.base, hype.quote);
println!(
"\nHYPE-USDC: base={:?}, quote={:?}, wire={:?}, index={:?}",
hype.base, hype.quote, hype.wire_symbol, hype.spot_index
);

// Current L2 orderbook for HYPE-USDC.
let ob = client.hyperliquid.spot.orderbook.get("HYPE-USDC", None).await?;
Expand All @@ -29,7 +35,8 @@ async fn main() -> oxarchive::Result<()> {
ob.mid_price
);

// Recent trades (last 24h). Spot fills backfill to 2025-03-22.
// Trades from the last 24 hours. Spot trade history is served from
// 2025-03-22T10:50:22Z.
let now_ms = chrono::Utc::now().timestamp_millis();
let day_ago_ms = now_ms - 24 * 60 * 60 * 1000;
let trades = client
Expand All @@ -43,28 +50,31 @@ async fn main() -> oxarchive::Result<()> {
end: now_ms.into(),
cursor: None,
limit: Some(100),
side: None,
},
)
.await?;
println!("\nPURR-USDC trades (last 24h): {}", trades.data.len());

// Candle history starts exactly at 2025-03-22T10:50:22Z; max 1,000 rows.
// One-minute candles for the last hour. Spot candle history is served
// from 2025-03-22T10:50:00Z; a page holds at most 1,000 rows.
let hour_ago_ms = now_ms - 60 * 60 * 1000;
let candles = client
.hyperliquid
.spot
.candles
.history(
"HYPE-USDC",
CandleHistoryParams {
start: 1742640622000_i64.into(),
end: 1742644222000_i64.into(),
start: hour_ago_ms.into(),
end: now_ms.into(),
cursor: None,
limit: Some(1000),
interval: Some(CandleInterval::OneMinute),
},
)
.await?;
println!("HYPE-USDC candles: {} rows", candles.data.len());
println!("HYPE-USDC candles (last hour): {} rows", candles.data.len());

// TWAP statuses for a symbol.
let twap = client
Expand Down
Loading
Loading