feat(data-layer): TickAggregator (tick-to-candle) in all 10 languages (#309)

* feat(data-layer): TickAggregator in Node, WASM, Python + C ABI hub

First data-layer feature (F2): roll trade ticks up into fixed-timeframe OHLCV
candles, exposed natively and over the C ABI.

- wickra-data wired as a binding dependency (workspace dep; its wickra-core dep
  is default-features=false so it never forces rayon into the rayon-free WASM
  build — native bindings re-enable parallel through their own dependency).
- Node `TickAggregator(bucket, gapFill?)` -> `push(price, size, ts): Candle[]`;
  WASM the same (array of objects); Python `push(...) -> list[tuple]`.
- C ABI: `WickraCandle` struct + `wickra_tick_aggregator_new/push/free` (push
  writes candles into a caller buffer and returns the count), generated via the
  capi generator's new DATA_LAYER section; cbindgen now parses wickra-data so
  `TickAggregator` is a forward-declared opaque; header vendored to bindings/go.

Verified bit-identical across Node/WASM/Python/C/C++ (o=100 h=101 l=100 c=101
v=3 ts=0 for the shared 3-tick probe). WIP: Go/C#/Java/R generated bindings and
the cross-language golden are still pending.

* feat(data-layer): TickAggregator in Go, C#, Java, R (lossless push/drain)

Complete F2 across all 10 languages: the C-ABI tick aggregator now uses a
two-step push/drain so gap-fill candles are never lost, and the four generated
bindings expose it idiomatically.

- C ABI redesigned: opaque TickAggregator handle (inner aggregator + pending
  buffer); push consumes a tick and returns the closed-candle count, drain copies
  them into a count-sized caller buffer.
- Go: NewTickAggregator + Push(price,size,ts) []Candle; C#: TickAggregator +
  Candle[] Push(...); Java: TickAggregator + Candle[] push(...); R: TickAggregator
  constructor + push() S3 generic returning an (n x 6) numeric matrix.
- Candle output record generated per language from WickraCandle.

Verified bit-identical to the native bindings (o=100 h=101 l=100 c=101 v=3 ts=0)
in Go, C#, Java, and R at runtime; R passes R CMD check (pre-existing doc
warnings only). WIP: cross-language data-layer golden + CHANGELOG still pending.

* test(data-layer): cross-language golden for the tick aggregator + CHANGELOG

gen_golden emits a deterministic tick stream (testdata/golden/data_ticks.csv) and
the reference candle streams with and without gap filling (data_candles.csv,
data_candles_gap.csv). Every binding replays the shared ticks through its
TickAggregator and checks the candles bit-for-bit (fp tolerance) against the Rust
reference:

- Node / WASM / Python / Go / C# / Java / R: a dedicated parity test each.
- C / C++: data_layer_test.c (compiled as both, run as ctest).

The gap-fill fixture closes several candles from a single push, exercising the
lossless push/drain path. Records the feature under CHANGELOG [Unreleased].

* fix(examples): rename the CSV-loader candle to WickraBar

