feat(data): expose CandleReader (CSV) natively in all 10 languages (#311)
Add the data-layer CSV candle reader to every binding so loading OHLCV candles from a CSV no longer needs a per-language CSV/dataframe dependency. - C ABI: wickra_candle_reader_new(bytes, len) / _count / _read / _free over an opaque CandleReader handle (parse the whole buffer up front, then drain). - Native: Node/WASM CandleReader.read() -> Candle[], Python read() -> list[tuple]. - C-ABI languages: Go Read() []Candle, C# Candle[] Read(), Java Candle[] read(), R read() S3 generic (n x 6 matrix); C / C++ call the C ABI directly. - Cross-language golden testdata/golden/data_csv*.csv pins the parsed candles bit-for-bit across every binding. Verified locally across Rust (test+clippy+fmt), Node, WASM, Python, C#, Go, Java, R, and the C/C++ cmake parity suite.
This commit is contained in:
@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **CSV candle reading in all 10 languages (data layer).** The `CandleReader`
|
||||
parses a `timestamp,open,high,low,close,volume` CSV buffer (a leading UTF-8 BOM
|
||||
and field whitespace are tolerated) into candles: construct it from a CSV string
|
||||
and call `read()` for every candle in file order. Exposed natively (Node.js /
|
||||
WASM `read(): Candle[]`, Python `read() -> list[tuple]`) and over the C ABI as Go
|
||||
`Read() []Candle`, C# `Candle[] Read()`, Java `Candle[] read()`, and the R
|
||||
`read()` S3 generic (an `n×6` matrix); C / C++ call `wickra_candle_reader_new` /
|
||||
`_count` / `_read` directly. A cross-language golden
|
||||
(`testdata/golden/data_csv*.csv`) pins the parsed candles identically across
|
||||
every binding. This makes CSV backtest loading dependency-free in every binding.
|
||||
- **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
|
||||
|
||||
@@ -128,6 +128,8 @@ typedef struct CalmarRatio CalmarRatio;
|
||||
|
||||
typedef struct Camarilla Camarilla;
|
||||
|
||||
typedef struct CandleReader CandleReader;
|
||||
|
||||
typedef struct CandleVolume CandleVolume;
|
||||
|
||||
typedef struct Cci Cci;
|
||||
@@ -13736,6 +13738,16 @@ bool wickra_resampler_flush(struct Resampler *handle, struct WickraCandle *out);
|
||||
|
||||
void wickra_resampler_free(struct Resampler *handle);
|
||||
|
||||
struct CandleReader *wickra_candle_reader_new(const uint8_t *data, uintptr_t len);
|
||||
|
||||
uintptr_t wickra_candle_reader_count(const struct CandleReader *handle);
|
||||
|
||||
uintptr_t wickra_candle_reader_read(struct CandleReader *handle,
|
||||
struct WickraCandle *out,
|
||||
uintptr_t cap);
|
||||
|
||||
void wickra_candle_reader_free(struct CandleReader *handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
@@ -68286,6 +68286,99 @@ pub unsafe extern "C" fn wickra_resampler_free(handle: *mut Resampler) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque CSV candle reader: parses an entire `timestamp,open,high,low,close,volume`
|
||||
/// CSV buffer up front and hands the candles out in drain order. Named
|
||||
/// `CandleReader` (the public C-ABI handle); the inner `wickra-data` reader is
|
||||
/// reached through its full path to avoid the name clash.
|
||||
#[derive(Debug)]
|
||||
pub struct CandleReader {
|
||||
candles: Vec<Candle>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
/// Parse an OHLCV CSV buffer (`len` bytes at `data`) into candles. The first line
|
||||
/// must be a header naming `timestamp,open,high,low,close,volume` (a leading UTF-8
|
||||
/// BOM and field whitespace are tolerated). Returns `NULL` on a `NULL` pointer or a
|
||||
/// malformed CSV (missing column, unparseable row, or an OHLC relation the core
|
||||
/// rejects). Read the candles with `wickra_candle_reader_read` and release with
|
||||
/// `wickra_candle_reader_free`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `data` must point to `len` readable bytes, or be `NULL`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wickra_candle_reader_new(
|
||||
data: *const u8,
|
||||
len: usize,
|
||||
) -> *mut CandleReader {
|
||||
if data.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let bytes = slice::from_raw_parts(data, len);
|
||||
let Ok(mut reader) = wickra_data::csv::CandleReader::from_reader(bytes) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
match reader.read_all() {
|
||||
Ok(candles) => Box::into_raw(Box::new(CandleReader { candles, pos: 0 })),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of candles not yet read from the reader. Returns `0` on a `NULL` handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be valid (from `wickra_candle_reader_new`, not freed), or `NULL`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wickra_candle_reader_count(handle: *const CandleReader) -> usize {
|
||||
match handle.as_ref() {
|
||||
Some(reader) => reader.candles.len() - reader.pos,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy up to `cap` not-yet-read candles into `out`, advance past them, and return
|
||||
/// the number written. Returns `0` on a `NULL` handle / `out`. Call with `cap` equal
|
||||
/// to `wickra_candle_reader_count` to drain every candle in one call.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` (from `wickra_candle_reader_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_candle_reader_read(
|
||||
handle: *mut CandleReader,
|
||||
out: *mut WickraCandle,
|
||||
cap: usize,
|
||||
) -> usize {
|
||||
let Some(reader) = handle.as_mut() else {
|
||||
return 0;
|
||||
};
|
||||
if out.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let count = (reader.candles.len() - reader.pos).min(cap);
|
||||
let slots = slice::from_raw_parts_mut(out, count);
|
||||
for (slot, candle) in slots
|
||||
.iter_mut()
|
||||
.zip(&reader.candles[reader.pos..reader.pos + count])
|
||||
{
|
||||
*slot = candle_to_c(*candle);
|
||||
}
|
||||
reader.pos += count;
|
||||
count
|
||||
}
|
||||
|
||||
/// Destroy a candle reader created by `wickra_candle_reader_new`. No-op if `handle`
|
||||
/// is `NULL`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must have been returned by `wickra_candle_reader_new` and not previously
|
||||
/// freed, or `NULL`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wickra_candle_reader_free(handle: *mut CandleReader) {
|
||||
if !handle.is_null() {
|
||||
drop(Box::from_raw(handle));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -59,6 +59,26 @@ public class DataLayerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CandleReaderMatchesGolden()
|
||||
{
|
||||
var csv = File.ReadAllText(Path.Combine(GoldenDir(), "data_csv.csv"));
|
||||
using var reader = new CandleReader(csv);
|
||||
var candles = reader.Read();
|
||||
var want = Read("data_csv_candles");
|
||||
Assert.Equal(want.Length, candles.Length);
|
||||
for (var i = 0; i < candles.Length; i++)
|
||||
{
|
||||
var c = candles[i];
|
||||
var got = new[] { c.Open, c.High, c.Low, c.Close, c.Volume, (double)c.Timestamp };
|
||||
for (var j = 0; j < 6; j++)
|
||||
{
|
||||
var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j]));
|
||||
Assert.True(Math.Abs(got[j] - want[i][j]) <= tol, $"candle reader row {i} col {j}: {got[j]} vs {want[i][j]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "data_candles")]
|
||||
[InlineData(true, "data_candles_gap")]
|
||||
|
||||
@@ -4981,6 +4981,60 @@ public sealed class Camarilla : IDisposable
|
||||
public void Dispose() => _handle.Dispose();
|
||||
}
|
||||
|
||||
public sealed class CandleReader : IDisposable
|
||||
{
|
||||
private readonly WickraHandle _handle;
|
||||
|
||||
/// <summary>Parse a timestamp,open,high,low,close,volume CSV string (a
|
||||
/// leading UTF-8 BOM and field whitespace are tolerated).</summary>
|
||||
public CandleReader(string csv)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(csv);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
||||
nint ptr;
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* data = bytes)
|
||||
{
|
||||
ptr = NativeMethods.wickra_candle_reader_new(data, (nuint)bytes.Length);
|
||||
}
|
||||
}
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
throw new ArgumentException("invalid CandleReader CSV");
|
||||
}
|
||||
_handle = new WickraHandle(ptr, NativeMethods.wickra_candle_reader_free);
|
||||
}
|
||||
|
||||
/// <summary>Every candle parsed from the CSV, in file order.</summary>
|
||||
public Candle[] Read()
|
||||
{
|
||||
var count = (long)NativeMethods.wickra_candle_reader_count(_handle.DangerousGetHandle());
|
||||
GC.KeepAlive(_handle);
|
||||
if (count <= 0)
|
||||
{
|
||||
return Array.Empty<Candle>();
|
||||
}
|
||||
var buffer = new WickraCandle[count];
|
||||
unsafe
|
||||
{
|
||||
fixed (WickraCandle* ptr = buffer)
|
||||
{
|
||||
NativeMethods.wickra_candle_reader_read(_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 CandleVolume : IDisposable
|
||||
{
|
||||
private readonly WickraHandle _handle;
|
||||
|
||||
@@ -12430,6 +12430,18 @@ internal static partial class NativeMethods
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static partial void wickra_resampler_free(nint handle);
|
||||
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static unsafe partial nint wickra_candle_reader_new(byte* data, nuint len);
|
||||
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static partial nuint wickra_candle_reader_count(nint handle);
|
||||
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static unsafe partial nuint wickra_candle_reader_read(nint handle, WickraCandle* @out, nuint cap);
|
||||
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static partial void wickra_candle_reader_free(nint handle);
|
||||
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
@@ -2,6 +2,7 @@ package wickra
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
@@ -51,6 +52,35 @@ func TestResamplerGolden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandleReaderGolden(t *testing.T) {
|
||||
csv, err := os.ReadFile("../../testdata/golden/data_csv.csv")
|
||||
if err != nil {
|
||||
t.Fatalf("read data_csv.csv: %v", err)
|
||||
}
|
||||
r, err := NewCandleReader(string(csv))
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
candles := r.Read()
|
||||
got := make([][6]float64, len(candles))
|
||||
for i, c := range candles {
|
||||
got[i] = [6]float64{c.Open, c.High, c.Low, c.Close, c.Volume, float64(c.Timestamp)}
|
||||
}
|
||||
want := readGolden(t, "data_csv_candles")
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("candle reader: %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("candle reader 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 {
|
||||
|
||||
@@ -128,6 +128,8 @@ typedef struct CalmarRatio CalmarRatio;
|
||||
|
||||
typedef struct Camarilla Camarilla;
|
||||
|
||||
typedef struct CandleReader CandleReader;
|
||||
|
||||
typedef struct CandleVolume CandleVolume;
|
||||
|
||||
typedef struct Cci Cci;
|
||||
@@ -13736,6 +13738,16 @@ bool wickra_resampler_flush(struct Resampler *handle, struct WickraCandle *out);
|
||||
|
||||
void wickra_resampler_free(struct Resampler *handle);
|
||||
|
||||
struct CandleReader *wickra_candle_reader_new(const uint8_t *data, uintptr_t len);
|
||||
|
||||
uintptr_t wickra_candle_reader_count(const struct CandleReader *handle);
|
||||
|
||||
uintptr_t wickra_candle_reader_read(struct CandleReader *handle,
|
||||
struct WickraCandle *out,
|
||||
uintptr_t cap);
|
||||
|
||||
void wickra_candle_reader_free(struct CandleReader *handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
@@ -5746,6 +5746,57 @@ func (ind *Camarilla) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// CandleReader parses an OHLCV CSV buffer into candles over the Wickra C ABI.
|
||||
type CandleReader struct {
|
||||
handle *C.struct_CandleReader
|
||||
}
|
||||
|
||||
// NewCandleReader parses a timestamp,open,high,low,close,volume CSV string (a
|
||||
// leading UTF-8 BOM and field whitespace are tolerated). It returns
|
||||
// ErrInvalidParams when the header or a row is malformed.
|
||||
func NewCandleReader(csv string) (*CandleReader, error) {
|
||||
data := []byte(csv)
|
||||
var ptr *C.struct_CandleReader
|
||||
if len(data) == 0 {
|
||||
ptr = C.wickra_candle_reader_new(nil, 0)
|
||||
} else {
|
||||
ptr = C.wickra_candle_reader_new((*C.uint8_t)(unsafe.Pointer(&data[0])), C.uintptr_t(len(data)))
|
||||
}
|
||||
if ptr == nil {
|
||||
return nil, ErrInvalidParams
|
||||
}
|
||||
obj := &CandleReader{handle: ptr}
|
||||
runtime.SetFinalizer(obj, (*CandleReader).Close)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// Read returns every candle parsed from the CSV, in file order.
|
||||
func (ind *CandleReader) Read() []Candle {
|
||||
n := int(C.wickra_candle_reader_count(ind.handle))
|
||||
runtime.KeepAlive(ind)
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
buf := make([]C.struct_WickraCandle, n)
|
||||
C.wickra_candle_reader_read(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 *CandleReader) Close() {
|
||||
if ind.handle != nil {
|
||||
C.wickra_candle_reader_free(ind.handle)
|
||||
ind.handle = nil
|
||||
runtime.SetFinalizer(ind, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// CandleVolume wraps the CandleVolume indicator over the Wickra C ABI.
|
||||
type CandleVolume struct {
|
||||
handle *C.struct_CandleVolume
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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.*;
|
||||
|
||||
/** CSV candle reader over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class CandleReader implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
/** Parse a timestamp,open,high,low,close,volume CSV string (a leading
|
||||
* UTF-8 BOM and field whitespace are tolerated). */
|
||||
public CandleReader(String csv) {
|
||||
if (csv == null) {
|
||||
throw new NullPointerException("csv");
|
||||
}
|
||||
byte[] bytes = csv.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
MemorySegment h;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment data = a.allocate(Math.max(1L, bytes.length));
|
||||
MemorySegment.copy(bytes, 0, data, JAVA_BYTE, 0L, bytes.length);
|
||||
h = (MemorySegment) NativeMethods.WICKRA_CANDLE_READER_NEW.invokeExact(data, (long) bytes.length);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid CandleReader CSV");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_CANDLE_READER_FREE);
|
||||
}
|
||||
|
||||
/** Every candle parsed from the CSV, in file order. */
|
||||
public Candle[] read() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_CANDLE_READER_COUNT.invokeExact(handle);
|
||||
if (n <= 0) {
|
||||
return new Candle[0];
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment out = a.allocate(48L * n);
|
||||
long w = (long) NativeMethods.WICKRA_CANDLE_READER_READ.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();
|
||||
}
|
||||
}
|
||||
@@ -3955,6 +3955,10 @@ public final class NativeMethods {
|
||||
public static MethodHandle WICKRA_RESAMPLER_UPDATE;
|
||||
public static MethodHandle WICKRA_RESAMPLER_FLUSH;
|
||||
public static MethodHandle WICKRA_RESAMPLER_FREE;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_NEW;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_COUNT;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_READ;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_FREE;
|
||||
|
||||
static {
|
||||
init0();
|
||||
@@ -8031,6 +8035,10 @@ public final class NativeMethods {
|
||||
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));
|
||||
WICKRA_CANDLE_READER_NEW = h("wickra_candle_reader_new", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG));
|
||||
WICKRA_CANDLE_READER_COUNT = h("wickra_candle_reader_count", FunctionDescriptor.of(JAVA_LONG, ADDRESS));
|
||||
WICKRA_CANDLE_READER_READ = h("wickra_candle_reader_read", FunctionDescriptor.of(JAVA_LONG, ADDRESS, ADDRESS, JAVA_LONG));
|
||||
WICKRA_CANDLE_READER_FREE = h("wickra_candle_reader_free", FunctionDescriptor.ofVoid(ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,6 +73,25 @@ class DataLayerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candleReaderMatchesGolden() throws IOException {
|
||||
String csv = Files.readString(goldenDir().resolve("data_csv.csv"));
|
||||
try (CandleReader reader = new CandleReader(csv)) {
|
||||
Candle[] candles = reader.read();
|
||||
double[][] want = read("data_csv_candles");
|
||||
assertEquals(want.length, candles.length, "candle reader count");
|
||||
for (int i = 0; i < candles.length; i++) {
|
||||
Candle k = candles[i];
|
||||
double[] got = {k.open(), k.high(), k.low(), k.close(), k.volume(), (double) k.timestamp()};
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double w = want[i][j];
|
||||
assertTrue(Math.abs(got[j] - w) <= 1e-9 * Math.max(1, Math.abs(w)),
|
||||
"candle reader row " + i + " col " + j + ": " + got[j] + " vs " + w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tickAggregatorMatchesGolden() throws IOException {
|
||||
double[][] ticks = read("data_ticks");
|
||||
|
||||
@@ -27,10 +27,11 @@ 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']);
|
||||
// Data-layer types (tick aggregator, resampler, CSV candle reader) are not
|
||||
// `Indicator`s: they transform raw market data into candles and have their own
|
||||
// update/flush/read shape, so they are excluded from the streaming-indicator
|
||||
// completeness contract.
|
||||
const DATA_LAYER = new Set(['TickAggregator', 'Resampler', 'CandleReader']);
|
||||
|
||||
// An "indicator class" is an exported constructor whose prototype carries the
|
||||
// streaming `update` method. This excludes `version` (a plain function), the bar
|
||||
|
||||
@@ -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, Resampler } = require('..');
|
||||
const { TickAggregator, Resampler, CandleReader } = require('..');
|
||||
|
||||
const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden');
|
||||
|
||||
@@ -52,6 +52,13 @@ test('tick aggregator gap-fill matches the golden candles', () => {
|
||||
assertCandles(run(true), readCsv('data_candles_gap'), 'gap');
|
||||
});
|
||||
|
||||
test('candle reader matches the golden candles', () => {
|
||||
const csv = fs.readFileSync(path.join(GOLDEN, 'data_csv.csv'), 'utf8');
|
||||
const reader = new CandleReader(csv);
|
||||
const got = reader.read().map((c) => [c.open, c.high, c.low, c.close, c.volume, c.timestamp]);
|
||||
assertCandles(got, readCsv('data_csv_candles'), 'candle-reader');
|
||||
});
|
||||
|
||||
const INPUT = readCsv('input'); // open,high,low,close,volume (timestamp = row index)
|
||||
|
||||
function runResample() {
|
||||
|
||||
Vendored
+11
@@ -5973,3 +5973,14 @@ export declare class Resampler {
|
||||
/** Emit the final, still-open candle (or `null` if none is pending). */
|
||||
flush(): CandleValue | null
|
||||
}
|
||||
export type CandleReaderNode = CandleReader
|
||||
/**
|
||||
* Parse OHLCV candles from a CSV string (header `timestamp,open,high,low,close,
|
||||
* volume`; a leading UTF-8 BOM is stripped).
|
||||
*/
|
||||
export declare class CandleReader {
|
||||
/** Parse the whole CSV up front; throws on a malformed header or row. */
|
||||
constructor(csv: string)
|
||||
/** Return every parsed candle as `{ open, high, low, close, volume, timestamp }`. */
|
||||
read(): Array<CandleValue>
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22000,3 +22000,30 @@ impl ResamplerNode {
|
||||
.map(candle_to_value))
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data layer: CSV candle reader =====
|
||||
|
||||
/// Parse OHLCV candles from a CSV string (header `timestamp,open,high,low,close,
|
||||
/// volume`; a leading UTF-8 BOM is stripped).
|
||||
#[napi(js_name = "CandleReader")]
|
||||
pub struct CandleReaderNode {
|
||||
candles: Vec<wc::Candle>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl CandleReaderNode {
|
||||
/// Parse the whole CSV up front; throws on a malformed header or row.
|
||||
#[napi(constructor)]
|
||||
pub fn new(csv: String) -> napi::Result<Self> {
|
||||
let mut reader =
|
||||
wickra_data::csv::CandleReader::from_reader(csv.as_bytes()).map_err(map_data_err)?;
|
||||
let candles = reader.read_all().map_err(map_data_err)?;
|
||||
Ok(Self { candles })
|
||||
}
|
||||
|
||||
/// Return every parsed candle as `{ open, high, low, close, volume, timestamp }`.
|
||||
#[napi]
|
||||
pub fn read(&self) -> Vec<CandleValue> {
|
||||
self.candles.iter().map(|&c| candle_to_value(c)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ from ._wickra import (
|
||||
# Data layer
|
||||
TickAggregator,
|
||||
Resampler,
|
||||
CandleReader,
|
||||
# Market Profile
|
||||
CompositeProfile,
|
||||
HighLowVolumeNodes,
|
||||
@@ -908,6 +909,7 @@ __all__ = [
|
||||
# Data layer
|
||||
"TickAggregator",
|
||||
"Resampler",
|
||||
"CandleReader",
|
||||
# Market Profile
|
||||
"CompositeProfile",
|
||||
"HighLowVolumeNodes",
|
||||
|
||||
@@ -28288,6 +28288,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// Data layer.
|
||||
m.add_class::<PyTickAggregator>()?;
|
||||
m.add_class::<PyResampler>()?;
|
||||
m.add_class::<PyCandleReader>()?;
|
||||
// Candlestick patterns.
|
||||
m.add_class::<PyDoji>()?;
|
||||
m.add_class::<PyHammer>()?;
|
||||
@@ -28666,3 +28667,32 @@ impl PyResampler {
|
||||
.map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data layer: CSV candle reader =====
|
||||
|
||||
/// Parse OHLCV candles from a CSV string (header `timestamp,open,high,low,close,
|
||||
/// volume`; a leading UTF-8 BOM is stripped).
|
||||
#[pyclass(name = "CandleReader", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyCandleReader {
|
||||
candles: Vec<wc::Candle>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCandleReader {
|
||||
#[new]
|
||||
fn new(csv: &str) -> PyResult<Self> {
|
||||
let mut reader =
|
||||
wickra_data::csv::CandleReader::from_reader(csv.as_bytes()).map_err(map_data_err)?;
|
||||
let candles = reader.read_all().map_err(map_data_err)?;
|
||||
Ok(Self { candles })
|
||||
}
|
||||
|
||||
/// Return every parsed candle as `(open, high, low, close, volume, timestamp)`.
|
||||
fn read(&self) -> Vec<CandleTuple> {
|
||||
self.candles
|
||||
.iter()
|
||||
.map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,18 @@ def test_tick_aggregator_matches_golden(gap_fill, fixture):
|
||||
assert abs(g[j] - w[j]) <= tol, f"row {i} col {j}: {g[j]} vs {w[j]}"
|
||||
|
||||
|
||||
def test_candle_reader_matches_golden():
|
||||
with open(os.path.join(GOLDEN, "data_csv.csv")) as f:
|
||||
text = f.read()
|
||||
got = ta.CandleReader(text).read()
|
||||
want = _read("data_csv_candles")
|
||||
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]}"
|
||||
|
||||
|
||||
INPUT = _read("input") # open,high,low,close,volume (timestamp = row index)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ S3method(flush,wickra_indicator)
|
||||
S3method(is_ready,wickra_indicator)
|
||||
S3method(name,wickra_indicator)
|
||||
S3method(push,wickra_indicator)
|
||||
S3method(read,wickra_indicator)
|
||||
S3method(reset,wickra_indicator)
|
||||
S3method(update,wickra_indicator)
|
||||
S3method(warmup_period,wickra_indicator)
|
||||
@@ -67,6 +68,7 @@ export(Butterfly)
|
||||
export(CalendarSpread)
|
||||
export(CalmarRatio)
|
||||
export(Camarilla)
|
||||
export(CandleReader)
|
||||
export(CandleVolume)
|
||||
export(Cci)
|
||||
export(CenterOfGravity)
|
||||
@@ -528,6 +530,7 @@ export(batch)
|
||||
export(is_ready)
|
||||
export(name)
|
||||
export(push)
|
||||
export(read)
|
||||
export(reset)
|
||||
export(warmup_period)
|
||||
importFrom(stats,update)
|
||||
|
||||
@@ -479,6 +479,14 @@ Camarilla <- function() {
|
||||
.wk_obj("camarilla", ptr, "Camarilla")
|
||||
}
|
||||
|
||||
#' CandleReader: parse OHLCV candles from a CSV string
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
CandleReader <- function(csv) {
|
||||
ptr <- .Call("wk_candle_reader_new", csv, PACKAGE = "wickra")
|
||||
.wk_obj("candle_reader", ptr, "CandleReader")
|
||||
}
|
||||
|
||||
#' CandleVolume indicator
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
|
||||
@@ -189,3 +189,26 @@ flush.wickra_indicator <- function(con) {
|
||||
out <- .Call(paste0("wk_", con$prefix, "_flush"), con$ptr, PACKAGE = "wickra")
|
||||
out
|
||||
}
|
||||
|
||||
#' Read every candle parsed by a CSV candle reader
|
||||
#'
|
||||
#' Returns all the candles a [CandleReader()] parsed from its CSV, as a numeric
|
||||
#' matrix with columns `open`, `high`, `low`, `close`, `volume`, `timestamp`.
|
||||
#'
|
||||
#' @param object A `wickra_indicator` created by [CandleReader()].
|
||||
#' @return A numeric matrix with six named columns (zero rows for an empty CSV).
|
||||
#' @examples
|
||||
#' r <- CandleReader("timestamp,open,high,low,close,volume\n0,100,101,99,100.5,10\n")
|
||||
#' read(r)
|
||||
#' @export
|
||||
read <- function(object) {
|
||||
UseMethod("read")
|
||||
}
|
||||
|
||||
#' @rdname read
|
||||
#' @export
|
||||
read.wickra_indicator <- function(object) {
|
||||
out <- .Call(paste0("wk_", object$prefix, "_read"), object$ptr, PACKAGE = "wickra")
|
||||
colnames(out) <- c("open", "high", "low", "close", "volume", "timestamp")
|
||||
out
|
||||
}
|
||||
|
||||
@@ -2603,6 +2603,40 @@ SEXP wk_camarilla_reset(SEXP e) {
|
||||
return R_NilValue;
|
||||
}
|
||||
|
||||
static void candle_reader_fin(SEXP e) {
|
||||
struct CandleReader *h = (struct CandleReader *)R_ExternalPtrAddr(e);
|
||||
if (h) wickra_candle_reader_free(h);
|
||||
R_ClearExternalPtr(e);
|
||||
}
|
||||
SEXP wk_candle_reader_new(SEXP csv) {
|
||||
SEXP s = STRING_ELT(csv, 0);
|
||||
const char *str = CHAR(s);
|
||||
struct CandleReader *h = wickra_candle_reader_new((const uint8_t *)str, (uintptr_t)LENGTH(s));
|
||||
if (!h) Rf_error("invalid CandleReader CSV");
|
||||
SEXP e = PROTECT(R_MakeExternalPtr(h, R_NilValue, R_NilValue));
|
||||
R_RegisterCFinalizerEx(e, candle_reader_fin, TRUE);
|
||||
UNPROTECT(1);
|
||||
return e;
|
||||
}
|
||||
SEXP wk_candle_reader_read(SEXP e) {
|
||||
struct CandleReader *h = (struct CandleReader *)R_ExternalPtrAddr(e);
|
||||
uintptr_t n = wickra_candle_reader_count(h);
|
||||
if (n == 0) return Rf_allocMatrix(REALSXP, 0, 6);
|
||||
struct WickraCandle *buf = (struct WickraCandle *)R_alloc(n, sizeof(struct WickraCandle));
|
||||
uintptr_t w = wickra_candle_reader_read(h, buf, n);
|
||||
SEXP r = PROTECT(Rf_allocMatrix(REALSXP, (int)w, 6));
|
||||
for (uintptr_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 candle_volume_fin(SEXP e) {
|
||||
struct CandleVolume *h = (struct CandleVolume *)R_ExternalPtrAddr(e);
|
||||
if (h) wickra_candle_volume_free(h);
|
||||
@@ -23021,6 +23055,8 @@ static const R_CallMethodDef CallEntries[] = {
|
||||
{"wk_camarilla_is_ready", (DL_FUNC)&wk_camarilla_is_ready, 1},
|
||||
{"wk_camarilla_name", (DL_FUNC)&wk_camarilla_name, 1},
|
||||
{"wk_camarilla_reset", (DL_FUNC)&wk_camarilla_reset, 1},
|
||||
{"wk_candle_reader_new", (DL_FUNC)&wk_candle_reader_new, 1},
|
||||
{"wk_candle_reader_read", (DL_FUNC)&wk_candle_reader_read, 1},
|
||||
{"wk_candle_volume_new", (DL_FUNC)&wk_candle_volume_new, 1},
|
||||
{"wk_candle_volume_update", (DL_FUNC)&wk_candle_volume_update, 7},
|
||||
{"wk_candle_volume_warmup_period", (DL_FUNC)&wk_candle_volume_warmup_period, 1},
|
||||
|
||||
@@ -51,6 +51,24 @@ test_that("tick aggregator matches the golden candles", {
|
||||
}
|
||||
})
|
||||
|
||||
test_that("candle reader 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]])))
|
||||
}
|
||||
|
||||
csv <- paste(readLines(file.path(gdir, "data_csv.csv")), collapse = "\n")
|
||||
reader <- CandleReader(csv)
|
||||
got <- unname(read(reader))
|
||||
want <- unname(read_mat("data_csv_candles"))
|
||||
expect_equal(nrow(got), nrow(want))
|
||||
expect_equal(got, want, tolerance = 1e-9)
|
||||
})
|
||||
|
||||
test_that("resampler matches the golden candles", {
|
||||
gdir <- find_data_golden_dir()
|
||||
skip_if(is.null(gdir), "golden fixtures not bundled with the package")
|
||||
|
||||
@@ -16101,3 +16101,34 @@ impl WasmResampler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data layer: CSV candle reader =====
|
||||
|
||||
/// Parse OHLCV candles from a CSV string (header `timestamp,open,high,low,close,
|
||||
/// volume`; a leading UTF-8 BOM is stripped).
|
||||
#[wasm_bindgen(js_name = CandleReader)]
|
||||
pub struct WasmCandleReader {
|
||||
candles: Vec<wc::Candle>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = CandleReader)]
|
||||
impl WasmCandleReader {
|
||||
/// Parse the whole CSV up front; throws on a malformed header or row.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(csv: &str) -> Result<WasmCandleReader, JsError> {
|
||||
let mut reader =
|
||||
wickra_data::csv::CandleReader::from_reader(csv.as_bytes()).map_err(map_data_err)?;
|
||||
let candles = reader.read_all().map_err(map_data_err)?;
|
||||
Ok(Self { candles })
|
||||
}
|
||||
|
||||
/// Return every parsed candle as a `{ open, high, low, close, volume,
|
||||
/// timestamp }` object.
|
||||
pub fn read(&self) -> Array {
|
||||
let arr = Array::new();
|
||||
for &c in &self.candles {
|
||||
arr.push(&candle_object(c));
|
||||
}
|
||||
arr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ test('wasm tick aggregator gap-fill matches the golden candles', () => {
|
||||
assertCandles(run(true), readCsv('data_candles_gap'), 'gap');
|
||||
});
|
||||
|
||||
test('wasm candle reader matches the golden candles', () => {
|
||||
const csv = fs.readFileSync(path.join(GOLDEN, 'data_csv.csv'), 'utf8');
|
||||
const reader = new W.CandleReader(csv);
|
||||
const got = reader.read().map((c) => [c.open, c.high, c.low, c.close, c.volume, c.timestamp]);
|
||||
assertCandles(got, readCsv('data_csv_candles'), 'candle-reader');
|
||||
});
|
||||
|
||||
const INPUT = readCsv('input'); // open,high,low,close,volume (timestamp = row index)
|
||||
|
||||
function runResample() {
|
||||
|
||||
@@ -147,6 +147,49 @@ static int check_resample(void) {
|
||||
return fails;
|
||||
}
|
||||
|
||||
static int check_reader(void) {
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/data_csv.csv", GDIR);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
printf("FAIL reader: cannot open %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
static char buf[1 << 16];
|
||||
size_t len = fread(buf, 1, sizeof(buf), f);
|
||||
fclose(f);
|
||||
|
||||
struct CandleReader *r = wickra_candle_reader_new((const uint8_t *)buf, len);
|
||||
if (!r) {
|
||||
printf("FAIL reader: new returned NULL\n");
|
||||
return 1;
|
||||
}
|
||||
double want[MAXROWS * 6];
|
||||
int nw = read_csv("data_csv_candles", want, 6);
|
||||
uintptr_t n = wickra_candle_reader_count(r);
|
||||
struct WickraCandle cands[MAXROWS];
|
||||
uintptr_t got = wickra_candle_reader_read(r, cands, n);
|
||||
wickra_candle_reader_free(r);
|
||||
if ((int)got != nw) {
|
||||
printf("FAIL reader: %d candles vs %d\n", (int)got, nw);
|
||||
return 1;
|
||||
}
|
||||
int fails = 0;
|
||||
for (uintptr_t i = 0; i < got; i++) {
|
||||
double row[6] = {cands[i].open, cands[i].high, cands[i].low,
|
||||
cands[i].close, cands[i].volume, (double)cands[i].timestamp};
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double w = want[i * 6 + j];
|
||||
double tol = 1e-9 * fmax(1.0, fabs(w));
|
||||
if (fabs(row[j] - w) > tol) {
|
||||
printf("FAIL reader row %d col %d: %g vs %g\n", (int)i, j, row[j], w);
|
||||
fails++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fails;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GDIR = (argc > 1) ? argv[1] : "testdata/golden";
|
||||
double ticks[MAXROWS * 3];
|
||||
@@ -154,6 +197,7 @@ int main(int argc, char **argv) {
|
||||
int fails = check("data_candles", false, ticks, nt);
|
||||
fails += check("data_candles_gap", true, ticks, nt);
|
||||
fails += check_resample();
|
||||
fails += check_reader();
|
||||
if (fails == 0) {
|
||||
printf("C/C++ data layer: OK (%d ticks)\n", nt);
|
||||
}
|
||||
|
||||
@@ -187,9 +187,57 @@ fn main() {
|
||||
emit_bars(dir, &candles);
|
||||
emit_data_layer(dir);
|
||||
emit_resampler(dir, &candles);
|
||||
emit_candle_reader_csv(dir, &candles);
|
||||
println!("golden fixtures written to {}", dir.display());
|
||||
}
|
||||
|
||||
/// Data layer: the CSV candle reader. Writes a source CSV in the reader's required
|
||||
/// `timestamp,open,high,low,close,volume` layout, plus the reference candles the
|
||||
/// reader parses out of it. Every binding parses the same bytes and checks the
|
||||
/// candles match — pinning column mapping and numeric round-tripping across the
|
||||
/// FFI.
|
||||
fn emit_candle_reader_csv(dir: &Path, candles: &[Candle]) {
|
||||
let mut src = Vec::with_capacity(candles.len());
|
||||
for c in candles {
|
||||
src.push(format!(
|
||||
"{},{},{},{},{},{}",
|
||||
c.timestamp, c.open, c.high, c.low, c.close, c.volume
|
||||
));
|
||||
}
|
||||
write_csv(
|
||||
dir,
|
||||
"data_csv",
|
||||
"timestamp,open,high,low,close,volume",
|
||||
&src,
|
||||
);
|
||||
|
||||
// Reference parse: feed the same bytes back through the reader so the fixture
|
||||
// is exactly what wickra-data's parser yields, not just the input echoed.
|
||||
let mut bytes = String::from("timestamp,open,high,low,close,volume\n");
|
||||
for row in &src {
|
||||
bytes.push_str(row);
|
||||
bytes.push('\n');
|
||||
}
|
||||
let mut reader =
|
||||
wickra_data::csv::CandleReader::from_reader(bytes.as_bytes()).expect("valid candle reader");
|
||||
let parsed = reader.read_all().expect("valid csv parse");
|
||||
let rows: Vec<String> = parsed
|
||||
.iter()
|
||||
.map(|c| {
|
||||
format!(
|
||||
"{},{},{},{},{},{}",
|
||||
c.open, c.high, c.low, c.close, c.volume, c.timestamp
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
write_csv(
|
||||
dir,
|
||||
"data_csv_candles",
|
||||
"open,high,low,close,volume,timestamp",
|
||||
&rows,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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]) {
|
||||
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
timestamp,open,high,low,close,volume
|
||||
0,99,101,98,100,1000
|
||||
1,102.57761950472302,104.77731091023225,101.25551066110417,103.4552020666134,1019.8669330795061
|
||||
2,106.10612242808222,108.13914959894458,104.61339756308799,106.64642473395035,1038.941834230865
|
||||
3,109.26253189460714,110.76487377959927,107.8309272112827,109.33326909627483,1056.4642473395036
|
||||
4,111.7365376962194,112.90403177129735,110.1528967845943,111.32039085967226,1071.7356090899523
|
||||
5,113.27609348158748,114.45148509543229,111.29955825219574,112.47494986604055,1084.1470984807897
|
||||
6,113.7284688053824,115.1642566915892,111.30268842257516,112.73847630878196,1093.2039085967226
|
||||
7,113.06855035377954,114.55977666009171,110.64086736017657,112.13209366648874,1098.544972998846
|
||||
8,111.40827542637513,112.72390874531129,109.43899848657534,110.75463180551151,1099.9573603041506
|
||||
9,108.98459460176909,109.99300155201126,107.76539185209613,108.7737988023383,1097.3847630878195
|
||||
10,106.12753789513545,107.73969337995807,104.79904459577605,106.41120008059868,1090.929742682568
|
||||
11,103.21387328427625,105.41662717550601,101.71978916733775,103.92254305856751,1080.849640381959
|
||||
12,100.61462528040111,103.00209502109563,99.18732582635697,101.57479556705148,1067.546318055115
|
||||
13,98.64575078243223,100.78188758933494,97.48620160125755,99.62233840816026,1051.5501371821465
|
||||
14,97.53034002152081,99.46748184049008,96.34710045689485,98.28424227586412,1033.4988150155905
|
||||
15,97.378063505514,99.16454670333486,95.93821562552817,97.72469882334903,1014.1120008059867
|
||||
16,98.18385394545021,99.67344281002588,96.54876504706593,98.0383539116416,1005.837414342758
|
||||
17,99.8438650794075,101.15293363552601,97.93278462060415,99.24185317672267,1025.5541102026832
|
||||
18,102.1834853863248,103.20029690993537,100.25554360082955,101.27235512444012,1044.2520443294852
|
||||
19,104.99031673022002,106.32510161131832,102.65835969292533,103.99314457402363,1061.185789094272
|
||||
20,108.04491654708718,109.54022022493461,105.7105413401633,107.20584501801073,1075.680249530793
|
||||
21,111.14367593283949,112.56654934841096,109.24526558927202,110.6681390048435,1087.157577241359
|
||||
22,114.11098793714572,115.26697281350663,112.95942875877287,114.11541363513378,1095.1602073889517
|
||||
23,116.80109288512898,118.47543335247398,115.61005717653698,117.28439764388199,1099.3691003633464
|
||||
24,119.09282467975903,121.38046215528227,117.64904116296829,119.93667863849153,1099.616460883584
|
||||
25,120.88220148856881,123.36781277048146,119.39438848583472,121.87999976774739,1095.8924274663138
|
||||
26,122.07798667229586,124.2878498649492,120.77557026109272,122.98543345374605,1088.3454655720152
|
||||
27,122.60397744514097,124.22410945235427,121.57876610123756,123.19889810845086,1077.2764487555987
|
||||
28,122.40925186267498,123.88697089091687,121.06827005264091,122.5459890808828,1063.126663787232
|
||||
29,121.48461656761053,122.98099977052848,119.63330909790388,121.12969230082183,1046.4602179413757
|
||||
30,119.8808727652764,121.29920058454442,117.70285703314954,119.12118485241757,1027.9415498198925
|
||||
31,117.72299769788953,118.8665240235534,115.60101790940676,116.74454423507063,1008.3089402817496
|
||||
32,115.21439166809358,116.4131695096543,113.05795434620948,114.2567321877702,1011.6549204850494
|
||||
33,112.6270381197495,114.07463180365934,110.47704737833695,111.92464106224679,1031.154136351338
|
||||
34,110.27641646211617,111.76231568498811,108.51535390119264,110.00125312406458,1049.4113351138608
|
||||
35,108.48360243707184,109.99872116521585,107.18792367213928,108.7030424002833,1065.6986598718788
|
||||
36,107.530320991091,109.22424173559783,106.49671695482826,108.19063769933508,1079.3667863849153
|
||||
37,107.61494922421186,109.90155645208623,106.26786689008574,108.55447411796011,1089.8708095811626
|
||||
38,108.81801012516657,111.30403713029216,107.32068773822765,109.80671474335324,1096.7919672031487
|
||||
39,111.08434919099572,113.29382811110736,109.67068524069803,111.88016416080967,1099.8543345374605
|
||||
40,114.22618875818226,115.76972371414958,113.09073586402833,114.63427081999565,1098.9358246623383
|
||||
41,117.94724565362051,119.15370942824079,116.6612183117217,117.86768208634197,1094.0730556679773
|
||||
42,121.88395973243564,123.33523703654073,119.88495316810628,121.33623047221137,1085.459890808828
|
||||
43,125.65653611891825,127.14038418498515,123.29089632531003,124.77474439137693,1073.4397097874114
|
||||
44,128.92069597346688,130.20955349568973,126.63187762484937,127.92073514707224,1058.4917192891762
|
||||
45,131.41114890560974,132.45313613345562,129.49585703767033,130.5378442655162,1041.2118485241756
|
||||
46,132.96978971477444,134.3228744433646,131.08387196585088,132.43695669444105,1022.2889914100246
|
||||
47,133.5549991814736,135.053120145851,131.99497292310176,133.49309388747918,1002.4775425453357
|
||||
48,133.2323987581558,135.065460892756,131.8235156308926,133.65657776549278,1017.4326781222982
|
||||
49,132.1510588175171,134.08480947785137,131.0237176510951,132.95746831142935,1036.647912925193
|
||||
50,130.5116755897077,132.71696973631924,129.29758425495962,131.50287840157117,1054.402111088937
|
||||
51,128.53409061924222,130.9222390672229,127.07925728332546,129.46740573130614,1069.9874687593544
|
||||
52,126.4306172006658,128.5591966352313,124.94895708842891,127.07753652299444,1082.7826469085653
|
||||
53,124.38927906587094,125.87336879788326,123.1073244542458,124.59141418625812,1092.2775421612807
|
||||
54,122.56791894474918,123.61827749324543,121.2254215875191,122.27578013601534,1098.093623006649
|
||||
55,121.09703354508844,122.45602084147426,119.02315927992295,120.38214657630877,1099.9990206550704
|
||||
56,120.08693553049852,121.58571423995242,117.62555095473105,119.12432966418496,1097.9177729151318
|
||||
57,119.63399522490424,121.03798192673773,117.25536324735009,118.65934994918358,1091.9328525664675
|
||||
58,119.82146372498268,120.94065716085713,117.95421275941922,119.07340619529367,1082.2828594968707
|
||||
59,120.7124947130592,121.93415307848436,119.152517136663,120.37417550208815,1069.3525084777123
|
||||
60,122.33587608239566,123.94838830624106,120.87761530843784,122.49012753228324,1053.6572918000436
|
||||
61,124.66785812762632,126.75625008370868,123.18852214765032,125.27691410373268,1035.8229282236828
|
||||
62,127.61552102482923,129.80523536741393,126.34054904004905,128.53026338263376,1016.5604175448309
|
||||
63,131.00775897051096,133.06294250948164,129.94904333909741,132.0042268780681,1003.3623047221139
|
||||
64,134.59892592769242,136.79793765689536,133.234137558996,135.43314928819893,1023.1509825101539
|
||||
65,138.08766685694894,140.0546941334019,136.58837142074304,138.555398697196,1042.016703682664
|
||||
66,141.1500141222941,142.5489901806554,139.73776131670974,141.13673737507105,1059.2073514707224
|
||||
67,143.48228026040917,144.5932922665057,141.8802335303824,142.99124553647894,1074.037588995245
|
||||
68,144.84649927621132,146.0756620034572,142.76876627418082,143.9979290014267,1085.9161814856498
|
||||
69,145.1098734724699,146.57143212166807,142.649968595823,144.11152724502116,1094.3695669444105
|
||||
70,144.27024859045207,145.7471249168318,141.88968005898082,143.36655638536055,1099.060735569487
|
||||
71,142.46200877506277,143.72992039092952,140.60631434563564,141.8742259615024,1099.8026652716362
|
||||
72,139.9404686061768,141.00752471999962,138.7454488027266,139.81250491654941,1096.5657776549278
|
||||
73,137.04703875358243,138.78071046739933,135.67655244818158,137.41022416199849,1089.4791172140503
|
||||
74,134.16122174198705,136.42630679247307,132.66155074344638,134.9266357939324,1078.8252067375315
|
||||
75,131.6480122345848,134.02210748888697,130.25415962109273,132.6282548753949,1065.0287840157116
|
||||
76,129.80997879551026,131.86785162975886,128.70717960530897,130.76505243955756,1048.6398688853799
|
||||
77,128.85205701193928,130.78472862032595,127.6154547134165,129.54812632180318,1030.3118356745701
|
||||
78,128.8642014864336,130.59557044803222,127.39947545719488,129.13084441879352,1010.7753652299442
|
||||
79,129.82321172551153,131.29749357237068,128.12086294416926,129.59514479102842,1009.1906850227682
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
open,high,low,close,volume,timestamp
|
||||
99,101,98,100,1000,0
|
||||
102.57761950472302,104.77731091023225,101.25551066110417,103.4552020666134,1019.8669330795061,1
|
||||
106.10612242808222,108.13914959894458,104.61339756308799,106.64642473395035,1038.941834230865,2
|
||||
109.26253189460714,110.76487377959927,107.8309272112827,109.33326909627483,1056.4642473395036,3
|
||||
111.7365376962194,112.90403177129735,110.1528967845943,111.32039085967226,1071.7356090899523,4
|
||||
113.27609348158748,114.45148509543229,111.29955825219574,112.47494986604055,1084.1470984807897,5
|
||||
113.7284688053824,115.1642566915892,111.30268842257516,112.73847630878196,1093.2039085967226,6
|
||||
113.06855035377954,114.55977666009171,110.64086736017657,112.13209366648874,1098.544972998846,7
|
||||
111.40827542637513,112.72390874531129,109.43899848657534,110.75463180551151,1099.9573603041506,8
|
||||
108.98459460176909,109.99300155201126,107.76539185209613,108.7737988023383,1097.3847630878195,9
|
||||
106.12753789513545,107.73969337995807,104.79904459577605,106.41120008059868,1090.929742682568,10
|
||||
103.21387328427625,105.41662717550601,101.71978916733775,103.92254305856751,1080.849640381959,11
|
||||
100.61462528040111,103.00209502109563,99.18732582635697,101.57479556705148,1067.546318055115,12
|
||||
98.64575078243223,100.78188758933494,97.48620160125755,99.62233840816026,1051.5501371821465,13
|
||||
97.53034002152081,99.46748184049008,96.34710045689485,98.28424227586412,1033.4988150155905,14
|
||||
97.378063505514,99.16454670333486,95.93821562552817,97.72469882334903,1014.1120008059867,15
|
||||
98.18385394545021,99.67344281002588,96.54876504706593,98.0383539116416,1005.837414342758,16
|
||||
99.8438650794075,101.15293363552601,97.93278462060415,99.24185317672267,1025.5541102026832,17
|
||||
102.1834853863248,103.20029690993537,100.25554360082955,101.27235512444012,1044.2520443294852,18
|
||||
104.99031673022002,106.32510161131832,102.65835969292533,103.99314457402363,1061.185789094272,19
|
||||
108.04491654708718,109.54022022493461,105.7105413401633,107.20584501801073,1075.680249530793,20
|
||||
111.14367593283949,112.56654934841096,109.24526558927202,110.6681390048435,1087.157577241359,21
|
||||
114.11098793714572,115.26697281350663,112.95942875877287,114.11541363513378,1095.1602073889517,22
|
||||
116.80109288512898,118.47543335247398,115.61005717653698,117.28439764388199,1099.3691003633464,23
|
||||
119.09282467975903,121.38046215528227,117.64904116296829,119.93667863849153,1099.616460883584,24
|
||||
120.88220148856881,123.36781277048146,119.39438848583472,121.87999976774739,1095.8924274663138,25
|
||||
122.07798667229586,124.2878498649492,120.77557026109272,122.98543345374605,1088.3454655720152,26
|
||||
122.60397744514097,124.22410945235427,121.57876610123756,123.19889810845086,1077.2764487555987,27
|
||||
122.40925186267498,123.88697089091687,121.06827005264091,122.5459890808828,1063.126663787232,28
|
||||
121.48461656761053,122.98099977052848,119.63330909790388,121.12969230082183,1046.4602179413757,29
|
||||
119.8808727652764,121.29920058454442,117.70285703314954,119.12118485241757,1027.9415498198925,30
|
||||
117.72299769788953,118.8665240235534,115.60101790940676,116.74454423507063,1008.3089402817496,31
|
||||
115.21439166809358,116.4131695096543,113.05795434620948,114.2567321877702,1011.6549204850494,32
|
||||
112.6270381197495,114.07463180365934,110.47704737833695,111.92464106224679,1031.154136351338,33
|
||||
110.27641646211617,111.76231568498811,108.51535390119264,110.00125312406458,1049.4113351138608,34
|
||||
108.48360243707184,109.99872116521585,107.18792367213928,108.7030424002833,1065.6986598718788,35
|
||||
107.530320991091,109.22424173559783,106.49671695482826,108.19063769933508,1079.3667863849153,36
|
||||
107.61494922421186,109.90155645208623,106.26786689008574,108.55447411796011,1089.8708095811626,37
|
||||
108.81801012516657,111.30403713029216,107.32068773822765,109.80671474335324,1096.7919672031487,38
|
||||
111.08434919099572,113.29382811110736,109.67068524069803,111.88016416080967,1099.8543345374605,39
|
||||
114.22618875818226,115.76972371414958,113.09073586402833,114.63427081999565,1098.9358246623383,40
|
||||
117.94724565362051,119.15370942824079,116.6612183117217,117.86768208634197,1094.0730556679773,41
|
||||
121.88395973243564,123.33523703654073,119.88495316810628,121.33623047221137,1085.459890808828,42
|
||||
125.65653611891825,127.14038418498515,123.29089632531003,124.77474439137693,1073.4397097874114,43
|
||||
128.92069597346688,130.20955349568973,126.63187762484937,127.92073514707224,1058.4917192891762,44
|
||||
131.41114890560974,132.45313613345562,129.49585703767033,130.5378442655162,1041.2118485241756,45
|
||||
132.96978971477444,134.3228744433646,131.08387196585088,132.43695669444105,1022.2889914100246,46
|
||||
133.5549991814736,135.053120145851,131.99497292310176,133.49309388747918,1002.4775425453357,47
|
||||
133.2323987581558,135.065460892756,131.8235156308926,133.65657776549278,1017.4326781222982,48
|
||||
132.1510588175171,134.08480947785137,131.0237176510951,132.95746831142935,1036.647912925193,49
|
||||
130.5116755897077,132.71696973631924,129.29758425495962,131.50287840157117,1054.402111088937,50
|
||||
128.53409061924222,130.9222390672229,127.07925728332546,129.46740573130614,1069.9874687593544,51
|
||||
126.4306172006658,128.5591966352313,124.94895708842891,127.07753652299444,1082.7826469085653,52
|
||||
124.38927906587094,125.87336879788326,123.1073244542458,124.59141418625812,1092.2775421612807,53
|
||||
122.56791894474918,123.61827749324543,121.2254215875191,122.27578013601534,1098.093623006649,54
|
||||
121.09703354508844,122.45602084147426,119.02315927992295,120.38214657630877,1099.9990206550704,55
|
||||
120.08693553049852,121.58571423995242,117.62555095473105,119.12432966418496,1097.9177729151318,56
|
||||
119.63399522490424,121.03798192673773,117.25536324735009,118.65934994918358,1091.9328525664675,57
|
||||
119.82146372498268,120.94065716085713,117.95421275941922,119.07340619529367,1082.2828594968707,58
|
||||
120.7124947130592,121.93415307848436,119.152517136663,120.37417550208815,1069.3525084777123,59
|
||||
122.33587608239566,123.94838830624106,120.87761530843784,122.49012753228324,1053.6572918000436,60
|
||||
124.66785812762632,126.75625008370868,123.18852214765032,125.27691410373268,1035.8229282236828,61
|
||||
127.61552102482923,129.80523536741393,126.34054904004905,128.53026338263376,1016.5604175448309,62
|
||||
131.00775897051096,133.06294250948164,129.94904333909741,132.0042268780681,1003.3623047221139,63
|
||||
134.59892592769242,136.79793765689536,133.234137558996,135.43314928819893,1023.1509825101539,64
|
||||
138.08766685694894,140.0546941334019,136.58837142074304,138.555398697196,1042.016703682664,65
|
||||
141.1500141222941,142.5489901806554,139.73776131670974,141.13673737507105,1059.2073514707224,66
|
||||
143.48228026040917,144.5932922665057,141.8802335303824,142.99124553647894,1074.037588995245,67
|
||||
144.84649927621132,146.0756620034572,142.76876627418082,143.9979290014267,1085.9161814856498,68
|
||||
145.1098734724699,146.57143212166807,142.649968595823,144.11152724502116,1094.3695669444105,69
|
||||
144.27024859045207,145.7471249168318,141.88968005898082,143.36655638536055,1099.060735569487,70
|
||||
142.46200877506277,143.72992039092952,140.60631434563564,141.8742259615024,1099.8026652716362,71
|
||||
139.9404686061768,141.00752471999962,138.7454488027266,139.81250491654941,1096.5657776549278,72
|
||||
137.04703875358243,138.78071046739933,135.67655244818158,137.41022416199849,1089.4791172140503,73
|
||||
134.16122174198705,136.42630679247307,132.66155074344638,134.9266357939324,1078.8252067375315,74
|
||||
131.6480122345848,134.02210748888697,130.25415962109273,132.6282548753949,1065.0287840157116,75
|
||||
129.80997879551026,131.86785162975886,128.70717960530897,130.76505243955756,1048.6398688853799,76
|
||||
128.85205701193928,130.78472862032595,127.6154547134165,129.54812632180318,1030.3118356745701,77
|
||||
128.8642014864336,130.59557044803222,127.39947545719488,129.13084441879352,1010.7753652299442,78
|
||||
129.82321172551153,131.29749357237068,128.12086294416926,129.59514479102842,1009.1906850227682,79
|
||||
|
Reference in New Issue
Block a user