From cb6da4d73752ed272f3cb4a6733c54b69837e81b Mon Sep 17 00:00:00 2001 From: kingchenc Date: Mon, 15 Jun 2026 22:36:16 +0200 Subject: [PATCH] feat(data-layer): Resampler (candle resampling) in all 10 languages (#310) * feat(data-layer): Resampler (candle resampling) in all 10 languages Second data-layer feature (F3): resample candles into a higher timeframe. - Native (Node.js/WASM): new Resampler(timeframe) -> update(o,h,l,c,v,ts): Candle|null + flush(): Candle|null. Python the same -> tuple|None. - C ABI: wickra_resampler_new/update/flush/free (update has the multi-output shape so the generators auto-emit it; flush is bespoke). Go Update -> (Candle, bool) + Flush; C# Candle? Update/Flush; Java Candle update/flush; R update() generic + a flush() S3 method (extends base::flush); C/C++ direct. - Cross-language golden (testdata/golden/data_resampled.csv): the shared input candles resampled into 5-unit buckets, the final partial bucket via flush, pinned bit-for-bit across every binding. Verified locally in all 10 (3 candles for the 5-unit smoke; 16 for the golden). The WickraCandle output record is shared with the tick aggregator (deduped). * test(node): exclude data-layer types from the indicator completeness contract The Resampler exposes update(), so the completeness test flagged it as an indicator and required batch/reset/isReady/warmupPeriod, which a data-layer type does not have. Exclude TickAggregator and Resampler like the bar builders. --- CHANGELOG.md | 8 ++ bindings/c/include/wickra.h | 17 ++++ bindings/c/src/lib.rs | 98 +++++++++++++++++++ .../csharp/Wickra.Tests/DataLayerTests.cs | 31 ++++++ .../csharp/Wickra/Generated/Indicators.g.cs | 44 +++++++++ .../Wickra/Generated/NativeMethods.g.cs | 14 +++ bindings/go/data_layer_test.go | 31 ++++++ bindings/go/include/wickra.h | 17 ++++ bindings/go/indicators_gen.go | 50 ++++++++++ .../src/main/java/org/wickra/Resampler.java | 73 ++++++++++++++ .../org/wickra/internal/NativeMethods.java | 8 ++ .../test/java/org/wickra/DataLayerTest.java | 27 +++++ bindings/node/__tests__/completeness.test.js | 10 +- bindings/node/__tests__/data_layer.test.js | 24 ++++- bindings/node/index.d.ts | 16 +++ bindings/node/index.js | 3 +- bindings/node/src/lib.rs | 63 ++++++++++++ bindings/python/python/wickra/__init__.py | 2 + bindings/python/src/lib.rs | 50 ++++++++++ bindings/python/tests/test_data_layer.py | 21 ++++ bindings/r/NAMESPACE | 2 + bindings/r/R/indicators.R | 8 ++ bindings/r/R/methods.R | 19 ++++ bindings/r/src/wickra.c | 61 ++++++++++++ bindings/r/tests/testthat/test-data-layer.R | 28 ++++++ bindings/wasm/src/lib.rs | 58 +++++++++++ bindings/wasm/tests/data_layer.test.js | 22 +++++ examples/c/data_layer_test.c | 54 ++++++++++ examples/rust/src/bin/gen_golden.rs | 29 ++++++ testdata/golden/data_resampled.csv | 17 ++++ 30 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 bindings/java/src/main/java/org/wickra/Resampler.java create mode 100644 testdata/golden/data_resampled.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index 7427276a..619792cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Candle resampling in all 10 languages (data layer).** The `Resampler` + aggregates candles into a higher timeframe (e.g. 1m → 5m): `update(open, high, + low, close, volume, timestamp)` returns the completed higher-timeframe candle on + a bucket boundary (else `null`/`None`/`NA`), and `flush()` emits the final, + still-open candle. Exposed natively (Node.js / WASM / Python) and over the C ABI + (Go / C# / Java return `(Candle, bool)` / `Candle?` / `Candle`; R via `update()` + and a `flush()` S3 method; C / C++ directly). A cross-language golden + (`testdata/golden/data_resampled.csv`) pins the resampled stream identically. - **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, diff --git a/bindings/c/include/wickra.h b/bindings/c/include/wickra.h index 9a660819..22f8e152 100644 --- a/bindings/c/include/wickra.h +++ b/bindings/c/include/wickra.h @@ -678,6 +678,8 @@ typedef struct RenkoBars RenkoBars; typedef struct RenkoTrailingStop RenkoTrailingStop; +typedef struct Resampler Resampler; + typedef struct RickshawMan RickshawMan; typedef struct RisingThreeMethods RisingThreeMethods; @@ -13719,6 +13721,21 @@ intptr_t wickra_tick_aggregator_drain(struct TickAggregator *handle, void wickra_tick_aggregator_free(struct TickAggregator *handle); +struct Resampler *wickra_resampler_new(int64_t timeframe); + +bool wickra_resampler_update(struct Resampler *handle, + double open, + double high, + double low, + double close, + double volume, + int64_t timestamp, + struct WickraCandle *out); + +bool wickra_resampler_flush(struct Resampler *handle, struct WickraCandle *out); + +void wickra_resampler_free(struct Resampler *handle); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index f3ada17c..ec1d9fb1 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -116,6 +116,8 @@ use wickra_core::{ use wickra_data::aggregator::{TickAggregator as DataTickAggregator, Timeframe}; +use wickra_data::resample::Resampler; + // ===== Scalar indicators (f64 -> f64) ===== /// Create a `AdaptiveCycle` indicator. @@ -68188,6 +68190,102 @@ pub unsafe extern "C" fn wickra_tick_aggregator_free(handle: *mut TickAggregator } } +/// Convert a core candle into the C-ABI view. +fn candle_to_c(candle: Candle) -> WickraCandle { + WickraCandle { + open: candle.open, + high: candle.high, + low: candle.low, + close: candle.close, + volume: candle.volume, + timestamp: candle.timestamp, + } +} + +/// Create a resampler aggregating input candles into `timeframe`-sized candles +/// (the same unit as the candle timestamps). Returns `NULL` on a non-positive +/// timeframe; release with `wickra_resampler_free`. +#[no_mangle] +pub extern "C" fn wickra_resampler_new(timeframe: i64) -> *mut Resampler { + match Timeframe::new(timeframe) { + Ok(tf) => Box::into_raw(Box::new(Resampler::new(tf))), + Err(_) => ptr::null_mut(), + } +} + +/// Push one candle. On `true` the completed higher-timeframe candle is written to +/// `*out`; `false` means none closed yet (still aggregating), a `NULL` handle / +/// `out`, or an invalid input candle. +/// +/// # Safety +/// `handle` (from `wickra_resampler_new`, not freed) and `out` must be valid or `NULL`. +#[no_mangle] +pub unsafe extern "C" fn wickra_resampler_update( + handle: *mut Resampler, + open: f64, + high: f64, + low: f64, + close: f64, + volume: f64, + timestamp: i64, + out: *mut WickraCandle, +) -> bool { + let Some(resampler) = handle.as_mut() else { + return false; + }; + if out.is_null() { + return false; + } + let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else { + return false; + }; + match resampler.push(candle) { + Ok(Some(emitted)) => { + *out = candle_to_c(emitted); + true + } + _ => false, + } +} + +/// Flush the final, still-open candle. On `true` it is written to `*out`; `false` +/// means nothing was pending or `handle` / `out` is `NULL`. +/// +/// # Safety +/// `handle` (from `wickra_resampler_new`, not freed) and `out` must be valid or `NULL`. +#[no_mangle] +pub unsafe extern "C" fn wickra_resampler_flush( + handle: *mut Resampler, + out: *mut WickraCandle, +) -> bool { + let Some(resampler) = handle.as_mut() else { + return false; + }; + if out.is_null() { + return false; + } + match resampler.flush() { + Ok(Some(emitted)) => { + *out = candle_to_c(emitted); + true + } + _ => false, + } +} + +/// Destroy a resampler created by `wickra_resampler_new`. No-op if `handle` is +/// `NULL`. +/// +/// # Safety +/// `handle` must have been returned by `wickra_resampler_new` and not previously +/// freed, or `NULL`. +#[no_mangle] +pub unsafe extern "C" fn wickra_resampler_free(handle: *mut Resampler) { + if !handle.is_null() { + drop(Box::from_raw(handle)); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/bindings/csharp/Wickra.Tests/DataLayerTests.cs b/bindings/csharp/Wickra.Tests/DataLayerTests.cs index c06f3224..33588a11 100644 --- a/bindings/csharp/Wickra.Tests/DataLayerTests.cs +++ b/bindings/csharp/Wickra.Tests/DataLayerTests.cs @@ -28,6 +28,37 @@ public class DataLayerTests return rows.ToArray(); } + [Fact] + public void ResamplerMatchesGolden() + { + var input = Read("input"); // open,high,low,close,volume (timestamp = row index) + using var r = new Resampler(5); + var got = new List(); + for (var i = 0; i < input.Length; i++) + { + var c = r.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], i); + if (c.HasValue) + { + got.Add(new[] { c.Value.Open, c.Value.High, c.Value.Low, c.Value.Close, c.Value.Volume, (double)c.Value.Timestamp }); + } + } + var f = r.Flush(); + if (f.HasValue) + { + got.Add(new[] { f.Value.Open, f.Value.High, f.Value.Low, f.Value.Close, f.Value.Volume, (double)f.Value.Timestamp }); + } + var want = Read("data_resampled"); + 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, $"resample row {i} col {j}: {got[i][j]} vs {want[i][j]}"); + } + } + } + [Theory] [InlineData(false, "data_candles")] [InlineData(true, "data_candles_gap")] diff --git a/bindings/csharp/Wickra/Generated/Indicators.g.cs b/bindings/csharp/Wickra/Generated/Indicators.g.cs index fdcba531..340abcdc 100644 --- a/bindings/csharp/Wickra/Generated/Indicators.g.cs +++ b/bindings/csharp/Wickra/Generated/Indicators.g.cs @@ -26580,6 +26580,50 @@ public sealed class RenkoTrailingStop : IDisposable public void Dispose() => _handle.Dispose(); } +public sealed class Resampler : IDisposable +{ + private readonly WickraHandle _handle; + + public Resampler(long timeframe) + { + var ptr = NativeMethods.wickra_resampler_new(timeframe); + if (ptr == nint.Zero) + { + throw new ArgumentException("invalid Resampler parameters"); + } + + _handle = new WickraHandle(ptr, NativeMethods.wickra_resampler_free); + } + + public Candle? Update(double open, double high, double low, double close, double volume, long timestamp) + { + WickraCandle native; + bool ok; + unsafe + { + ok = NativeMethods.wickra_resampler_update(_handle.DangerousGetHandle(), open, high, low, close, volume, timestamp, &native); + } + + GC.KeepAlive(_handle); + return ok ? new Candle(native.open, native.high, native.low, native.close, native.volume, native.timestamp) : null; + } + + /// Emit the final, still-open candle (null if none is pending). + public Candle? Flush() + { + WickraCandle native; + bool ok; + unsafe + { + ok = NativeMethods.wickra_resampler_flush(_handle.DangerousGetHandle(), &native); + } + GC.KeepAlive(_handle); + return ok ? new Candle(native.open, native.high, native.low, native.close, native.volume, native.timestamp) : null; + } + + public void Dispose() => _handle.Dispose(); +} + public sealed class RickshawMan : IDisposable { private readonly WickraHandle _handle; diff --git a/bindings/csharp/Wickra/Generated/NativeMethods.g.cs b/bindings/csharp/Wickra/Generated/NativeMethods.g.cs index b5f745c1..d2972d3e 100644 --- a/bindings/csharp/Wickra/Generated/NativeMethods.g.cs +++ b/bindings/csharp/Wickra/Generated/NativeMethods.g.cs @@ -12416,6 +12416,20 @@ internal static partial class NativeMethods [LibraryImport(WickraNative.LibraryName)] internal static partial void wickra_tick_aggregator_free(nint handle); + [LibraryImport(WickraNative.LibraryName)] + internal static partial nint wickra_resampler_new(long timeframe); + + [LibraryImport(WickraNative.LibraryName)] + [return: MarshalAs(UnmanagedType.U1)] + internal static unsafe partial bool wickra_resampler_update(nint handle, double open, double high, double low, double close, double volume, long timestamp, WickraCandle* @out); + + [LibraryImport(WickraNative.LibraryName)] + [return: MarshalAs(UnmanagedType.U1)] + internal static unsafe partial bool wickra_resampler_flush(nint handle, WickraCandle* @out); + + [LibraryImport(WickraNative.LibraryName)] + internal static partial void wickra_resampler_free(nint handle); + } [StructLayout(LayoutKind.Sequential)] diff --git a/bindings/go/data_layer_test.go b/bindings/go/data_layer_test.go index e07b7ad8..22505a22 100644 --- a/bindings/go/data_layer_test.go +++ b/bindings/go/data_layer_test.go @@ -20,6 +20,37 @@ func dataParseF(t *testing.T, s string) float64 { return v } +func TestResamplerGolden(t *testing.T) { + input := readGolden(t, "input") // open,high,low,close,volume (timestamp = row index) + r, err := NewResampler(5) + if err != nil { + t.Fatalf("new: %v", err) + } + var got [][6]float64 + for i, row := range input { + o, h, l, c, v := dataParseF(t, row[0]), dataParseF(t, row[1]), dataParseF(t, row[2]), dataParseF(t, row[3]), dataParseF(t, row[4]) + if k, ok := r.Update(o, h, l, c, v, int64(i)); ok { + got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)}) + } + } + if k, ok := r.Flush(); ok { + got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)}) + } + r.Close() + want := readGolden(t, "data_resampled") + if len(got) != len(want) { + t.Fatalf("resample: %d candles vs %d", 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("resample row %d col %d: %v vs %v", i, j, got[i][j], w) + } + } + } +} + func TestTickAggregatorGolden(t *testing.T) { ticks := readGolden(t, "data_ticks") cases := []struct { diff --git a/bindings/go/include/wickra.h b/bindings/go/include/wickra.h index 9a660819..22f8e152 100644 --- a/bindings/go/include/wickra.h +++ b/bindings/go/include/wickra.h @@ -678,6 +678,8 @@ typedef struct RenkoBars RenkoBars; typedef struct RenkoTrailingStop RenkoTrailingStop; +typedef struct Resampler Resampler; + typedef struct RickshawMan RickshawMan; typedef struct RisingThreeMethods RisingThreeMethods; @@ -13719,6 +13721,21 @@ intptr_t wickra_tick_aggregator_drain(struct TickAggregator *handle, void wickra_tick_aggregator_free(struct TickAggregator *handle); +struct Resampler *wickra_resampler_new(int64_t timeframe); + +bool wickra_resampler_update(struct Resampler *handle, + double open, + double high, + double low, + double close, + double volume, + int64_t timestamp, + struct WickraCandle *out); + +bool wickra_resampler_flush(struct Resampler *handle, struct WickraCandle *out); + +void wickra_resampler_free(struct Resampler *handle); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/bindings/go/indicators_gen.go b/bindings/go/indicators_gen.go index 9e5cc4a8..cca87df2 100644 --- a/bindings/go/indicators_gen.go +++ b/bindings/go/indicators_gen.go @@ -28057,6 +28057,56 @@ func (ind *RenkoTrailingStop) Close() { } } +// Resampler wraps the Resampler indicator over the Wickra C ABI. +type Resampler struct { + handle *C.struct_Resampler +} + +// NewResampler constructs a Resampler. It returns ErrInvalidParams when the +// native constructor rejects the arguments. +func NewResampler(timeframe int64) (*Resampler, error) { + ptr := C.wickra_resampler_new(C.int64_t(timeframe)) + if ptr == nil { + return nil, ErrInvalidParams + } + obj := &Resampler{handle: ptr} + runtime.SetFinalizer(obj, (*Resampler).Close) + return obj, nil +} + +// Update feeds one observation. The bool reports whether a value is +// available yet (false during warmup). +func (ind *Resampler) Update(open float64, high float64, low float64, close float64, volume float64, timestamp int64) (Candle, bool) { + var out C.struct_WickraCandle + ok := bool(C.wickra_resampler_update(ind.handle, C.double(open), C.double(high), C.double(low), C.double(close), C.double(volume), C.int64_t(timestamp), &out)) + runtime.KeepAlive(ind) + if !ok { + return Candle{}, false + } + return Candle{float64(out.open), float64(out.high), float64(out.low), float64(out.close), float64(out.volume), int64(out.timestamp)}, true +} + +// Flush emits the final, still-open candle (ok is false if none is pending). +func (ind *Resampler) Flush() (Candle, bool) { + var out C.struct_WickraCandle + ok := bool(C.wickra_resampler_flush(ind.handle, &out)) + runtime.KeepAlive(ind) + if !ok { + return Candle{}, false + } + return Candle{float64(out.open), float64(out.high), float64(out.low), float64(out.close), float64(out.volume), int64(out.timestamp)}, true +} + +// Close frees the native handle. It is idempotent and safe to call +// alongside the finalizer. +func (ind *Resampler) Close() { + if ind.handle != nil { + C.wickra_resampler_free(ind.handle) + ind.handle = nil + runtime.SetFinalizer(ind, nil) + } +} + // RickshawMan wraps the RickshawMan indicator over the Wickra C ABI. type RickshawMan struct { handle *C.struct_RickshawMan diff --git a/bindings/java/src/main/java/org/wickra/Resampler.java b/bindings/java/src/main/java/org/wickra/Resampler.java new file mode 100644 index 00000000..2da50f41 --- /dev/null +++ b/bindings/java/src/main/java/org/wickra/Resampler.java @@ -0,0 +1,73 @@ +// 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 Resampler indicator over the Wickra C ABI. Not thread-safe; close when done. */ +public final class Resampler implements AutoCloseable { + private final MemorySegment handle; + private final Cleaner.Cleanable cleanable; + + public Resampler(long timeframe) { + MemorySegment h; + try { + h = (MemorySegment) NativeMethods.WICKRA_RESAMPLER_NEW.invokeExact(timeframe); + } catch (Throwable t) { + throw WickraNative.rethrow(t); + } + if (h.address() == 0L) { + throw new IllegalArgumentException("invalid Resampler parameters"); + } + this.handle = h; + this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_RESAMPLER_FREE); + } + + /** Push one observation; returns the result, or null during warmup. */ + public Candle update(double open, double high, double low, double close, double volume, long timestamp) { + try (Arena a = Arena.ofConfined()) { + MemorySegment out = a.allocate(48L); + byte ok = (byte) NativeMethods.WICKRA_RESAMPLER_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp, out); + if (ok == 0) { + return null; + } + return new Candle( + out.get(JAVA_DOUBLE, 0L), + out.get(JAVA_DOUBLE, 8L), + out.get(JAVA_DOUBLE, 16L), + out.get(JAVA_DOUBLE, 24L), + out.get(JAVA_DOUBLE, 32L), + (double) out.get(JAVA_LONG, 40L)); + } catch (Throwable t) { + throw WickraNative.rethrow(t); + } + } + + /** Emit the final, still-open candle (null if none is pending). */ + public Candle flush() { + try (Arena a = Arena.ofConfined()) { + MemorySegment out = a.allocate(48L); + byte ok = (byte) NativeMethods.WICKRA_RESAMPLER_FLUSH.invokeExact(handle, out); + if (ok == 0) { + return null; + } + return new Candle( + out.get(JAVA_DOUBLE, 0L), + out.get(JAVA_DOUBLE, 8L), + out.get(JAVA_DOUBLE, 16L), + out.get(JAVA_DOUBLE, 24L), + out.get(JAVA_DOUBLE, 32L), + (double) out.get(JAVA_LONG, 40L)); + } catch (Throwable t) { + throw WickraNative.rethrow(t); + } + } + + @Override public void close() { + cleanable.clean(); + } +} diff --git a/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java b/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java index 22988ad1..5f7475f9 100644 --- a/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java +++ b/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java @@ -3951,6 +3951,10 @@ public final class NativeMethods { public static MethodHandle WICKRA_TICK_AGGREGATOR_PUSH; public static MethodHandle WICKRA_TICK_AGGREGATOR_DRAIN; public static MethodHandle WICKRA_TICK_AGGREGATOR_FREE; + public static MethodHandle WICKRA_RESAMPLER_NEW; + public static MethodHandle WICKRA_RESAMPLER_UPDATE; + public static MethodHandle WICKRA_RESAMPLER_FLUSH; + public static MethodHandle WICKRA_RESAMPLER_FREE; static { init0(); @@ -8023,6 +8027,10 @@ public final class NativeMethods { 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)); + WICKRA_RESAMPLER_NEW = h("wickra_resampler_new", FunctionDescriptor.of(ADDRESS, JAVA_LONG)); + WICKRA_RESAMPLER_UPDATE = h("wickra_resampler_update", FunctionDescriptor.of(JAVA_BYTE, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_LONG, ADDRESS)); + WICKRA_RESAMPLER_FLUSH = h("wickra_resampler_flush", FunctionDescriptor.of(JAVA_BYTE, ADDRESS, ADDRESS)); + WICKRA_RESAMPLER_FREE = h("wickra_resampler_free", FunctionDescriptor.ofVoid(ADDRESS)); } } diff --git a/bindings/java/src/test/java/org/wickra/DataLayerTest.java b/bindings/java/src/test/java/org/wickra/DataLayerTest.java index 36b5f0c9..0d7344dd 100644 --- a/bindings/java/src/test/java/org/wickra/DataLayerTest.java +++ b/bindings/java/src/test/java/org/wickra/DataLayerTest.java @@ -46,6 +46,33 @@ class DataLayerTest { return rows.toArray(new double[0][]); } + @Test + void resamplerMatchesGolden() throws IOException { + double[][] input = read("input"); // open,high,low,close,volume (timestamp = row index) + try (Resampler r = new Resampler(5)) { + List got = new ArrayList<>(); + for (int i = 0; i < input.length; i++) { + Candle c = r.update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], i); + if (c != null) { + got.add(new double[]{c.open(), c.high(), c.low(), c.close(), c.volume(), (double) c.timestamp()}); + } + } + Candle f = r.flush(); + if (f != null) { + got.add(new double[]{f.open(), f.high(), f.low(), f.close(), f.volume(), (double) f.timestamp()}); + } + double[][] want = read("data_resampled"); + assertEquals(want.length, got.size(), "resample 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)), + "resample row " + i + " col " + j + ": " + got.get(i)[j] + " vs " + w); + } + } + } + } + @Test void tickAggregatorMatchesGolden() throws IOException { double[][] ticks = read("data_ticks"); diff --git a/bindings/node/__tests__/completeness.test.js b/bindings/node/__tests__/completeness.test.js index 0b8927db..65724767 100644 --- a/bindings/node/__tests__/completeness.test.js +++ b/bindings/node/__tests__/completeness.test.js @@ -27,9 +27,14 @@ const BAR_BUILDERS = new Set([ 'ThreeLineBreakBars', ]); +// Data-layer types (tick aggregator, resampler) are not `Indicator`s: they +// transform raw market data into candles and have their own update/flush shape, +// so they are excluded from the streaming-indicator completeness contract. +const DATA_LAYER = new Set(['TickAggregator', 'Resampler']); + // An "indicator class" is an exported constructor whose prototype carries the // streaming `update` method. This excludes `version` (a plain function), the bar -// builders, and any non-indicator export. +// builders, the data-layer types, and any non-indicator export. function indicatorClasses() { return Object.keys(wickra).filter((name) => { const value = wickra[name]; @@ -37,7 +42,8 @@ function indicatorClasses() { typeof value === 'function' && value.prototype && typeof value.prototype.update === 'function' && - !BAR_BUILDERS.has(name) + !BAR_BUILDERS.has(name) && + !DATA_LAYER.has(name) ); }); } diff --git a/bindings/node/__tests__/data_layer.test.js b/bindings/node/__tests__/data_layer.test.js index 0b5e8a52..1ff7151d 100644 --- a/bindings/node/__tests__/data_layer.test.js +++ b/bindings/node/__tests__/data_layer.test.js @@ -8,7 +8,7 @@ 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 { TickAggregator, Resampler } = require('..'); const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden'); @@ -51,3 +51,25 @@ test('tick aggregator matches the golden candles', () => { test('tick aggregator gap-fill matches the golden candles', () => { assertCandles(run(true), readCsv('data_candles_gap'), 'gap'); }); + +const INPUT = readCsv('input'); // open,high,low,close,volume (timestamp = row index) + +function runResample() { + const r = new Resampler(5); + const out = []; + INPUT.forEach(([o, h, l, c, v], i) => { + const candle = r.update(o, h, l, c, v, i); + if (candle) { + out.push([candle.open, candle.high, candle.low, candle.close, candle.volume, candle.timestamp]); + } + }); + const f = r.flush(); + if (f) { + out.push([f.open, f.high, f.low, f.close, f.volume, f.timestamp]); + } + return out; +} + +test('resampler matches the golden candles', () => { + assertCandles(runResample(), readCsv('data_resampled'), 'resample'); +}); diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index bf1350d5..235db139 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -5957,3 +5957,19 @@ export declare class TickAggregator { /** Whether gap filling is enabled. */ fillsGaps(): boolean } +export type ResamplerNode = Resampler +/** Resample candles into a higher timeframe (e.g. 1m -> 5m). */ +export declare class Resampler { + /** + * Construct a resampler that aggregates inputs into `timeframe`-sized + * candles (same unit as the candle timestamps). + */ + constructor(timeframe: number) + /** + * Push one candle; returns the completed higher-timeframe candle when a + * bucket boundary is crossed, otherwise `null`. + */ + update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): CandleValue | null + /** Emit the final, still-open candle (or `null` if none is pending). */ + flush(): CandleValue | null +} diff --git a/bindings/node/index.js b/bindings/node/index.js index f14c8e03..8fc2ee43 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, SterlingRatio, BurkeRatio, MartinRatio, TailRatio, KRatio, CommonSenseRatio, GainToPainRatio, UpsidePotentialRatio, M2Measure, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, ADOSC, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, NakedPoc, SinglePrints, ProfileShape, HighLowVolumeNodes, CompositeProfile, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, RangeBars, TickBars, VolumeBars, DollarBars, ImbalanceBars, RunBars, ThreeLineBreakBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd, TickAggregator } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, SterlingRatio, BurkeRatio, MartinRatio, TailRatio, KRatio, CommonSenseRatio, GainToPainRatio, UpsidePotentialRatio, M2Measure, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, ADOSC, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, NakedPoc, SinglePrints, ProfileShape, HighLowVolumeNodes, CompositeProfile, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, RangeBars, TickBars, VolumeBars, DollarBars, ImbalanceBars, RunBars, ThreeLineBreakBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd, TickAggregator, Resampler } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -828,3 +828,4 @@ module.exports.IntradayIntensity = IntradayIntensity module.exports.BetterVolume = BetterVolume module.exports.VolumeWeightedMacd = VolumeWeightedMacd module.exports.TickAggregator = TickAggregator +module.exports.Resampler = Resampler diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 4c12399e..9896ab07 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -21937,3 +21937,66 @@ impl TickAggregatorNode { self.inner.fills_gaps() } } + +// ===== Data layer: resampling (candle -> higher-timeframe candle) ===== + +fn candle_to_value(c: wc::Candle) -> CandleValue { + CandleValue { + open: c.open, + high: c.high, + low: c.low, + close: c.close, + volume: c.volume, + timestamp: c.timestamp as f64, + } +} + +/// Resample candles into a higher timeframe (e.g. 1m -> 5m). +#[napi(js_name = "Resampler")] +pub struct ResamplerNode { + inner: wickra_data::resample::Resampler, +} + +#[napi] +impl ResamplerNode { + /// Construct a resampler that aggregates inputs into `timeframe`-sized + /// candles (same unit as the candle timestamps). + #[napi(constructor)] + pub fn new(timeframe: f64) -> napi::Result { + let tf = wickra_data::aggregator::Timeframe::new(timeframe as i64).map_err(map_data_err)?; + Ok(Self { + inner: wickra_data::resample::Resampler::new(tf), + }) + } + + /// Push one candle; returns the completed higher-timeframe candle when a + /// bucket boundary is crossed, otherwise `null`. + #[napi] + pub fn update( + &mut self, + open: f64, + high: f64, + low: f64, + close: f64, + volume: f64, + timestamp: f64, + ) -> napi::Result> { + let candle = + wc::Candle::new(open, high, low, close, volume, timestamp as i64).map_err(map_err)?; + Ok(self + .inner + .push(candle) + .map_err(map_data_err)? + .map(candle_to_value)) + } + + /// Emit the final, still-open candle (or `null` if none is pending). + #[napi] + pub fn flush(&mut self) -> napi::Result> { + Ok(self + .inner + .flush() + .map_err(map_data_err)? + .map(candle_to_value)) + } +} diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 5b4dccb0..dac093a2 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -360,6 +360,7 @@ from ._wickra import ( CandleVolume, # Data layer TickAggregator, + Resampler, # Market Profile CompositeProfile, HighLowVolumeNodes, @@ -906,6 +907,7 @@ __all__ = [ "CandleVolume", # Data layer "TickAggregator", + "Resampler", # Market Profile "CompositeProfile", "HighLowVolumeNodes", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 705492ab..f7fb5961 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -28287,6 +28287,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; // Data layer. m.add_class::()?; + m.add_class::()?; // Candlestick patterns. m.add_class::()?; m.add_class::()?; @@ -28616,3 +28617,52 @@ impl PyTickAggregator { format!("TickAggregator(fills_gaps={})", self.inner.fills_gaps()) } } + +// ===== Data layer: resampling (candle -> higher-timeframe candle) ===== + +/// Resample candles into a higher timeframe (e.g. 1m -> 5m). +#[pyclass(name = "Resampler", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyResampler { + inner: wickra_data::resample::Resampler, +} + +#[pymethods] +impl PyResampler { + #[new] + fn new(timeframe: i64) -> PyResult { + let tf = wickra_data::aggregator::Timeframe::new(timeframe).map_err(map_data_err)?; + Ok(Self { + inner: wickra_data::resample::Resampler::new(tf), + }) + } + + /// Push one candle; returns the completed higher-timeframe candle as + /// `(open, high, low, close, volume, timestamp)` on a bucket boundary, else + /// `None`. + fn update( + &mut self, + open: f64, + high: f64, + low: f64, + close: f64, + volume: f64, + timestamp: i64, + ) -> PyResult> { + let candle = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?; + Ok(self + .inner + .push(candle) + .map_err(map_data_err)? + .map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp))) + } + + /// Emit the final, still-open candle (or `None` if none is pending). + fn flush(&mut self) -> PyResult> { + Ok(self + .inner + .flush() + .map_err(map_data_err)? + .map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp))) + } +} diff --git a/bindings/python/tests/test_data_layer.py b/bindings/python/tests/test_data_layer.py index a959a833..8f58c7c2 100644 --- a/bindings/python/tests/test_data_layer.py +++ b/bindings/python/tests/test_data_layer.py @@ -42,3 +42,24 @@ def test_tick_aggregator_matches_golden(gap_fill, fixture): 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]}" + + +INPUT = _read("input") # open,high,low,close,volume (timestamp = row index) + + +def test_resampler_matches_golden(): + r = ta.Resampler(5) + got = [] + for i, (o, h, l, c, v) in enumerate(INPUT): + candle = r.update(o, h, l, c, v, i) + if candle is not None: + got.append(candle) + f = r.flush() + if f is not None: + got.append(f) + want = _read("data_resampled") + 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]}" diff --git a/bindings/r/NAMESPACE b/bindings/r/NAMESPACE index 54a98938..69852c09 100644 --- a/bindings/r/NAMESPACE +++ b/bindings/r/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand S3method(batch,wickra_indicator) +S3method(flush,wickra_indicator) S3method(is_ready,wickra_indicator) S3method(name,wickra_indicator) S3method(push,wickra_indicator) @@ -341,6 +342,7 @@ export(RegimeLabel) export(RelativeStrengthAB) export(RenkoBars) export(RenkoTrailingStop) +export(Resampler) export(RickshawMan) export(RisingThreeMethods) export(Rmi) diff --git a/bindings/r/R/indicators.R b/bindings/r/R/indicators.R index 91646b6c..4124be32 100644 --- a/bindings/r/R/indicators.R +++ b/bindings/r/R/indicators.R @@ -2679,6 +2679,14 @@ RenkoTrailingStop <- function(block_size) { .wk_obj("renko_trailing_stop", ptr, "RenkoTrailingStop") } +#' Resampler indicator +#' @keywords internal +#' @export +Resampler <- function(timeframe) { + ptr <- .Call("wk_resampler_new", timeframe, PACKAGE = "wickra") + .wk_obj("resampler", ptr, "Resampler") +} + #' RickshawMan indicator #' @keywords internal #' @export diff --git a/bindings/r/R/methods.R b/bindings/r/R/methods.R index 37363c12..bc7eecff 100644 --- a/bindings/r/R/methods.R +++ b/bindings/r/R/methods.R @@ -170,3 +170,22 @@ push.wickra_indicator <- function(object, price, size, timestamp) { colnames(out) <- c("open", "high", "low", "close", "volume", "timestamp") out } + +#' Flush a resampler's final candle +#' +#' Emit the final, still-open candle a [Resampler()] is aggregating (the partial +#' higher-timeframe bar that no later input has closed yet). Extends the base +#' generic [base::flush()]. +#' +#' @param con A `wickra_indicator` created by [Resampler()]. +#' @return A named numeric vector (`open`, `high`, `low`, `close`, `volume`, +#' `timestamp`), or `NULL` if nothing is pending. +#' @examples +#' r <- Resampler(5) +#' for (t in 0:6) update(r, 100 + t, 101 + t, 99 + t, 100 + t, 10, t) +#' flush(r) +#' @exportS3Method base::flush +flush.wickra_indicator <- function(con) { + out <- .Call(paste0("wk_", con$prefix, "_flush"), con$ptr, PACKAGE = "wickra") + out +} diff --git a/bindings/r/src/wickra.c b/bindings/r/src/wickra.c index 9a177f5a..2a2bd80a 100644 --- a/bindings/r/src/wickra.c +++ b/bindings/r/src/wickra.c @@ -14606,6 +14606,64 @@ SEXP wk_renko_trailing_stop_reset(SEXP e) { return R_NilValue; } +static void resampler_fin(SEXP e) { + struct Resampler *h = (struct Resampler *)R_ExternalPtrAddr(e); + if (h) wickra_resampler_free(h); + R_ClearExternalPtr(e); +} +SEXP wk_resampler_new(SEXP a0) { + struct Resampler *h = wickra_resampler_new((int64_t)Rf_asReal(a0)); + if (!h) Rf_error("invalid Resampler parameters"); + SEXP e = PROTECT(R_MakeExternalPtr(h, R_NilValue, R_NilValue)); + R_RegisterCFinalizerEx(e, resampler_fin, TRUE); + UNPROTECT(1); + return e; +} +SEXP wk_resampler_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5) { + struct Resampler *h = (struct Resampler *)R_ExternalPtrAddr(e); + struct WickraCandle out; + int ok = wickra_resampler_update(h, Rf_asReal(a0), Rf_asReal(a1), Rf_asReal(a2), Rf_asReal(a3), Rf_asReal(a4), (int64_t)Rf_asReal(a5), &out); + SEXP r = PROTECT(Rf_allocVector(REALSXP, 6)); + REAL(r)[0] = ok ? (double)out.open : NA_REAL; + REAL(r)[1] = ok ? (double)out.high : NA_REAL; + REAL(r)[2] = ok ? (double)out.low : NA_REAL; + REAL(r)[3] = ok ? (double)out.close : NA_REAL; + REAL(r)[4] = ok ? (double)out.volume : NA_REAL; + REAL(r)[5] = ok ? (double)out.timestamp : NA_REAL; + SEXP nm = PROTECT(Rf_allocVector(STRSXP, 6)); + SET_STRING_ELT(nm, 0, Rf_mkChar("open")); + SET_STRING_ELT(nm, 1, Rf_mkChar("high")); + SET_STRING_ELT(nm, 2, Rf_mkChar("low")); + SET_STRING_ELT(nm, 3, Rf_mkChar("close")); + SET_STRING_ELT(nm, 4, Rf_mkChar("volume")); + SET_STRING_ELT(nm, 5, Rf_mkChar("timestamp")); + Rf_setAttrib(r, R_NamesSymbol, nm); + UNPROTECT(2); + return r; +} +SEXP wk_resampler_flush(SEXP e) { + struct Resampler *h = (struct Resampler *)R_ExternalPtrAddr(e); + struct WickraCandle out; + if (!wickra_resampler_flush(h, &out)) return R_NilValue; + SEXP r = PROTECT(Rf_allocVector(REALSXP, 6)); + REAL(r)[0] = out.open; + REAL(r)[1] = out.high; + REAL(r)[2] = out.low; + REAL(r)[3] = out.close; + REAL(r)[4] = out.volume; + REAL(r)[5] = (double)out.timestamp; + SEXP nm = PROTECT(Rf_allocVector(STRSXP, 6)); + SET_STRING_ELT(nm, 0, Rf_mkChar("open")); + SET_STRING_ELT(nm, 1, Rf_mkChar("high")); + SET_STRING_ELT(nm, 2, Rf_mkChar("low")); + SET_STRING_ELT(nm, 3, Rf_mkChar("close")); + SET_STRING_ELT(nm, 4, Rf_mkChar("volume")); + SET_STRING_ELT(nm, 5, Rf_mkChar("timestamp")); + Rf_setAttrib(r, R_NamesSymbol, nm); + UNPROTECT(2); + return r; +} + static void rickshaw_man_fin(SEXP e) { struct RickshawMan *h = (struct RickshawMan *)R_ExternalPtrAddr(e); if (h) wickra_rickshaw_man_free(h); @@ -24781,6 +24839,9 @@ static const R_CallMethodDef CallEntries[] = { {"wk_renko_trailing_stop_is_ready", (DL_FUNC)&wk_renko_trailing_stop_is_ready, 1}, {"wk_renko_trailing_stop_name", (DL_FUNC)&wk_renko_trailing_stop_name, 1}, {"wk_renko_trailing_stop_reset", (DL_FUNC)&wk_renko_trailing_stop_reset, 1}, + {"wk_resampler_new", (DL_FUNC)&wk_resampler_new, 1}, + {"wk_resampler_update", (DL_FUNC)&wk_resampler_update, 7}, + {"wk_resampler_flush", (DL_FUNC)&wk_resampler_flush, 1}, {"wk_rickshaw_man_new", (DL_FUNC)&wk_rickshaw_man_new, 0}, {"wk_rickshaw_man_update", (DL_FUNC)&wk_rickshaw_man_update, 7}, {"wk_rickshaw_man_batch", (DL_FUNC)&wk_rickshaw_man_batch, 7}, diff --git a/bindings/r/tests/testthat/test-data-layer.R b/bindings/r/tests/testthat/test-data-layer.R index 81f6de22..d36303e2 100644 --- a/bindings/r/tests/testthat/test-data-layer.R +++ b/bindings/r/tests/testthat/test-data-layer.R @@ -50,3 +50,31 @@ test_that("tick aggregator matches the golden candles", { expect_equal(got, want, tolerance = 1e-9, info = spec$fixture) } }) + +test_that("resampler 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)] + do.call(rbind, lapply(lines, function(l) as.numeric(strsplit(l, ",")[[1]]))) + } + + input <- read_mat("input") # open,high,low,close,volume (timestamp = row index) + r <- Resampler(5) + got <- matrix(numeric(0), 0, 6) + for (i in seq_len(nrow(input))) { + c <- update(r, input[i, 1], input[i, 2], input[i, 3], input[i, 4], input[i, 5], i - 1) + if (!is.na(c[1])) { + got <- rbind(got, unname(c)) + } + } + f <- flush(r) + if (!is.null(f)) { + got <- rbind(got, unname(f)) + } + want <- unname(read_mat("data_resampled")) + expect_equal(nrow(got), nrow(want)) + expect_equal(got, want, tolerance = 1e-9) +}) diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 340823fc..bb1821dc 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -16043,3 +16043,61 @@ impl WasmTickAggregator { self.inner.fills_gaps() } } + +// ===== Data layer: resampling (candle -> higher-timeframe candle) ===== + +fn candle_object(c: wc::Candle) -> Object { + 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(); + obj +} + +/// Resample candles into a higher timeframe (e.g. 1m -> 5m). +#[wasm_bindgen(js_name = Resampler)] +pub struct WasmResampler { + inner: wickra_data::resample::Resampler, +} + +#[wasm_bindgen(js_class = Resampler)] +impl WasmResampler { + /// Construct a resampler aggregating inputs into `timeframe`-sized candles. + #[wasm_bindgen(constructor)] + pub fn new(timeframe: f64) -> Result { + let tf = wickra_data::aggregator::Timeframe::new(timeframe as i64).map_err(map_data_err)?; + Ok(Self { + inner: wickra_data::resample::Resampler::new(tf), + }) + } + + /// Push one candle; returns a `{ open, high, low, close, volume, timestamp }` + /// object on a bucket boundary, otherwise `null`. + pub fn update( + &mut self, + open: f64, + high: f64, + low: f64, + close: f64, + volume: f64, + timestamp: f64, + ) -> Result { + let candle = + wc::Candle::new(open, high, low, close, volume, timestamp as i64).map_err(map_err)?; + match self.inner.push(candle).map_err(map_data_err)? { + Some(c) => Ok(candle_object(c).into()), + None => Ok(JsValue::NULL), + } + } + + /// Emit the final, still-open candle (or `null` if none is pending). + pub fn flush(&mut self) -> Result { + match self.inner.flush().map_err(map_data_err)? { + Some(c) => Ok(candle_object(c).into()), + None => Ok(JsValue::NULL), + } + } +} diff --git a/bindings/wasm/tests/data_layer.test.js b/bindings/wasm/tests/data_layer.test.js index d1a6a99c..4656c6a3 100644 --- a/bindings/wasm/tests/data_layer.test.js +++ b/bindings/wasm/tests/data_layer.test.js @@ -49,3 +49,25 @@ test('wasm tick aggregator matches the golden candles', () => { test('wasm tick aggregator gap-fill matches the golden candles', () => { assertCandles(run(true), readCsv('data_candles_gap'), 'gap'); }); + +const INPUT = readCsv('input'); // open,high,low,close,volume (timestamp = row index) + +function runResample() { + const r = new W.Resampler(5); + const out = []; + INPUT.forEach(([o, h, l, c, v], i) => { + const candle = r.update(o, h, l, c, v, i); + if (candle) { + out.push([candle.open, candle.high, candle.low, candle.close, candle.volume, candle.timestamp]); + } + }); + const f = r.flush(); + if (f) { + out.push([f.open, f.high, f.low, f.close, f.volume, f.timestamp]); + } + return out; +} + +test('wasm resampler matches the golden candles', () => { + assertCandles(runResample(), readCsv('data_resampled'), 'resample'); +}); diff --git a/examples/c/data_layer_test.c b/examples/c/data_layer_test.c index 7db34d3a..e66a2d20 100644 --- a/examples/c/data_layer_test.c +++ b/examples/c/data_layer_test.c @@ -94,12 +94,66 @@ static int check(const char *fixture, bool gap, const double *ticks, int nt) { return fails; } +static int check_resample(void) { + double input[MAXROWS * 5]; + int ni = read_csv("input", input, 5); + double want[MAXROWS * 6]; + int nw = read_csv("data_resampled", want, 6); + struct Resampler *r = wickra_resampler_new(5); + if (!r) { + printf("FAIL resample: new returned NULL\n"); + return 1; + } + double got[MAXROWS * 6]; + int ng = 0; + struct WickraCandle out; + for (int i = 0; i < ni; i++) { + if (wickra_resampler_update(r, input[i * 5 + 0], input[i * 5 + 1], input[i * 5 + 2], + input[i * 5 + 3], input[i * 5 + 4], (int64_t)i, &out)) { + got[ng * 6 + 0] = out.open; + got[ng * 6 + 1] = out.high; + got[ng * 6 + 2] = out.low; + got[ng * 6 + 3] = out.close; + got[ng * 6 + 4] = out.volume; + got[ng * 6 + 5] = (double)out.timestamp; + ng++; + } + } + if (wickra_resampler_flush(r, &out)) { + got[ng * 6 + 0] = out.open; + got[ng * 6 + 1] = out.high; + got[ng * 6 + 2] = out.low; + got[ng * 6 + 3] = out.close; + got[ng * 6 + 4] = out.volume; + got[ng * 6 + 5] = (double)out.timestamp; + ng++; + } + wickra_resampler_free(r); + if (ng != nw) { + printf("FAIL resample: %d candles vs %d\n", 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 resample row %d col %d: %g vs %g\n", 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); + fails += check_resample(); if (fails == 0) { printf("C/C++ data layer: OK (%d ticks)\n", nt); } diff --git a/examples/rust/src/bin/gen_golden.rs b/examples/rust/src/bin/gen_golden.rs index 310ead39..5591118b 100644 --- a/examples/rust/src/bin/gen_golden.rs +++ b/examples/rust/src/bin/gen_golden.rs @@ -20,6 +20,7 @@ use wickra::{ Indicator, IntradayIntensity, MacdIndicator, Rsi, Sma, Tick, }; use wickra_data::aggregator::{TickAggregator, Timeframe}; +use wickra_data::resample::Resampler; const N: usize = 80; @@ -185,9 +186,37 @@ fn main() { emit_profiles(dir, &candles); emit_bars(dir, &candles); emit_data_layer(dir); + emit_resampler(dir, &candles); println!("golden fixtures written to {}", dir.display()); } +/// Data layer: the resampler. Resamples the shared input candles (timestamp = +/// row index) into 5-unit buckets; the final partial bucket comes out of flush. +fn emit_resampler(dir: &Path, candles: &[Candle]) { + let mut resampler = Resampler::new(Timeframe::new(5).unwrap()); + let mut rows = Vec::new(); + for &candle in candles { + if let Some(out) = resampler.push(candle).expect("valid resample push") { + rows.push(format!( + "{},{},{},{},{},{}", + out.open, out.high, out.low, out.close, out.volume, out.timestamp + )); + } + } + if let Some(out) = resampler.flush().expect("valid resample flush") { + rows.push(format!( + "{},{},{},{},{},{}", + out.open, out.high, out.low, out.close, out.volume, out.timestamp + )); + } + write_csv( + dir, + "data_resampled", + "open,high,low,close,volume,timestamp", + &rows, + ); +} + /// 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 diff --git a/testdata/golden/data_resampled.csv b/testdata/golden/data_resampled.csv new file mode 100644 index 00000000..97a880af --- /dev/null +++ b/testdata/golden/data_resampled.csv @@ -0,0 +1,17 @@ +open,high,low,close,volume,timestamp +99,112.90403177129735,98,111.32039085967226,5187.008623739826,0 +113.27609348158748,115.1642566915892,107.76539185209613,108.7737988023383,5473.238103468329,5 +106.12753789513545,107.73969337995807,96.34710045689485,98.28424227586412,5324.374653317379,10 +97.378063505514,106.32510161131832,95.93821562552817,103.99314457402363,5150.941358775185,15 +108.04491654708718,121.38046215528227,105.7105413401633,119.93667863849153,5456.9835954080345,20 +120.88220148856881,124.2878498649492,119.39438848583472,121.12969230082183,5371.101223522536,25 +119.8808727652764,121.29920058454442,108.51535390119264,110.00125312406458,5128.47088205189,30 +108.48360243707184,113.29382811110736,106.26786689008574,111.88016416080967,5431.582557578566,35 +114.22618875818226,130.20955349568973,113.09073586402833,127.92073514707224,5410.400200215731,40 +131.41114890560974,135.065460892756,129.49585703767033,132.95746831142935,5120.058973527027,45 +130.5116755897077,132.71696973631924,121.2254215875191,122.27578013601534,5397.543391924787,50 +121.09703354508844,122.45602084147426,117.25536324735009,120.37417550208815,5441.485014111252,55 +122.33587608239566,136.79793765689536,120.87761530843784,135.43314928819893,5132.5539248008245,60 +138.08766685694894,146.57143212166807,136.58837142074304,144.11152724502116,5355.547392578692,65 +144.27024859045207,145.7471249168318,132.66155074344638,134.9266357939324,5463.733502447633,70 +131.6480122345848,134.02210748888697,127.39947545719488,129.59514479102842,5163.946538828373,75