The example CSV helper (wickra_csv.h) defined its own struct WickraCandle, which
now collides with the public C ABI WickraCandle (the tick aggregator output) in
any example that includes both headers (backtest, multi_timeframe, the strategy
examples). The public type owns the name; rename the example loader's bar to
WickraBar. The generated golden_test.c is untouched (its only match was the
unrelated WickraCandleVolumeOutput).
This commit is contained in:
kingchenc
2026-06-15 21:24:33 +02:00
committed by GitHub
parent fd9f4c8bc6
commit 8a103ef920
48 changed files with 1368 additions and 29 deletions
+11
View File
@@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Tick-to-candle aggregation in all 10 languages (data layer).** The
`TickAggregator` — roll trade ticks up into fixed-timeframe OHLCV candles, with
optional gap filling — is now exposed natively (Node.js / WASM `push(price,
size, ts): Candle[]`, Python `push(...) -> list[tuple]`) and over the C ABI as
Go `Push() []Candle`, C# `Candle[] Push()`, Java `Candle[] push()`, and the R
`push()` generic (an `n×6` matrix); C / C++ call the C ABI directly. The C ABI
uses a lossless two-step `wickra_tick_aggregator_push` / `_drain` so a single
push that gap-fills across many empty buckets never overflows a fixed buffer. A
new cross-language golden (`testdata/golden/data_*.csv`) pins the candle stream
identically across every binding. This is the first feature of a data layer that
makes the non-Rust bindings dependency-free for tick aggregation.
- **`name()` on every indicator in all 10 languages.** The canonical
`Indicator::name()` / `BarBuilder::name()` accessor is now exposed through every
binding — Node.js `name()`, WASM `name()`, Python `name()`, and the C ABI
Generated
+4
View File
@@ -1969,6 +1969,7 @@ name = "wickra-c"
version = "0.9.2"
dependencies = [
"wickra-core",
"wickra-data",
]
[[package]]
@@ -2016,6 +2017,7 @@ dependencies = [
"napi-build",
"napi-derive",
"wickra-core",
"wickra-data",
]
[[package]]
@@ -2025,6 +2027,7 @@ dependencies = [
"numpy",
"pyo3",
"wickra-core",
"wickra-data",
]
[[package]]
@@ -2038,6 +2041,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
"wickra-core",
"wickra-data",
]
[[package]]
+1
View File
@@ -27,6 +27,7 @@ categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.9.2" }
wickra-data = { path = "crates/wickra-data", version = "0.9.2" }
thiserror = "2"
rayon = "1.10"
+1
View File
@@ -57,3 +57,4 @@ parallel = ["wickra-core/parallel"]
# does not set it, which would leave rayon in the wasm build. wickra-c is
# `publish = false`, so no version pin is needed (and none to keep in sync).
wickra-core = { path = "../../crates/wickra-core", default-features = false }
wickra-data = { path = "../../crates/wickra-data" }
+1 -1
View File
@@ -17,4 +17,4 @@ documentation = false
# discovered and emitted as forward-declared opaque structs. Their fields are
# never exposed — only `T *` handles cross the boundary.
parse_deps = true
include = ["wickra-core"]
include = ["wickra-core", "wickra-data"]
+24
View File
@@ -876,6 +876,8 @@ typedef struct ThreeStarsInSouth ThreeStarsInSouth;
typedef struct Thrusting Thrusting;
typedef struct TickAggregator TickAggregator;
typedef struct TickBars TickBars;
typedef struct TickIndex TickIndex;
@@ -1675,6 +1677,15 @@ typedef struct WickraFootprintLevel {
double ask_vol;
} WickraFootprintLevel;
typedef struct WickraCandle {
double open;
double high;
double low;
double close;
double volume;
int64_t timestamp;
} WickraCandle;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
@@ -13695,6 +13706,19 @@ void wickra_footprint_reset(struct Footprint *handle);
void wickra_footprint_free(struct Footprint *handle);
struct TickAggregator *wickra_tick_aggregator_new(int64_t bucket, bool gap_fill);
intptr_t wickra_tick_aggregator_push(struct TickAggregator *handle,
double price,
double size,
int64_t timestamp);
intptr_t wickra_tick_aggregator_drain(struct TickAggregator *handle,
struct WickraCandle *out,
uintptr_t cap);
void wickra_tick_aggregator_free(struct TickAggregator *handle);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
+119 -3
View File
@@ -98,9 +98,9 @@ use wickra_core::{
TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure,
TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, Tema,
TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineBreakBars,
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickBars,
TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, Trade,
TradeImbalance, TradeQuote, TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel,
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tick,
TickBars, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile,
Trade, TradeImbalance, TradeQuote, TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel,
TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer,
TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
@@ -114,6 +114,8 @@ use wickra_core::{
T3,
};
use wickra_data::aggregator::{TickAggregator as DataTickAggregator, Timeframe};
// ===== Scalar indicators (f64 -> f64) =====
/// Create a `AdaptiveCycle` indicator.
@@ -68072,6 +68074,120 @@ pub unsafe extern "C" fn wickra_footprint_free(handle: *mut Footprint) {
}
}
// ===== Data layer (tick aggregation; caller buffer + count) =====
/// C-ABI view of an OHLCV candle (the tick aggregator's output).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCandle {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub timestamp: i64,
}
/// Opaque tick aggregator: the streaming aggregator plus the buffer of candles
/// the most recent `push` closed, awaiting `drain`. Named `TickAggregator` (the
/// public C-ABI handle); the inner `wickra-data` type is aliased to avoid the
/// name clash.
#[derive(Debug)]
pub struct TickAggregator {
inner: DataTickAggregator,
pending: Vec<Candle>,
}
/// Create a tick aggregator with the given bucket size (the same unit as the
/// tick timestamps). `gap_fill` emits a flat placeholder candle for every
/// skipped bucket. Returns `NULL` on a non-positive bucket; release with
/// `wickra_tick_aggregator_free`.
#[no_mangle]
pub extern "C" fn wickra_tick_aggregator_new(bucket: i64, gap_fill: bool) -> *mut TickAggregator {
match Timeframe::new(bucket) {
Ok(timeframe) => Box::into_raw(Box::new(TickAggregator {
inner: DataTickAggregator::new(timeframe).with_gap_fill(gap_fill),
pending: Vec::new(),
})),
Err(_) => ptr::null_mut(),
}
}
/// Push one trade tick. Buffers the candles it closed inside the handle and
/// returns the count (`0` if the open bar merely grew), or `-1` on a `NULL`
/// handle / invalid tick / malformed timestamp. Read the candles with
/// `wickra_tick_aggregator_drain`; the next `push` replaces the buffer.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_aggregator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_push(
handle: *mut TickAggregator,
price: f64,
size: f64,
timestamp: i64,
) -> isize {
let Some(aggregator) = handle.as_mut() else {
return -1;
};
let Ok(tick) = Tick::new(price, size, timestamp) else {
return -1;
};
let Ok(candles) = aggregator.inner.push(tick) else {
return -1;
};
aggregator.pending = candles;
isize::try_from(aggregator.pending.len()).unwrap_or(isize::MAX)
}
/// Copy up to `cap` buffered candles (from the last `push`) into `out`, remove
/// them from the buffer, and return the number written. Returns `0` on a `NULL`
/// handle / `out`. Call with `cap` equal to the last `push` return to drain the
/// whole batch losslessly.
///
/// # Safety
/// `handle` (from `wickra_tick_aggregator_new`, not freed) and `out` must be valid
/// or `NULL`; when non-`NULL`, `out` must cover `cap` `WickraCandle` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_drain(
handle: *mut TickAggregator,
out: *mut WickraCandle,
cap: usize,
) -> isize {
let Some(aggregator) = handle.as_mut() else {
return 0;
};
if out.is_null() {
return 0;
}
let count = aggregator.pending.len().min(cap);
let slots = slice::from_raw_parts_mut(out, count);
for (slot, candle) in slots.iter_mut().zip(aggregator.pending.drain(..count)) {
*slot = WickraCandle {
open: candle.open,
high: candle.high,
low: candle.low,
close: candle.close,
volume: candle.volume,
timestamp: candle.timestamp,
};
}
isize::try_from(count).unwrap_or(isize::MAX)
}
/// Destroy a tick aggregator created by `wickra_tick_aggregator_new`. No-op if
/// `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tick_aggregator_new` and not
/// previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_free(handle: *mut TickAggregator) {
if !handle.is_null() {
drop(Box::from_raw(handle));
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Wickra;
using Xunit;
// Cross-language data-layer parity: replay the shared golden tick stream through
// the TickAggregator and check the candles against the Rust reference, with and
// without gap filling.
public class DataLayerTests
{
private static string GoldenDir([System.Runtime.CompilerServices.CallerFilePath] string file = "") =>
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
private static double[][] Read(string name)
{
var lines = File.ReadAllLines(Path.Combine(GoldenDir(), name + ".csv"));
var rows = new List<double[]>();
for (var i = 1; i < lines.Length; i++)
{
if (lines[i].Length == 0)
{
continue;
}
rows.Add(Array.ConvertAll(lines[i].Split(','), s => double.Parse(s, CultureInfo.InvariantCulture)));
}
return rows.ToArray();
}
[Theory]
[InlineData(false, "data_candles")]
[InlineData(true, "data_candles_gap")]
public void TickAggregatorMatchesGolden(bool gapFill, string fixture)
{
var ticks = Read("data_ticks");
using var agg = new TickAggregator(1000, gapFill);
var got = new List<double[]>();
foreach (var t in ticks)
{
foreach (var c in agg.Push(t[0], t[1], (long)t[2]))
{
got.Add(new[] { c.Open, c.High, c.Low, c.Close, c.Volume, (double)c.Timestamp });
}
}
var want = Read(fixture);
Assert.Equal(want.Length, got.Count);
for (var i = 0; i < got.Count; i++)
{
for (var j = 0; j < 6; j++)
{
var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j]));
Assert.True(
Math.Abs(got[i][j] - want[i][j]) <= tol,
$"{fixture} row {i} col {j}: {got[i][j]} vs {want[i][j]}");
}
}
}
}
@@ -16,6 +16,7 @@ public readonly record struct AutoFibOutput(double Level0, double Level236, doub
public readonly record struct BollingerOutput(double Upper, double Middle, double Lower, double Stddev);
public readonly record struct BomarBandsOutput(double Upper, double Middle, double Lower);
public readonly record struct CamarillaPivotsOutput(double Pp, double R1, double R2, double R3, double R4, double S1, double S2, double S3, double S4);
public readonly record struct Candle(double Open, double High, double Low, double Close, double Volume, double Timestamp);
public readonly record struct CandleVolumeOutput(double Body, double Width);
public readonly record struct CentralPivotRangeOutput(double Pivot, double Tc, double Bc);
public readonly record struct ChandeKrollStopOutput(double StopLong, double StopShort);
@@ -34704,6 +34705,51 @@ public sealed class Thrusting : IDisposable
public void Dispose() => _handle.Dispose();
}
public sealed class TickAggregator : IDisposable
{
private readonly WickraHandle _handle;
public TickAggregator(long bucket, bool gapFill)
{
var ptr = NativeMethods.wickra_tick_aggregator_new(bucket, gapFill);
if (ptr == nint.Zero)
{
throw new ArgumentException("invalid TickAggregator parameters");
}
_handle = new WickraHandle(ptr, NativeMethods.wickra_tick_aggregator_free);
}
/// <summary>Feed one trade tick; returns the candles it closed.</summary>
public Candle[] Push(double price, double size, long timestamp)
{
var count = (long)NativeMethods.wickra_tick_aggregator_push(_handle.DangerousGetHandle(), price, size, timestamp);
GC.KeepAlive(_handle);
if (count <= 0)
{
return Array.Empty<Candle>();
}
var buffer = new WickraCandle[count];
unsafe
{
fixed (WickraCandle* ptr = buffer)
{
NativeMethods.wickra_tick_aggregator_drain(_handle.DangerousGetHandle(), ptr, (nuint)count);
}
}
GC.KeepAlive(_handle);
var result = new Candle[count];
for (var i = 0; i < count; i++)
{
result[i] = new Candle(buffer[i].open, buffer[i].high, buffer[i].low, buffer[i].close, buffer[i].volume, buffer[i].timestamp);
}
return result;
}
public void Dispose() => _handle.Dispose();
}
public sealed class TickBars : IDisposable
{
private readonly WickraHandle _handle;
@@ -12404,6 +12404,18 @@ internal static partial class NativeMethods
[LibraryImport(WickraNative.LibraryName)]
internal static partial void wickra_footprint_free(nint handle);
[LibraryImport(WickraNative.LibraryName)]
internal static partial nint wickra_tick_aggregator_new(long bucket, [MarshalAs(UnmanagedType.U1)] bool gapFill);
[LibraryImport(WickraNative.LibraryName)]
internal static partial nint wickra_tick_aggregator_push(nint handle, double price, double size, long timestamp);
[LibraryImport(WickraNative.LibraryName)]
internal static unsafe partial nint wickra_tick_aggregator_drain(nint handle, WickraCandle* @out, nuint cap);
[LibraryImport(WickraNative.LibraryName)]
internal static partial void wickra_tick_aggregator_free(nint handle);
}
[StructLayout(LayoutKind.Sequential)]
@@ -12503,6 +12515,17 @@ internal static partial class NativeMethods
public double s4;
}
[StructLayout(LayoutKind.Sequential)]
internal struct WickraCandle
{
public double open;
public double high;
public double low;
public double close;
public double volume;
public long timestamp;
}
[StructLayout(LayoutKind.Sequential)]
internal struct WickraCandleVolumeOutput
{
+58
View File
@@ -0,0 +1,58 @@
package wickra
import (
"math"
"strconv"
"testing"
)
// Cross-language data-layer parity: replay the shared golden tick stream through
// the TickAggregator and assert the candles match the Rust reference, with and
// without gap filling. Fixtures are generated by
// `cargo run -p wickra-examples --bin gen_golden`.
func dataParseF(t *testing.T, s string) float64 {
t.Helper()
v, err := strconv.ParseFloat(s, 64)
if err != nil {
t.Fatalf("parse %q: %v", s, err)
}
return v
}
func TestTickAggregatorGolden(t *testing.T) {
ticks := readGolden(t, "data_ticks")
cases := []struct {
gap bool
fixture string
}{
{false, "data_candles"},
{true, "data_candles_gap"},
}
for _, c := range cases {
agg, err := NewTickAggregator(1000, c.gap)
if err != nil {
t.Fatalf("new: %v", err)
}
var got [][6]float64
for _, r := range ticks {
price, size, ts := dataParseF(t, r[0]), dataParseF(t, r[1]), int64(dataParseF(t, r[2]))
for _, k := range agg.Push(price, size, ts) {
got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)})
}
}
agg.Close()
want := readGolden(t, c.fixture)
if len(got) != len(want) {
t.Fatalf("%s: %d candles vs %d", c.fixture, len(got), len(want))
}
for i := range got {
for j := 0; j < 6; j++ {
w := dataParseF(t, want[i][j])
if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) {
t.Errorf("%s row %d col %d: %v vs %v", c.fixture, i, j, got[i][j], w)
}
}
}
}
}
+24
View File
@@ -876,6 +876,8 @@ typedef struct ThreeStarsInSouth ThreeStarsInSouth;
typedef struct Thrusting Thrusting;
typedef struct TickAggregator TickAggregator;
typedef struct TickBars TickBars;
typedef struct TickIndex TickIndex;
@@ -1675,6 +1677,15 @@ typedef struct WickraFootprintLevel {
double ask_vol;
} WickraFootprintLevel;
typedef struct WickraCandle {
double open;
double high;
double low;
double close;
double volume;
int64_t timestamp;
} WickraCandle;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
@@ -13695,6 +13706,19 @@ void wickra_footprint_reset(struct Footprint *handle);
void wickra_footprint_free(struct Footprint *handle);
struct TickAggregator *wickra_tick_aggregator_new(int64_t bucket, bool gap_fill);
intptr_t wickra_tick_aggregator_push(struct TickAggregator *handle,
double price,
double size,
int64_t timestamp);
intptr_t wickra_tick_aggregator_drain(struct TickAggregator *handle,
struct WickraCandle *out,
uintptr_t cap);
void wickra_tick_aggregator_free(struct TickAggregator *handle);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
+55
View File
@@ -102,6 +102,16 @@ type CamarillaPivotsOutput struct {
S4 float64
}
// Candle is the output of the Candle indicator.
type Candle struct {
Open float64
High float64
Low float64
Close float64
Volume float64
Timestamp int64
}
// CandleVolumeOutput is the output of the CandleVolume indicator.
type CandleVolumeOutput struct {
Body float64
@@ -36391,6 +36401,51 @@ func (ind *Thrusting) Close() {
}
}
// TickAggregator wraps the TickAggregator indicator over the Wickra C ABI.
type TickAggregator struct {
handle *C.struct_TickAggregator
}
// NewTickAggregator constructs a TickAggregator. It returns ErrInvalidParams when the
// native constructor rejects the arguments.
func NewTickAggregator(bucket int64, gapFill bool) (*TickAggregator, error) {
ptr := C.wickra_tick_aggregator_new(C.int64_t(bucket), C.bool(gapFill))
if ptr == nil {
return nil, ErrInvalidParams
}
obj := &TickAggregator{handle: ptr}
runtime.SetFinalizer(obj, (*TickAggregator).Close)
return obj, nil
}
// Push feeds one trade tick and returns the candles it closed (none while
// the open bar grows, one per closed bucket, plus gap-fill placeholders).
func (ind *TickAggregator) Push(price float64, size float64, timestamp int64) []Candle {
n := int(C.wickra_tick_aggregator_push(ind.handle, C.double(price), C.double(size), C.int64_t(timestamp)))
runtime.KeepAlive(ind)
if n <= 0 {
return nil
}
buf := make([]C.struct_WickraCandle, n)
C.wickra_tick_aggregator_drain(ind.handle, &buf[0], C.uintptr_t(n))
runtime.KeepAlive(ind)
out := make([]Candle, n)
for i := 0; i < n; i++ {
out[i] = Candle{float64(buf[i].open), float64(buf[i].high), float64(buf[i].low), float64(buf[i].close), float64(buf[i].volume), int64(buf[i].timestamp)}
}
return out
}
// Close frees the native handle. It is idempotent and safe to call
// alongside the finalizer.
func (ind *TickAggregator) Close() {
if ind.handle != nil {
C.wickra_tick_aggregator_free(ind.handle)
ind.handle = nil
runtime.SetFinalizer(ind, nil)
}
}
// TickBars wraps the TickBars indicator over the Wickra C ABI.
type TickBars struct {
handle *C.struct_TickBars
@@ -0,0 +1,5 @@
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
package org.wickra;
/** Output record produced by the matching indicator. */
public record Candle(double open, double high, double low, double close, double volume, double timestamp) {}
@@ -0,0 +1,62 @@
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
package org.wickra;
import org.wickra.internal.NativeMethods;
import org.wickra.internal.WickraNative;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.ref.Cleaner;
import static java.lang.foreign.ValueLayout.*;
/** Streaming TickAggregator indicator over the Wickra C ABI. Not thread-safe; close when done. */
public final class TickAggregator implements AutoCloseable {
private final MemorySegment handle;
private final Cleaner.Cleanable cleanable;
public TickAggregator(long bucket, boolean gapFill) {
MemorySegment h;
try {
h = (MemorySegment) NativeMethods.WICKRA_TICK_AGGREGATOR_NEW.invokeExact(bucket, (byte) (gapFill ? 1 : 0));
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
if (h.address() == 0L) {
throw new IllegalArgumentException("invalid TickAggregator parameters");
}
this.handle = h;
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_TICK_AGGREGATOR_FREE);
}
/** Feed one trade tick; returns the candles it closed (possibly empty). */
public Candle[] push(double price, double size, long timestamp) {
try {
long n = (long) NativeMethods.WICKRA_TICK_AGGREGATOR_PUSH.invokeExact(handle, price, size, timestamp);
if (n <= 0) {
return new Candle[0];
}
try (Arena a = Arena.ofConfined()) {
MemorySegment out = a.allocate(48L * n);
long w = (long) NativeMethods.WICKRA_TICK_AGGREGATOR_DRAIN.invokeExact(handle, out, n);
Candle[] result = new Candle[(int) w];
for (int i = 0; i < w; i++) {
long b = (long) i * 48L;
result[i] = new Candle(
out.get(JAVA_DOUBLE, b + 0L),
out.get(JAVA_DOUBLE, b + 8L),
out.get(JAVA_DOUBLE, b + 16L),
out.get(JAVA_DOUBLE, b + 24L),
out.get(JAVA_DOUBLE, b + 32L),
(double) out.get(JAVA_LONG, b + 40L));
}
return result;
}
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
@Override public void close() {
cleanable.clean();
}
}
@@ -3947,6 +3947,10 @@ public final class NativeMethods {
public static MethodHandle WICKRA_FOOTPRINT_NAME;
public static MethodHandle WICKRA_FOOTPRINT_RESET;
public static MethodHandle WICKRA_FOOTPRINT_FREE;
public static MethodHandle WICKRA_TICK_AGGREGATOR_NEW;
public static MethodHandle WICKRA_TICK_AGGREGATOR_PUSH;
public static MethodHandle WICKRA_TICK_AGGREGATOR_DRAIN;
public static MethodHandle WICKRA_TICK_AGGREGATOR_FREE;
static {
init0();
@@ -8015,6 +8019,10 @@ public final class NativeMethods {
WICKRA_FOOTPRINT_NAME = h("wickra_footprint_name", FunctionDescriptor.of(ADDRESS, ADDRESS));
WICKRA_FOOTPRINT_RESET = h("wickra_footprint_reset", FunctionDescriptor.ofVoid(ADDRESS));
WICKRA_FOOTPRINT_FREE = h("wickra_footprint_free", FunctionDescriptor.ofVoid(ADDRESS));
WICKRA_TICK_AGGREGATOR_NEW = h("wickra_tick_aggregator_new", FunctionDescriptor.of(ADDRESS, JAVA_LONG, JAVA_BYTE));
WICKRA_TICK_AGGREGATOR_PUSH = h("wickra_tick_aggregator_push", FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_LONG));
WICKRA_TICK_AGGREGATOR_DRAIN = h("wickra_tick_aggregator_drain", FunctionDescriptor.of(JAVA_LONG, ADDRESS, ADDRESS, JAVA_LONG));
WICKRA_TICK_AGGREGATOR_FREE = h("wickra_tick_aggregator_free", FunctionDescriptor.ofVoid(ADDRESS));
}
}
@@ -0,0 +1,76 @@
package org.wickra;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Cross-language data-layer parity: replay the shared golden tick stream through
* the TickAggregator and check the candles against the Rust reference, with and
* without gap filling.
*/
class DataLayerTest {
private static Path goldenDir() {
java.io.File d = new java.io.File("").getAbsoluteFile();
while (d != null) {
java.io.File g = new java.io.File(d, "testdata/golden");
if (g.isDirectory()) {
return g.toPath();
}
d = d.getParentFile();
}
throw new IllegalStateException("testdata/golden not found");
}
private static double[][] read(String name) throws IOException {
List<String> lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
List<double[]> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
if (lines.get(i).isEmpty()) {
continue;
}
String[] p = lines.get(i).split(",");
double[] r = new double[p.length];
for (int k = 0; k < p.length; k++) {
r[k] = Double.parseDouble(p[k]);
}
rows.add(r);
}
return rows.toArray(new double[0][]);
}
@Test
void tickAggregatorMatchesGolden() throws IOException {
double[][] ticks = read("data_ticks");
String[][] cases = {{"data_candles", "false"}, {"data_candles_gap", "true"}};
for (String[] c : cases) {
boolean gap = Boolean.parseBoolean(c[1]);
try (TickAggregator a = new TickAggregator(1000, gap)) {
List<double[]> got = new ArrayList<>();
for (double[] t : ticks) {
for (Candle k : a.push(t[0], t[1], (long) t[2])) {
got.add(new double[]{
k.open(), k.high(), k.low(), k.close(), k.volume(), (double) k.timestamp()
});
}
}
double[][] want = read(c[0]);
assertEquals(want.length, got.size(), c[0] + " candle count");
for (int i = 0; i < got.size(); i++) {
for (int j = 0; j < 6; j++) {
double w = want[i][j];
assertTrue(Math.abs(got.get(i)[j] - w) <= 1e-9 * Math.max(1, Math.abs(w)),
c[0] + " row " + i + " col " + j + ": " + got.get(i)[j] + " vs " + w);
}
}
}
}
}
}
+1
View File
@@ -25,6 +25,7 @@ workspace = true
[dependencies]
wickra-core = { workspace = true }
wickra-data = { workspace = true }
napi = { version = "2.16", features = ["napi8"] }
napi-derive = "2.16"
@@ -0,0 +1,53 @@
// Cross-language data-layer parity for the Node binding: replay the shared
// golden tick stream through the TickAggregator and check the candles bit-for-bit
// (within fp tolerance) against the Rust-generated fixtures, with and without
// gap filling. Fixtures are produced by `cargo run -p wickra-examples --bin
// gen_golden`.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { TickAggregator } = require('..');
const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden');
function readCsv(name) {
const lines = fs.readFileSync(path.join(GOLDEN, `${name}.csv`), 'utf8').split(/\r?\n/);
lines.shift();
return lines.filter((l) => l.length > 0).map((l) => l.split(',').map(Number));
}
const TICKS = readCsv('data_ticks');
function run(gapFill) {
const agg = new TickAggregator(1000, gapFill);
const out = [];
for (const [price, size, ts] of TICKS) {
for (const c of agg.push(price, size, ts)) {
out.push([c.open, c.high, c.low, c.close, c.volume, c.timestamp]);
}
}
return out;
}
function assertCandles(got, want, label) {
assert.equal(got.length, want.length, `${label}: candle count ${got.length} vs ${want.length}`);
for (let i = 0; i < got.length; i++) {
for (let j = 0; j < 6; j++) {
const tol = 1e-9 * Math.max(1, Math.abs(want[i][j]));
assert.ok(
Math.abs(got[i][j] - want[i][j]) <= tol,
`${label} row ${i} col ${j}: ${got[i][j]} vs ${want[i][j]}`,
);
}
}
}
test('tick aggregator matches the golden candles', () => {
assertCandles(run(false), readCsv('data_candles'), 'no-gap');
});
test('tick aggregator gap-fill matches the golden candles', () => {
assertCandles(run(true), readCsv('data_candles_gap'), 'gap');
});
+27
View File
@@ -594,6 +594,15 @@ export interface VolumeWeightedMacdValue {
signal: number
histogram: number
}
/** One aggregated OHLCV candle emitted by [`TickAggregatorNode`]. */
export interface CandleValue {
open: number
high: number
low: number
close: number
volume: number
timestamp: number
}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -5930,3 +5939,21 @@ export declare class VolumeWeightedMacd {
isReady(): boolean
warmupPeriod(): number
}
export type TickAggregatorNode = TickAggregator
/** Roll trade ticks up into fixed-timeframe OHLCV candles. */
export declare class TickAggregator {
/**
* Construct an aggregator with the given bucket size (in the same unit as
* the tick timestamps). Pass `gapFill = true` to emit a flat placeholder
* candle for every skipped bucket.
*/
constructor(bucket: number, gapFill?: boolean | undefined | null)
/**
* Push one trade tick; returns every candle that closed as a result (none
* while the open bar keeps growing, one on a bucket boundary, plus one flat
* candle per skipped bucket when gap filling is enabled).
*/
push(price: number, size: number, timestamp: number): Array<CandleValue>
/** Whether gap filling is enabled. */
fillsGaps(): boolean
}
File diff suppressed because one or more lines are too long
+72
View File
@@ -21865,3 +21865,75 @@ impl VolumeWeightedMacdNode {
self.inner.warmup_period() as u32
}
}
// ===== Data layer: tick-to-candle aggregation =====
/// Convert a `wickra-data` error into a JS error.
fn map_data_err(e: wickra_data::Error) -> NapiError {
NapiError::new(Status::InvalidArg, e.to_string())
}
/// One aggregated OHLCV candle emitted by [`TickAggregatorNode`].
#[napi(object)]
pub struct CandleValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub timestamp: f64,
}
/// Roll trade ticks up into fixed-timeframe OHLCV candles.
#[napi(js_name = "TickAggregator")]
pub struct TickAggregatorNode {
inner: wickra_data::aggregator::TickAggregator,
}
#[napi]
impl TickAggregatorNode {
/// Construct an aggregator with the given bucket size (in the same unit as
/// the tick timestamps). Pass `gapFill = true` to emit a flat placeholder
/// candle for every skipped bucket.
#[napi(constructor)]
pub fn new(bucket: f64, gap_fill: Option<bool>) -> napi::Result<Self> {
let timeframe =
wickra_data::aggregator::Timeframe::new(bucket as i64).map_err(map_data_err)?;
let mut inner = wickra_data::aggregator::TickAggregator::new(timeframe);
if gap_fill.unwrap_or(false) {
inner = inner.with_gap_fill(true);
}
Ok(Self { inner })
}
/// Push one trade tick; returns every candle that closed as a result (none
/// while the open bar keeps growing, one on a bucket boundary, plus one flat
/// candle per skipped bucket when gap filling is enabled).
#[napi]
pub fn push(
&mut self,
price: f64,
size: f64,
timestamp: f64,
) -> napi::Result<Vec<CandleValue>> {
let tick = wc::Tick::new(price, size, timestamp as i64).map_err(map_err)?;
let candles = self.inner.push(tick).map_err(map_data_err)?;
Ok(candles
.into_iter()
.map(|c| CandleValue {
open: c.open,
high: c.high,
low: c.low,
close: c.close,
volume: c.volume,
timestamp: c.timestamp as f64,
})
.collect())
}
/// Whether gap filling is enabled.
#[napi(js_name = "fillsGaps")]
pub fn fills_gaps(&self) -> bool {
self.inner.fills_gaps()
}
}
+1
View File
@@ -22,5 +22,6 @@ workspace = true
[dependencies]
wickra-core = { workspace = true }
wickra-data = { workspace = true }
pyo3 = { workspace = true }
numpy = { workspace = true }
@@ -358,6 +358,8 @@ from ._wickra import (
ThreeLineBreak,
Equivolume,
CandleVolume,
# Data layer
TickAggregator,
# Market Profile
CompositeProfile,
HighLowVolumeNodes,
@@ -902,6 +904,8 @@ __all__ = [
"ThreeLineBreak",
"Equivolume",
"CandleVolume",
# Data layer
"TickAggregator",
# Market Profile
"CompositeProfile",
"HighLowVolumeNodes",
+59
View File
@@ -28285,6 +28285,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyProfileShape>()?;
m.add_class::<PyHighLowVolumeNodes>()?;
m.add_class::<PyCompositeProfile>()?;
// Data layer.
m.add_class::<PyTickAggregator>()?;
// Candlestick patterns.
m.add_class::<PyDoji>()?;
m.add_class::<PyHammer>()?;
@@ -28557,3 +28559,60 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyM2Measure>()?;
Ok(())
}
// ===== Data layer: tick-to-candle aggregation =====
/// One aggregated candle as `(open, high, low, close, volume, timestamp)`.
type CandleTuple = (f64, f64, f64, f64, f64, i64);
/// Convert a `wickra-data` error into a Python `ValueError`.
fn map_data_err(e: wickra_data::Error) -> PyErr {
PyValueError::new_err(e.to_string())
}
/// Roll trade ticks up into fixed-timeframe OHLCV candles.
#[pyclass(
name = "TickAggregator",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyTickAggregator {
inner: wickra_data::aggregator::TickAggregator,
}
#[pymethods]
impl PyTickAggregator {
#[new]
#[pyo3(signature = (bucket, gap_fill = false))]
fn new(bucket: i64, gap_fill: bool) -> PyResult<Self> {
let timeframe = wickra_data::aggregator::Timeframe::new(bucket).map_err(map_data_err)?;
let mut inner = wickra_data::aggregator::TickAggregator::new(timeframe);
if gap_fill {
inner = inner.with_gap_fill(true);
}
Ok(Self { inner })
}
/// Push one trade tick; returns the candles closed as a result, each a
/// `(open, high, low, close, volume, timestamp)` tuple.
fn push(&mut self, price: f64, size: f64, timestamp: i64) -> PyResult<Vec<CandleTuple>> {
let tick = wc::Tick::new(price, size, timestamp).map_err(map_err)?;
Ok(self
.inner
.push(tick)
.map_err(map_data_err)?
.into_iter()
.map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp))
.collect())
}
#[getter]
fn fills_gaps(&self) -> bool {
self.inner.fills_gaps()
}
fn __repr__(&self) -> String {
format!("TickAggregator(fills_gaps={})", self.inner.fills_gaps())
}
}
+44
View File
@@ -0,0 +1,44 @@
"""Cross-language data-layer parity for the Python binding: replay the shared
golden tick stream through the TickAggregator and check the candles against the
Rust-generated fixtures, with and without gap filling. Fixtures are produced by
``cargo run -p wickra-examples --bin gen_golden``.
"""
import csv
import os
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _read(name):
with open(os.path.join(GOLDEN, name + ".csv"), newline="") as f:
rows = list(csv.reader(f))
return [[float(x) for x in r] for r in rows[1:] if r]
TICKS = _read("data_ticks")
def _run(gap_fill):
agg = ta.TickAggregator(1000, gap_fill=gap_fill)
out = []
for price, size, ts in TICKS:
out.extend(agg.push(price, size, int(ts)))
return out
@pytest.mark.parametrize(
"gap_fill,fixture",
[(False, "data_candles"), (True, "data_candles_gap")],
)
def test_tick_aggregator_matches_golden(gap_fill, fixture):
got = _run(gap_fill)
want = _read(fixture)
assert len(got) == len(want)
for i, (g, w) in enumerate(zip(got, want)):
for j in range(6):
tol = 1e-9 * max(1.0, abs(w[j]))
assert abs(g[j] - w[j]) <= tol, f"row {i} col {j}: {g[j]} vs {w[j]}"
+3
View File
@@ -3,6 +3,7 @@
S3method(batch,wickra_indicator)
S3method(is_ready,wickra_indicator)
S3method(name,wickra_indicator)
S3method(push,wickra_indicator)
S3method(reset,wickra_indicator)
S3method(update,wickra_indicator)
S3method(warmup_period,wickra_indicator)
@@ -439,6 +440,7 @@ export(ThreeOutside)
export(ThreeSoldiersOrCrows)
export(ThreeStarsInSouth)
export(Thrusting)
export(TickAggregator)
export(TickBars)
export(TickIndex)
export(Tii)
@@ -523,6 +525,7 @@ export(Zlema)
export(batch)
export(is_ready)
export(name)
export(push)
export(reset)
export(warmup_period)
importFrom(stats,update)
+8
View File
@@ -3471,6 +3471,14 @@ Thrusting <- function() {
.wk_obj("thrusting", ptr, "Thrusting")
}
#' TickAggregator indicator
#' @keywords internal
#' @export
TickAggregator <- function(bucket, gap_fill) {
ptr <- .Call("wk_tick_aggregator_new", bucket, gap_fill, PACKAGE = "wickra")
.wk_obj("tick_aggregator", ptr, "TickAggregator")
}
#' TickBars indicator
#' @keywords internal
#' @export
+32
View File
@@ -138,3 +138,35 @@ name <- function(object) {
name.wickra_indicator <- function(object) {
.Call(paste0("wk_", object$prefix, "_name"), object$ptr, PACKAGE = "wickra")
}
#' Push a trade tick into a tick aggregator
#'
#' Feeds one trade tick to a [TickAggregator()] and returns the candles it
#' closed as a numeric matrix with columns `open`, `high`, `low`, `close`,
#' `volume`, `timestamp` (zero rows while the open bar merely grows).
#'
#' @param object A `wickra_indicator` created by [TickAggregator()].
#' @param price Trade price.
#' @param size Trade size (volume).
#' @param timestamp Trade timestamp, in the same unit as the aggregator bucket.
#' @return A numeric matrix with six named columns (possibly zero rows).
#' @examples
#' agg <- TickAggregator(1000)
#' push(agg, 100, 1, 0)
#' push(agg, 102, 1, 1000) # closes the first bucket
#' @export
push <- function(object, price, size, timestamp) {
UseMethod("push")
}
#' @rdname push
#' @export
push.wickra_indicator <- function(object, price, size, timestamp) {
out <- .Call(
paste0("wk_", object$prefix, "_push"),
object$ptr, price, size, timestamp,
PACKAGE = "wickra"
)
colnames(out) <- c("open", "high", "low", "close", "volume", "timestamp")
out
}
+34
View File
@@ -18965,6 +18965,38 @@ SEXP wk_thrusting_reset(SEXP e) {
return R_NilValue;
}
static void tick_aggregator_fin(SEXP e) {
struct TickAggregator *h = (struct TickAggregator *)R_ExternalPtrAddr(e);
if (h) wickra_tick_aggregator_free(h);
R_ClearExternalPtr(e);
}
SEXP wk_tick_aggregator_new(SEXP a0, SEXP a1) {
struct TickAggregator *h = wickra_tick_aggregator_new((int64_t)Rf_asReal(a0), (bool)(Rf_asLogical(a1) == TRUE));
if (!h) Rf_error("invalid TickAggregator parameters");
SEXP e = PROTECT(R_MakeExternalPtr(h, R_NilValue, R_NilValue));
R_RegisterCFinalizerEx(e, tick_aggregator_fin, TRUE);
UNPROTECT(1);
return e;
}
SEXP wk_tick_aggregator_push(SEXP e, SEXP price, SEXP size, SEXP ts) {
struct TickAggregator *h = (struct TickAggregator *)R_ExternalPtrAddr(e);
intptr_t n = wickra_tick_aggregator_push(h, Rf_asReal(price), Rf_asReal(size), (int64_t)Rf_asReal(ts));
if (n <= 0) return Rf_allocMatrix(REALSXP, 0, 6);
struct WickraCandle *buf = (struct WickraCandle *)R_alloc(n, sizeof(struct WickraCandle));
intptr_t w = wickra_tick_aggregator_drain(h, buf, (uintptr_t)n);
SEXP r = PROTECT(Rf_allocMatrix(REALSXP, (int)w, 6));
for (intptr_t i = 0; i < w; i++) {
REAL(r)[i + w * 0] = buf[i].open;
REAL(r)[i + w * 1] = buf[i].high;
REAL(r)[i + w * 2] = buf[i].low;
REAL(r)[i + w * 3] = buf[i].close;
REAL(r)[i + w * 4] = buf[i].volume;
REAL(r)[i + w * 5] = (double)buf[i].timestamp;
}
UNPROTECT(1);
return r;
}
static void tick_bars_fin(SEXP e) {
struct TickBars *h = (struct TickBars *)R_ExternalPtrAddr(e);
if (h) wickra_tick_bars_free(h);
@@ -25418,6 +25450,8 @@ static const R_CallMethodDef CallEntries[] = {
{"wk_thrusting_is_ready", (DL_FUNC)&wk_thrusting_is_ready, 1},
{"wk_thrusting_name", (DL_FUNC)&wk_thrusting_name, 1},
{"wk_thrusting_reset", (DL_FUNC)&wk_thrusting_reset, 1},
{"wk_tick_aggregator_new", (DL_FUNC)&wk_tick_aggregator_new, 2},
{"wk_tick_aggregator_push", (DL_FUNC)&wk_tick_aggregator_push, 4},
{"wk_tick_bars_new", (DL_FUNC)&wk_tick_bars_new, 1},
{"wk_tick_bars_update", (DL_FUNC)&wk_tick_bars_update, 7},
{"wk_tick_bars_name", (DL_FUNC)&wk_tick_bars_name, 1},
@@ -0,0 +1,52 @@
# Cross-language data-layer parity: replay the shared golden tick stream through
# the TickAggregator and check the candles against the Rust reference, with and
# without gap filling. Fixtures are generated by
# `cargo run -p wickra-examples --bin gen_golden`.
find_data_golden_dir <- function() {
d <- normalizePath(".", mustWork = FALSE)
repeat {
g <- file.path(d, "testdata", "golden")
if (dir.exists(g)) {
return(g)
}
parent <- dirname(d)
if (identical(parent, d)) {
return(NULL)
}
d <- parent
}
}
test_that("tick aggregator matches the golden candles", {
gdir <- find_data_golden_dir()
skip_if(is.null(gdir), "golden fixtures not bundled with the package")
read_mat <- function(name) {
lines <- readLines(file.path(gdir, paste0(name, ".csv")))[-1]
lines <- lines[nzchar(lines)]
if (length(lines) == 0) {
return(matrix(numeric(0), 0, 6))
}
do.call(rbind, lapply(lines, function(l) as.numeric(strsplit(l, ",")[[1]])))
}
ticks <- read_mat("data_ticks")
specs <- list(
list(gap = FALSE, fixture = "data_candles"),
list(gap = TRUE, fixture = "data_candles_gap")
)
for (spec in specs) {
agg <- TickAggregator(1000, spec$gap)
got <- matrix(numeric(0), 0, 6)
for (i in seq_len(nrow(ticks))) {
out <- push(agg, ticks[i, 1], ticks[i, 2], ticks[i, 3])
if (nrow(out) > 0) {
got <- rbind(got, unname(out))
}
}
want <- unname(read_mat(spec$fixture))
expect_equal(nrow(got), nrow(want), info = spec$fixture)
expect_equal(got, want, tolerance = 1e-9, info = spec$fixture)
}
})
+1
View File
@@ -24,6 +24,7 @@ workspace = true
# depend on the local path directly with default features disabled. This also
# strips the parallel batch helpers we don't ship to JavaScript.
wickra-core = { path = "../../crates/wickra-core", default-features = false }
wickra-data = { path = "../../crates/wickra-data" }
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
+54
View File
@@ -15989,3 +15989,57 @@ impl Default for WasmIntradayIntensity {
Self::new()
}
}
// ===== Data layer: tick-to-candle aggregation =====
/// Convert a `wickra-data` error into a JS error.
fn map_data_err(e: wickra_data::Error) -> JsError {
JsError::new(&e.to_string())
}
/// Roll trade ticks up into fixed-timeframe OHLCV candles.
#[wasm_bindgen(js_name = TickAggregator)]
pub struct WasmTickAggregator {
inner: wickra_data::aggregator::TickAggregator,
}
#[wasm_bindgen(js_class = TickAggregator)]
impl WasmTickAggregator {
/// Construct an aggregator with the given bucket size (same unit as the tick
/// timestamps). Pass `gapFill = true` to emit a flat placeholder candle for
/// every skipped bucket.
#[wasm_bindgen(constructor)]
pub fn new(bucket: f64, gap_fill: Option<bool>) -> Result<WasmTickAggregator, JsError> {
let timeframe =
wickra_data::aggregator::Timeframe::new(bucket as i64).map_err(map_data_err)?;
let mut inner = wickra_data::aggregator::TickAggregator::new(timeframe);
if gap_fill.unwrap_or(false) {
inner = inner.with_gap_fill(true);
}
Ok(Self { inner })
}
/// Push one trade tick; returns an array of `{ open, high, low, close,
/// volume, timestamp }` candles closed as a result.
pub fn push(&mut self, price: f64, size: f64, timestamp: f64) -> Result<Array, JsError> {
let tick = wc::Tick::new(price, size, timestamp as i64).map_err(map_err)?;
let arr = Array::new();
for c in self.inner.push(tick).map_err(map_data_err)? {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &c.open.into()).ok();
Reflect::set(&obj, &"high".into(), &c.high.into()).ok();
Reflect::set(&obj, &"low".into(), &c.low.into()).ok();
Reflect::set(&obj, &"close".into(), &c.close.into()).ok();
Reflect::set(&obj, &"volume".into(), &c.volume.into()).ok();
Reflect::set(&obj, &"timestamp".into(), &(c.timestamp as f64).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
/// Whether gap filling is enabled.
#[wasm_bindgen(js_name = fillsGaps)]
pub fn fills_gaps(&self) -> bool {
self.inner.fills_gaps()
}
}
+51
View File
@@ -0,0 +1,51 @@
// Cross-language data-layer parity for the WASM binding: replay the shared
// golden tick stream through TickAggregator and check the candles against the
// Rust-generated fixtures, with and without gap filling.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const W = require('../pkg/wickra_wasm.js');
const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden');
function readCsv(name) {
const lines = fs.readFileSync(path.join(GOLDEN, `${name}.csv`), 'utf8').split(/\r?\n/);
lines.shift();
return lines.filter((l) => l.length > 0).map((l) => l.split(',').map(Number));
}
const TICKS = readCsv('data_ticks');
function run(gapFill) {
const agg = new W.TickAggregator(1000, gapFill);
const out = [];
for (const [price, size, ts] of TICKS) {
for (const c of agg.push(price, size, ts)) {
out.push([c.open, c.high, c.low, c.close, c.volume, c.timestamp]);
}
}
return out;
}
function assertCandles(got, want, label) {
assert.equal(got.length, want.length, `${label}: candle count ${got.length} vs ${want.length}`);
for (let i = 0; i < got.length; i++) {
for (let j = 0; j < 6; j++) {
const tol = 1e-9 * Math.max(1, Math.abs(want[i][j]));
assert.ok(
Math.abs(got[i][j] - want[i][j]) <= tol,
`${label} row ${i} col ${j}: ${got[i][j]} vs ${want[i][j]}`,
);
}
}
}
test('wasm tick aggregator matches the golden candles', () => {
assertCandles(run(false), readCsv('data_candles'), 'no-gap');
});
test('wasm tick aggregator gap-fill matches the golden candles', () => {
assertCandles(run(true), readCsv('data_candles_gap'), 'gap');
});
+6 -1
View File
@@ -22,7 +22,12 @@ all-features = true
workspace = true
[dependencies]
wickra-core = { workspace = true }
# Direct path+version (not the workspace inheritance) so `default-features = false`
# is honoured: depending on wickra-data must NOT force wickra-core's `parallel`
# (rayon) feature on — the WASM binding needs a rayon-free build and the data
# layer never uses the parallel batch path. Native bindings re-enable `parallel`
# through their own wickra-core dependency (cargo unifies the features).
wickra-core = { path = "../wickra-core", version = "0.9.2", default-features = false }
thiserror = { workspace = true }
csv = "1.3"
serde = { version = "1", features = ["derive"] }
+2 -1
View File
@@ -83,7 +83,8 @@ endif()
# prove the C ABI header is consumable from each language. The fixtures live at
# the repo root; pass the absolute path as the test argument.
get_filename_component(WICKRA_GOLDEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../testdata/golden" ABSOLUTE)
foreach(gt_pair "golden_test:golden_test.c" "golden_test_cpp:golden_test.cpp")
foreach(gt_pair "golden_test:golden_test.c" "golden_test_cpp:golden_test.cpp"
"data_layer_test:data_layer_test.c" "data_layer_test_cpp:data_layer_test.cpp")
string(REPLACE ":" ";" gt_list "${gt_pair}")
list(GET gt_list 0 gt_name)
list(GET gt_list 1 gt_src)
+2 -2
View File
@@ -34,7 +34,7 @@ static double last_finite(const double *v, size_t n) {
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
WickraBar *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "backtest: no candles read from %s\n", path);
@@ -77,7 +77,7 @@ int main(int argc, char **argv) {
double last_atr = NAN;
double last_obv = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
const WickraBar *c = &candles[i];
WickraBollingerOutput bo;
if (wickra_bollinger_bands_update(bb, c->close, &bo)) {
last_bb = bo;
+107
View File
@@ -0,0 +1,107 @@
/* Cross-language data-layer parity for the C / C++ binding: replay the shared
* golden tick stream through the TickAggregator (push/drain) and check the
* candles against the Rust reference, with and without gap filling. The same
* source compiles under both a C and a C++ compiler. Fixtures are generated by
* `cargo run -p wickra-examples --bin gen_golden`; the golden dir is argv[1].
*/
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "wickra.h"
#define MAXROWS 256
static const char *GDIR;
static int read_csv(const char *name, double *rows, int ncol) {
char path[1024];
snprintf(path, sizeof(path), "%s/%s.csv", GDIR, name);
FILE *f = fopen(path, "r");
if (!f) {
fprintf(stderr, "cannot open %s\n", path);
exit(2);
}
char line[4096];
int n = 0, first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = '\0';
if (first) {
first = 0;
continue;
}
if (line[0] == '\0') {
continue;
}
char *p = line;
for (int j = 0; j < ncol; j++) {
rows[n * ncol + j] = strtod(p, &p);
if (*p == ',') {
p++;
}
}
n++;
}
fclose(f);
return n;
}
static int check(const char *fixture, bool gap, const double *ticks, int nt) {
struct TickAggregator *a = wickra_tick_aggregator_new(1000, gap);
if (!a) {
printf("FAIL %s: new returned NULL\n", fixture);
return 1;
}
double want[MAXROWS * 6];
int nw = read_csv(fixture, want, 6);
double got[MAXROWS * 6];
int ng = 0;
for (int i = 0; i < nt; i++) {
intptr_t n = wickra_tick_aggregator_push(a, ticks[i * 3 + 0], ticks[i * 3 + 1],
(int64_t)ticks[i * 3 + 2]);
if (n > 0) {
struct WickraCandle buf[MAXROWS];
intptr_t w = wickra_tick_aggregator_drain(a, buf, (uintptr_t)n);
for (intptr_t j = 0; j < w; j++) {
got[ng * 6 + 0] = buf[j].open;
got[ng * 6 + 1] = buf[j].high;
got[ng * 6 + 2] = buf[j].low;
got[ng * 6 + 3] = buf[j].close;
got[ng * 6 + 4] = buf[j].volume;
got[ng * 6 + 5] = (double)buf[j].timestamp;
ng++;
}
}
}
wickra_tick_aggregator_free(a);
if (ng != nw) {
printf("FAIL %s: %d candles vs %d\n", fixture, ng, nw);
return 1;
}
int fails = 0;
for (int i = 0; i < ng; i++) {
for (int j = 0; j < 6; j++) {
double w = want[i * 6 + j];
double tol = 1e-9 * fmax(1.0, fabs(w));
if (fabs(got[i * 6 + j] - w) > tol) {
printf("FAIL %s row %d col %d: %g vs %g\n", fixture, i, j, got[i * 6 + j], w);
fails++;
}
}
}
return fails;
}
int main(int argc, char **argv) {
GDIR = (argc > 1) ? argv[1] : "testdata/golden";
double ticks[MAXROWS * 3];
int nt = read_csv("data_ticks", ticks, 3);
int fails = check("data_candles", false, ticks, nt);
fails += check("data_candles_gap", true, ticks, nt);
if (fails == 0) {
printf("C/C++ data layer: OK (%d ticks)\n", nt);
}
return fails ? 1 : 0;
}
+3
View File
@@ -0,0 +1,3 @@
/* C++ build of the data-layer parity test: the C ABI header is `extern "C"`, so
* the exact same source compiles under a C++ compiler too. */
#include "data_layer_test.c"
+7 -7
View File
@@ -30,12 +30,12 @@
/* Aggregate `in` into fixed-width time buckets of `tf_ms`. Writes the bucketed
* candles into the caller-owned `out` (capacity >= count) and returns how many
* buckets were produced. */
static size_t resample(const WickraCandle *in, size_t count, int64_t tf_ms,
WickraCandle *out) {
static size_t resample(const WickraBar *in, size_t count, int64_t tf_ms,
WickraBar *out) {
size_t produced = 0;
int open_bucket = 0;
int64_t bucket_start = 0;
WickraCandle cur = {0};
WickraBar cur = {0};
for (size_t i = 0; i < count; ++i) {
int64_t b = in[i].timestamp - (in[i].timestamp % tf_ms);
if (!open_bucket || b != bucket_start) {
@@ -63,7 +63,7 @@ static size_t resample(const WickraCandle *in, size_t count, int64_t tf_ms,
return produced;
}
static void summarize(const char *label, const WickraCandle *candles, size_t n) {
static void summarize(const char *label, const WickraBar *candles, size_t n) {
if (n == 0) {
printf(" %-5s (empty)\n", label);
return;
@@ -76,7 +76,7 @@ static void summarize(const char *label, const WickraCandle *candles, size_t n)
double last_hist = NAN;
double last_adx = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
const WickraBar *c = &candles[i];
double r = wickra_rsi_update(rsi, c->close);
if (isfinite(r)) {
last_rsi = r;
@@ -119,14 +119,14 @@ static void summarize(const char *label, const WickraCandle *candles, size_t n)
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1m.csv";
WickraCandle *ones = NULL;
WickraBar *ones = NULL;
size_t n = wickra_load_csv(path, &ones);
if (n == 0) {
fprintf(stderr, "multi_timeframe: no candles read from %s\n", path);
return 1;
}
/* Resampling only ever produces fewer candles than the 1m source. */
WickraCandle *buf = (WickraCandle *)malloc(n * sizeof(*buf));
WickraBar *buf = (WickraBar *)malloc(n * sizeof(*buf));
if (buf == NULL) {
fprintf(stderr, "multi_timeframe: allocation failed\n");
free(ones);
+2 -2
View File
@@ -37,7 +37,7 @@
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
WickraBar *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < SQUEEZE_LOOKBACK + BB_PERIOD) {
fprintf(stderr, "dataset has only %llu bars; need at least %d\n",
@@ -64,7 +64,7 @@ int main(int argc, char **argv) {
double equity = 1.0;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
const WickraBar *c = &candles[i];
double price = c->close;
WickraBollingerOutput b;
int bb_ready = wickra_bollinger_bands_update(bb, price, &b);
+2 -2
View File
@@ -34,7 +34,7 @@
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
WickraBar *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "CSV is empty: %s\n", path);
@@ -58,7 +58,7 @@ int main(int argc, char **argv) {
int prev_hist_sign = -1;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
const WickraBar *c = &candles[i];
double price = c->close;
WickraMacdOutput m;
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
+1 -1
View File
@@ -34,7 +34,7 @@
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
WickraBar *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < RSI_PERIOD * 4) {
fprintf(stderr, "dataset too small: %llu\n", (unsigned long long)n);
+7 -7
View File
@@ -15,27 +15,27 @@
#include <stddef.h>
#include <stdint.h>
typedef struct WickraCandle {
typedef struct WickraBar {
int64_t timestamp;
double open;
double high;
double low;
double close;
double volume;
} WickraCandle;
} WickraBar;
/* Load an OHLCV CSV into a malloc'd array. Returns the candle count and stores
* the array in *out (caller frees with free()). Returns 0 and leaves *out NULL
* on any error (missing file, no parseable rows). A leading header line whose
* first field is non-numeric is skipped. */
size_t wickra_load_csv(const char *path, WickraCandle **out);
size_t wickra_load_csv(const char *path, WickraBar **out);
#ifdef WICKRA_CSV_IMPL
#include <stdio.h>
#include <stdlib.h>
size_t wickra_load_csv(const char *path, WickraCandle **out) {
size_t wickra_load_csv(const char *path, WickraBar **out) {
*out = NULL;
FILE *f = fopen(path, "r");
if (f == NULL) {
@@ -45,7 +45,7 @@ size_t wickra_load_csv(const char *path, WickraCandle **out) {
size_t cap = 1024;
size_t n = 0;
WickraCandle *rows = (WickraCandle *)malloc(cap * sizeof(*rows));
WickraBar *rows = (WickraBar *)malloc(cap * sizeof(*rows));
if (rows == NULL) {
fclose(f);
return 0;
@@ -53,7 +53,7 @@ size_t wickra_load_csv(const char *path, WickraCandle **out) {
char line[512];
while (fgets(line, (int)sizeof(line), f) != NULL) {
WickraCandle c;
WickraBar c;
long long ts = 0;
/* sscanf returns the number of fields successfully matched. A header
* row ("timestamp,...") matches 0 and is skipped. */
@@ -66,7 +66,7 @@ size_t wickra_load_csv(const char *path, WickraCandle **out) {
if (n == cap) {
cap *= 2;
WickraCandle *grown = (WickraCandle *)realloc(rows, cap * sizeof(*rows));
WickraBar *grown = (WickraBar *)realloc(rows, cap * sizeof(*rows));
if (grown == NULL) {
free(rows);
fclose(f);
+45 -1
View File
@@ -17,8 +17,9 @@ use std::path::Path;
use wickra::{
AdOscillator, Adx, Atr, AverageDrawdown, AwesomeOscillatorHistogram, Beta, Candle, Ema,
Indicator, IntradayIntensity, MacdIndicator, Rsi, Sma,
Indicator, IntradayIntensity, MacdIndicator, Rsi, Sma, Tick,
};
use wickra_data::aggregator::{TickAggregator, Timeframe};
const N: usize = 80;
@@ -183,9 +184,52 @@ fn main() {
emit_special(dir, &candles);
emit_profiles(dir, &candles);
emit_bars(dir, &candles);
emit_data_layer(dir);
println!("golden fixtures written to {}", dir.display());
}
/// Deterministic trade tick `i`: price on the shared varied path, a small
/// repeating size, and a timestamp that places roughly three ticks per
/// 1000-unit bucket. A deliberate jump at `i == 36` opens a multi-bucket gap so
/// the gap-fill fixture exercises several flat candles emitted from one push.
fn tick(i: usize) -> (f64, f64, i64) {
let t = i as f64;
let price = 100.0 + 10.0 * (t * 0.3).sin() + 0.5 * t;
let size = 1.0 + (i % 5) as f64;
let base = i64::try_from(i).expect("tick index fits i64") * 350;
let ts = if i >= 36 { base + 5000 } else { base };
(price, size, ts)
}
/// Data layer: the tick-to-candle aggregator. Writes the shared tick input plus
/// the reference candle streams with and without gap filling.
fn emit_data_layer(dir: &Path) {
const N_TICKS: usize = 60;
let ticks: Vec<(f64, f64, i64)> = (0..N_TICKS).map(tick).collect();
let mut tin = Vec::with_capacity(N_TICKS);
for &(price, size, ts) in &ticks {
tin.push(format!("{price},{size},{ts}"));
}
write_csv(dir, "data_ticks", "price,size,timestamp", &tin);
let header = "open,high,low,close,volume,timestamp";
for (name, gap_fill) in [("data_candles", false), ("data_candles_gap", true)] {
let mut agg = TickAggregator::new(Timeframe::new(1000).unwrap()).with_gap_fill(gap_fill);
let mut rows = Vec::new();
for &(price, size, ts) in &ticks {
let tick = Tick::new(price, size, ts).expect("valid tick");
for c in agg.push(tick).expect("valid push") {
rows.push(format!(
"{},{},{},{},{},{}",
c.open, c.high, c.low, c.close, c.volume, c.timestamp
));
}
}
write_csv(dir, name, header, &rows);
}
}
// AUTO-GENERATED scalar-output golden tranche (single f64 output).
#[allow(clippy::too_many_lines)]
fn emit_scalar(dir: &Path, candles: &[Candle], closes: &[f64]) {
+22
View File
@@ -0,0 +1,22 @@
open,high,low,close,volume,timestamp
100,106.64642473395035,100,106.64642473395035,6,0
109.33326909627483,112.47494986604055,109.33326909627483,112.47494986604055,10,1000
112.73847630878196,112.73847630878196,110.75463180551151,110.75463180551151,9,2000
108.7737988023383,108.7737988023383,103.92254305856751,103.92254305856751,8,3000
101.57479556705148,101.57479556705148,98.28424227586412,98.28424227586412,12,4000
97.72469882334903,99.24185317672267,97.72469882334903,99.24185317672267,6,5000
101.27235512444012,103.99314457402363,101.27235512444012,103.99314457402363,9,6000
107.20584501801073,114.11541363513378,107.20584501801073,114.11541363513378,6,7000
117.28439764388199,121.87999976774739,117.28439764388199,121.87999976774739,10,8000
122.98543345374605,123.19889810845086,122.5459890808828,122.5459890808828,9,9000
121.12969230082183,121.12969230082183,116.74454423507063,116.74454423507063,8,10000
114.2567321877702,114.2567321877702,110.00125312406458,110.00125312406458,12,11000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,1,12000
108.19063769933508,108.55447411796011,108.19063769933508,108.55447411796011,5,17000
109.80671474335324,111.88016416080967,109.80671474335324,111.88016416080967,9,18000
114.63427081999565,121.33623047221137,114.63427081999565,121.33623047221137,6,19000
124.77474439137693,130.5378442655162,124.77474439137693,130.5378442655162,10,20000
132.43695669444105,133.65657776549278,132.43695669444105,133.65657776549278,9,21000
132.95746831142935,132.95746831142935,129.46740573130614,129.46740573130614,8,22000
127.07753652299444,127.07753652299444,122.27578013601534,122.27578013601534,12,23000
120.38214657630877,120.38214657630877,118.65934994918358,118.65934994918358,6,24000
1 open high low close volume timestamp
2 100 106.64642473395035 100 106.64642473395035 6 0
3 109.33326909627483 112.47494986604055 109.33326909627483 112.47494986604055 10 1000
4 112.73847630878196 112.73847630878196 110.75463180551151 110.75463180551151 9 2000
5 108.7737988023383 108.7737988023383 103.92254305856751 103.92254305856751 8 3000
6 101.57479556705148 101.57479556705148 98.28424227586412 98.28424227586412 12 4000
7 97.72469882334903 99.24185317672267 97.72469882334903 99.24185317672267 6 5000
8 101.27235512444012 103.99314457402363 101.27235512444012 103.99314457402363 9 6000
9 107.20584501801073 114.11541363513378 107.20584501801073 114.11541363513378 6 7000
10 117.28439764388199 121.87999976774739 117.28439764388199 121.87999976774739 10 8000
11 122.98543345374605 123.19889810845086 122.5459890808828 122.5459890808828 9 9000
12 121.12969230082183 121.12969230082183 116.74454423507063 116.74454423507063 8 10000
13 114.2567321877702 114.2567321877702 110.00125312406458 110.00125312406458 12 11000
14 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 1 12000
15 108.19063769933508 108.55447411796011 108.19063769933508 108.55447411796011 5 17000
16 109.80671474335324 111.88016416080967 109.80671474335324 111.88016416080967 9 18000
17 114.63427081999565 121.33623047221137 114.63427081999565 121.33623047221137 6 19000
18 124.77474439137693 130.5378442655162 124.77474439137693 130.5378442655162 10 20000
19 132.43695669444105 133.65657776549278 132.43695669444105 133.65657776549278 9 21000
20 132.95746831142935 132.95746831142935 129.46740573130614 129.46740573130614 8 22000
21 127.07753652299444 127.07753652299444 122.27578013601534 122.27578013601534 12 23000
22 120.38214657630877 120.38214657630877 118.65934994918358 118.65934994918358 6 24000
+26
View File
@@ -0,0 +1,26 @@
open,high,low,close,volume,timestamp
100,106.64642473395035,100,106.64642473395035,6,0
109.33326909627483,112.47494986604055,109.33326909627483,112.47494986604055,10,1000
112.73847630878196,112.73847630878196,110.75463180551151,110.75463180551151,9,2000
108.7737988023383,108.7737988023383,103.92254305856751,103.92254305856751,8,3000
101.57479556705148,101.57479556705148,98.28424227586412,98.28424227586412,12,4000
97.72469882334903,99.24185317672267,97.72469882334903,99.24185317672267,6,5000
101.27235512444012,103.99314457402363,101.27235512444012,103.99314457402363,9,6000
107.20584501801073,114.11541363513378,107.20584501801073,114.11541363513378,6,7000
117.28439764388199,121.87999976774739,117.28439764388199,121.87999976774739,10,8000
122.98543345374605,123.19889810845086,122.5459890808828,122.5459890808828,9,9000
121.12969230082183,121.12969230082183,116.74454423507063,116.74454423507063,8,10000
114.2567321877702,114.2567321877702,110.00125312406458,110.00125312406458,12,11000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,1,12000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,0,13000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,0,14000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,0,15000
108.7030424002833,108.7030424002833,108.7030424002833,108.7030424002833,0,16000
108.19063769933508,108.55447411796011,108.19063769933508,108.55447411796011,5,17000
109.80671474335324,111.88016416080967,109.80671474335324,111.88016416080967,9,18000
114.63427081999565,121.33623047221137,114.63427081999565,121.33623047221137,6,19000
124.77474439137693,130.5378442655162,124.77474439137693,130.5378442655162,10,20000
132.43695669444105,133.65657776549278,132.43695669444105,133.65657776549278,9,21000
132.95746831142935,132.95746831142935,129.46740573130614,129.46740573130614,8,22000
127.07753652299444,127.07753652299444,122.27578013601534,122.27578013601534,12,23000
120.38214657630877,120.38214657630877,118.65934994918358,118.65934994918358,6,24000
1 open high low close volume timestamp
2 100 106.64642473395035 100 106.64642473395035 6 0
3 109.33326909627483 112.47494986604055 109.33326909627483 112.47494986604055 10 1000
4 112.73847630878196 112.73847630878196 110.75463180551151 110.75463180551151 9 2000
5 108.7737988023383 108.7737988023383 103.92254305856751 103.92254305856751 8 3000
6 101.57479556705148 101.57479556705148 98.28424227586412 98.28424227586412 12 4000
7 97.72469882334903 99.24185317672267 97.72469882334903 99.24185317672267 6 5000
8 101.27235512444012 103.99314457402363 101.27235512444012 103.99314457402363 9 6000
9 107.20584501801073 114.11541363513378 107.20584501801073 114.11541363513378 6 7000
10 117.28439764388199 121.87999976774739 117.28439764388199 121.87999976774739 10 8000
11 122.98543345374605 123.19889810845086 122.5459890808828 122.5459890808828 9 9000
12 121.12969230082183 121.12969230082183 116.74454423507063 116.74454423507063 8 10000
13 114.2567321877702 114.2567321877702 110.00125312406458 110.00125312406458 12 11000
14 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 1 12000
15 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 0 13000
16 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 0 14000
17 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 0 15000
18 108.7030424002833 108.7030424002833 108.7030424002833 108.7030424002833 0 16000
19 108.19063769933508 108.55447411796011 108.19063769933508 108.55447411796011 5 17000
20 109.80671474335324 111.88016416080967 109.80671474335324 111.88016416080967 9 18000
21 114.63427081999565 121.33623047221137 114.63427081999565 121.33623047221137 6 19000
22 124.77474439137693 130.5378442655162 124.77474439137693 130.5378442655162 10 20000
23 132.43695669444105 133.65657776549278 132.43695669444105 133.65657776549278 9 21000
24 132.95746831142935 132.95746831142935 129.46740573130614 129.46740573130614 8 22000
25 127.07753652299444 127.07753652299444 122.27578013601534 122.27578013601534 12 23000
26 120.38214657630877 120.38214657630877 118.65934994918358 118.65934994918358 6 24000
+61
View File
@@ -0,0 +1,61 @@
price,size,timestamp
100,1,0
103.4552020666134,2,350
106.64642473395035,3,700
109.33326909627483,4,1050
111.32039085967226,5,1400
112.47494986604055,1,1750
112.73847630878196,2,2100
112.13209366648874,3,2450
110.75463180551151,4,2800
108.7737988023383,5,3150
106.41120008059868,1,3500
103.92254305856751,2,3850
101.57479556705148,3,4200
99.62233840816026,4,4550
98.28424227586412,5,4900
97.72469882334903,1,5250
98.0383539116416,2,5600
99.24185317672267,3,5950
101.27235512444012,4,6300
103.99314457402363,5,6650
107.20584501801073,1,7000
110.6681390048435,2,7350
114.11541363513378,3,7700
117.28439764388199,4,8050
119.93667863849153,5,8400
121.87999976774739,1,8750
122.98543345374605,2,9100
123.19889810845086,3,9450
122.5459890808828,4,9800
121.12969230082183,5,10150
119.12118485241757,1,10500
116.74454423507063,2,10850
114.2567321877702,3,11200
111.92464106224679,4,11550
110.00125312406458,5,11900
108.7030424002833,1,12250
108.19063769933508,2,17600
108.55447411796011,3,17950
109.80671474335324,4,18300
111.88016416080967,5,18650
114.63427081999565,1,19000
117.86768208634197,2,19350
121.33623047221137,3,19700
124.77474439137693,4,20050
127.92073514707224,5,20400
130.5378442655162,1,20750
132.43695669444105,2,21100
133.49309388747918,3,21450
133.65657776549278,4,21800
132.95746831142935,5,22150
131.50287840157117,1,22500
129.46740573130614,2,22850
127.07753652299444,3,23200
124.59141418625812,4,23550
122.27578013601534,5,23900
120.38214657630877,1,24250
119.12432966418496,2,24600
118.65934994918358,3,24950
119.07340619529367,4,25300
120.37417550208815,5,25650
1 price size timestamp
2 100 1 0
3 103.4552020666134 2 350
4 106.64642473395035 3 700
5 109.33326909627483 4 1050
6 111.32039085967226 5 1400
7 112.47494986604055 1 1750
8 112.73847630878196 2 2100
9 112.13209366648874 3 2450
10 110.75463180551151 4 2800
11 108.7737988023383 5 3150
12 106.41120008059868 1 3500
13 103.92254305856751 2 3850
14 101.57479556705148 3 4200
15 99.62233840816026 4 4550
16 98.28424227586412 5 4900
17 97.72469882334903 1 5250
18 98.0383539116416 2 5600
19 99.24185317672267 3 5950
20 101.27235512444012 4 6300
21 103.99314457402363 5 6650
22 107.20584501801073 1 7000
23 110.6681390048435 2 7350
24 114.11541363513378 3 7700
25 117.28439764388199 4 8050
26 119.93667863849153 5 8400
27 121.87999976774739 1 8750
28 122.98543345374605 2 9100
29 123.19889810845086 3 9450
30 122.5459890808828 4 9800
31 121.12969230082183 5 10150
32 119.12118485241757 1 10500
33 116.74454423507063 2 10850
34 114.2567321877702 3 11200
35 111.92464106224679 4 11550
36 110.00125312406458 5 11900
37 108.7030424002833 1 12250
38 108.19063769933508 2 17600
39 108.55447411796011 3 17950
40 109.80671474335324 4 18300
41 111.88016416080967 5 18650
42 114.63427081999565 1 19000
43 117.86768208634197 2 19350
44 121.33623047221137 3 19700
45 124.77474439137693 4 20050
46 127.92073514707224 5 20400
47 130.5378442655162 1 20750
48 132.43695669444105 2 21100
49 133.49309388747918 3 21450
50 133.65657776549278 4 21800
51 132.95746831142935 5 22150
52 131.50287840157117 1 22500
53 129.46740573130614 2 22850
54 127.07753652299444 3 23200
55 124.59141418625812 4 23550
56 122.27578013601534 5 23900
57 120.38214657630877 1 24250
58 119.12432966418496 2 24600
59 118.65934994918358 3 24950
60 119.07340619529367 4 25300
61 120.37417550208815 5 25650