d5ff0a9df6
Timeframe gained new/millis/seconds/one_minute_ms; add minutes, hours and days alongside them. Each builds on seconds (minutes(5) -> a 300-second bucket), consistent with Timeframe::seconds, and guards the multiplication with checked_mul so an oversized n yields Error::InvalidTimeframe instead of an overflow panic. A non-positive n is rejected by Timeframe::new. Each method carries a runnable doctest, and unit tests cover the known bucket sizes, non-positive rejection and overflow rejection.
196 lines
6.3 KiB
Markdown
196 lines
6.3 KiB
Markdown
# Data Layer (`wickra-data`)
|
|
|
|
`wickra-data` is a separate crate that feeds candles into Wickra's indicators.
|
|
It is not part of `wickra-core` — depend on it explicitly:
|
|
|
|
```toml
|
|
[dependencies]
|
|
wickra = "0.1"
|
|
wickra-data = "0.1"
|
|
```
|
|
|
|
It provides four pieces:
|
|
|
|
- a streaming OHLCV **CSV reader**,
|
|
- a **tick-to-candle aggregator**,
|
|
- a **candle resampler** for multi-timeframe analysis,
|
|
- an optional **Binance Spot WebSocket** kline feed (feature `live-binance`).
|
|
|
|
## CSV reader
|
|
|
|
`CandleReader` streams OHLCV rows out of a CSV file into validated `Candle`
|
|
values.
|
|
|
|
```rust
|
|
use wickra_data::csv::CandleReader;
|
|
|
|
let mut reader = CandleReader::open("ohlcv.csv")?;
|
|
let candles = reader.read_all()?; // Vec<Candle>
|
|
|
|
// Or stream row by row without buffering the whole file:
|
|
let mut reader = CandleReader::open("ohlcv.csv")?;
|
|
for candle in reader.candles() {
|
|
let candle = candle?;
|
|
// feed `candle` into an indicator...
|
|
}
|
|
```
|
|
|
|
The reader is defensive about real-world files:
|
|
|
|
- The first line **must** be a header naming the columns
|
|
`timestamp,open,high,low,close,volume`. A missing column, or a file with no
|
|
header at all, is rejected with a clear `Error::Malformed` instead of
|
|
silently consuming the first data row.
|
|
- A leading UTF-8 byte-order mark (Excel exports one) is stripped.
|
|
- Whitespace around values is trimmed.
|
|
- Each row is validated through `Candle::new`, so an inconsistent OHLC row
|
|
(e.g. `high < low`) surfaces as an error.
|
|
|
|
## Tick aggregator
|
|
|
|
`TickAggregator` rolls a stream of trade `Tick`s up into `Candle`s of an
|
|
arbitrary timeframe. The timeframe's bucket size is in the same unit as the
|
|
tick timestamps (milliseconds for Binance, seconds for daily bars, …).
|
|
Build a `Timeframe` with `Timeframe::new` (a raw bucket size), the
|
|
`millis` / `seconds` / `one_minute_ms` shortcuts, or the `minutes` / `hours` /
|
|
`days` constructors — each of the last three builds on **seconds**, so
|
|
`Timeframe::minutes(5)` is a 300-second bucket.
|
|
|
|
```rust
|
|
use wickra_data::aggregator::{TickAggregator, Timeframe};
|
|
use wickra_core::Tick;
|
|
|
|
let mut agg = TickAggregator::new(Timeframe::one_minute_ms());
|
|
|
|
for tick in trade_feed {
|
|
// push returns every candle that closed because of this tick —
|
|
// empty while the bar grows, one candle when a bar boundary is crossed.
|
|
for closed in agg.push(tick)? {
|
|
// feed `closed` into an indicator...
|
|
}
|
|
}
|
|
|
|
// Capture the final, still-open bar at the end of the stream.
|
|
if let Some(last) = agg.flush()? {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Out-of-order ticks — across or within a bucket — are rejected with
|
|
`Error::Malformed` rather than silently corrupting a bar.
|
|
|
|
### Gap filling
|
|
|
|
By default a tick that jumps across one or more empty buckets simply opens
|
|
the next non-empty bar, leaving a time hole in the output. Enable
|
|
`with_gap_fill` to emit a flat placeholder candle
|
|
(`open == high == low == close`, `volume == 0`) for every skipped bucket, so
|
|
downstream indicators see an unbroken, evenly spaced series:
|
|
|
|
```rust
|
|
let mut agg = TickAggregator::new(Timeframe::one_minute_ms()).with_gap_fill(true);
|
|
```
|
|
|
|
## Resampler
|
|
|
|
`Resampler` rolls an existing candle stream up to a coarser timeframe — for
|
|
example 1-minute bars into 5-minute bars, without touching the original tick
|
|
stream.
|
|
|
|
```rust
|
|
use wickra_data::aggregator::Timeframe;
|
|
use wickra_data::resample::{resample_all, Resampler};
|
|
|
|
// One-shot over an iterator:
|
|
let five_min = resample_all(Timeframe::millis(5 * 60_000), one_min_candles)?;
|
|
|
|
// Or incrementally:
|
|
let mut r = Resampler::new(Timeframe::millis(60 * 60_000)); // 1-hour bars
|
|
for candle in one_min_candles {
|
|
if let Some(closed) = r.push(candle?)? {
|
|
// a coarser bar just closed
|
|
}
|
|
}
|
|
let last = r.flush()?;
|
|
```
|
|
|
|
The output timeframe's bucket must be a multiple of the input timeframe's
|
|
bucket — picking sensible aggregations (1m → 5m → 1h) is the caller's
|
|
responsibility. A candle that arrives in a bucket earlier than the open bar
|
|
is rejected as out of order.
|
|
|
|
## Binance live feed
|
|
|
|
With the `live-binance` feature enabled, `BinanceKlineStream` connects to the
|
|
Binance Spot WebSocket and yields closed klines as candles.
|
|
|
|
```toml
|
|
wickra-data = { version = "0.1", features = ["live-binance"] }
|
|
```
|
|
|
|
```rust
|
|
use wickra::{Indicator, Rsi};
|
|
use wickra_data::live::binance::{BinanceKlineStream, Interval};
|
|
|
|
let mut stream =
|
|
BinanceKlineStream::connect(&["BTCUSDT".into()], Interval::OneMinute).await?;
|
|
let mut rsi = Rsi::new(14)?;
|
|
|
|
while let Some(event) = stream.next_event().await? {
|
|
if event.is_closed {
|
|
if let Some(v) = rsi.update(event.candle.close) {
|
|
println!("RSI = {v:.2}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The stream is resilient: it reconnects with exponential backoff after a
|
|
dropped connection, skips non-kline frames (subscription acks, heartbeats),
|
|
applies a read timeout and message-size limits, and tracks a closed flag so a
|
|
deliberately closed stream is not reused.
|
|
|
|
A runnable example lives at `crates/wickra-data/examples/live_binance.rs`:
|
|
|
|
```bash
|
|
cargo run -p wickra-data --example live_binance --features live-binance
|
|
```
|
|
|
|
## Example datasets
|
|
|
|
The repository ships seven ready-to-use OHLCV datasets under
|
|
`crates/wickra/examples/data/`, one per timeframe, holding real Binance
|
|
**BTCUSDT** spot candles in the standard `timestamp,open,high,low,close,volume`
|
|
layout the `CandleReader` reads. The timestamp is each candle's open time in
|
|
milliseconds.
|
|
|
|
| File | Timeframe | Rows |
|
|
| --- | --- | --- |
|
|
| `btcusdt-1m.csv` | 1 minute | 50 000 |
|
|
| `btcusdt-5m.csv` | 5 minutes | 10 000 |
|
|
| `btcusdt-15m.csv` | 15 minutes | 10 000 |
|
|
| `btcusdt-1h.csv` | 1 hour | 10 000 |
|
|
| `btcusdt-12h.csv` | 12 hours | 5 000 |
|
|
| `btcusdt-1d.csv` | 1 day | full history |
|
|
| `btcusdt-1month.csv` | 1 month | full history |
|
|
|
|
The monthly file is named `btcusdt-1month.csv` rather than `btcusdt-1M.csv` so
|
|
it does not collide with `btcusdt-1m.csv` on case-insensitive filesystems. The
|
|
indicator benchmarks and the `example_data` integration test both run against
|
|
these files.
|
|
|
|
Regenerate them with the latest market history — this downloads from the
|
|
Binance REST API and needs the system `curl` (shipped with Windows 10+, macOS
|
|
and Linux):
|
|
|
|
```bash
|
|
cargo run -p wickra --example fetch_btcusdt
|
|
```
|
|
|
|
## See also
|
|
|
|
- [Quickstart: Rust](Quickstart-Rust.md) — the core indicator API.
|
|
- [Indicators Overview](Indicators-Overview.md) — every indicator and its
|
|
parameters.
|
|
- Source: <https://github.com/kingchenc/wickra>
|