diff --git a/CHANGELOG.md b/CHANGELOG.md index 619792cc..b459e893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bindings/c/include/wickra.h b/bindings/c/include/wickra.h index 22f8e152..aa43950c 100644 --- a/bindings/c/include/wickra.h +++ b/bindings/c/include/wickra.h @@ -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 diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index ec1d9fb1..fd3beafa 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -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, + 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::*; diff --git a/bindings/csharp/Wickra.Tests/DataLayerTests.cs b/bindings/csharp/Wickra.Tests/DataLayerTests.cs index 33588a11..f44e087e 100644 --- a/bindings/csharp/Wickra.Tests/DataLayerTests.cs +++ b/bindings/csharp/Wickra.Tests/DataLayerTests.cs @@ -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")] diff --git a/bindings/csharp/Wickra/Generated/Indicators.g.cs b/bindings/csharp/Wickra/Generated/Indicators.g.cs index 340abcdc..b836d873 100644 --- a/bindings/csharp/Wickra/Generated/Indicators.g.cs +++ b/bindings/csharp/Wickra/Generated/Indicators.g.cs @@ -4981,6 +4981,60 @@ public sealed class Camarilla : IDisposable public void Dispose() => _handle.Dispose(); } +public sealed class CandleReader : IDisposable +{ + private readonly WickraHandle _handle; + + /// Parse a timestamp,open,high,low,close,volume CSV string (a + /// leading UTF-8 BOM and field whitespace are tolerated). + 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); + } + + /// Every candle parsed from the CSV, in file order. + public Candle[] Read() + { + var count = (long)NativeMethods.wickra_candle_reader_count(_handle.DangerousGetHandle()); + GC.KeepAlive(_handle); + if (count <= 0) + { + return Array.Empty(); + } + 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; diff --git a/bindings/csharp/Wickra/Generated/NativeMethods.g.cs b/bindings/csharp/Wickra/Generated/NativeMethods.g.cs index d2972d3e..c5419bad 100644 --- a/bindings/csharp/Wickra/Generated/NativeMethods.g.cs +++ b/bindings/csharp/Wickra/Generated/NativeMethods.g.cs @@ -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)] diff --git a/bindings/go/data_layer_test.go b/bindings/go/data_layer_test.go index 22505a22..e8131f3e 100644 --- a/bindings/go/data_layer_test.go +++ b/bindings/go/data_layer_test.go @@ -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 { diff --git a/bindings/go/include/wickra.h b/bindings/go/include/wickra.h index 22f8e152..aa43950c 100644 --- a/bindings/go/include/wickra.h +++ b/bindings/go/include/wickra.h @@ -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 diff --git a/bindings/go/indicators_gen.go b/bindings/go/indicators_gen.go index cca87df2..4a624250 100644 --- a/bindings/go/indicators_gen.go +++ b/bindings/go/indicators_gen.go @@ -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 diff --git a/bindings/java/src/main/java/org/wickra/CandleReader.java b/bindings/java/src/main/java/org/wickra/CandleReader.java new file mode 100644 index 00000000..6e0485ba --- /dev/null +++ b/bindings/java/src/main/java/org/wickra/CandleReader.java @@ -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(); + } +} 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 5f7475f9..d7596c1d 100644 --- a/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java +++ b/bindings/java/src/main/java/org/wickra/internal/NativeMethods.java @@ -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)); } } diff --git a/bindings/java/src/test/java/org/wickra/DataLayerTest.java b/bindings/java/src/test/java/org/wickra/DataLayerTest.java index 0d7344dd..303a0966 100644 --- a/bindings/java/src/test/java/org/wickra/DataLayerTest.java +++ b/bindings/java/src/test/java/org/wickra/DataLayerTest.java @@ -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"); diff --git a/bindings/node/__tests__/completeness.test.js b/bindings/node/__tests__/completeness.test.js index 65724767..91bee16e 100644 --- a/bindings/node/__tests__/completeness.test.js +++ b/bindings/node/__tests__/completeness.test.js @@ -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 diff --git a/bindings/node/__tests__/data_layer.test.js b/bindings/node/__tests__/data_layer.test.js index 1ff7151d..583d7060 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, 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() { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 235db139..5c3310a5 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -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 +} diff --git a/bindings/node/index.js b/bindings/node/index.js index 8fc2ee43..4101f540 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, Resampler } = 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, CandleReader } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -829,3 +829,4 @@ module.exports.BetterVolume = BetterVolume module.exports.VolumeWeightedMacd = VolumeWeightedMacd module.exports.TickAggregator = TickAggregator module.exports.Resampler = Resampler +module.exports.CandleReader = CandleReader diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 9896ab07..a57677bd 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -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, +} + +#[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 { + 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 { + self.candles.iter().map(|&c| candle_to_value(c)).collect() + } +} diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index dac093a2..fc4cf8a5 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -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", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index f7fb5961..bf447b2a 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -28288,6 +28288,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Data layer. m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Candlestick patterns. m.add_class::()?; m.add_class::()?; @@ -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, +} + +#[pymethods] +impl PyCandleReader { + #[new] + fn new(csv: &str) -> PyResult { + 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 { + self.candles + .iter() + .map(|c| (c.open, c.high, c.low, c.close, c.volume, c.timestamp)) + .collect() + } +} diff --git a/bindings/python/tests/test_data_layer.py b/bindings/python/tests/test_data_layer.py index 8f58c7c2..d5fa26e8 100644 --- a/bindings/python/tests/test_data_layer.py +++ b/bindings/python/tests/test_data_layer.py @@ -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) diff --git a/bindings/r/NAMESPACE b/bindings/r/NAMESPACE index 69852c09..294cbc26 100644 --- a/bindings/r/NAMESPACE +++ b/bindings/r/NAMESPACE @@ -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) diff --git a/bindings/r/R/indicators.R b/bindings/r/R/indicators.R index 4124be32..d47ecfc7 100644 --- a/bindings/r/R/indicators.R +++ b/bindings/r/R/indicators.R @@ -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 diff --git a/bindings/r/R/methods.R b/bindings/r/R/methods.R index bc7eecff..045f6719 100644 --- a/bindings/r/R/methods.R +++ b/bindings/r/R/methods.R @@ -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 +} diff --git a/bindings/r/src/wickra.c b/bindings/r/src/wickra.c index 2a2bd80a..daebb82d 100644 --- a/bindings/r/src/wickra.c +++ b/bindings/r/src/wickra.c @@ -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}, diff --git a/bindings/r/tests/testthat/test-data-layer.R b/bindings/r/tests/testthat/test-data-layer.R index d36303e2..40b39a24 100644 --- a/bindings/r/tests/testthat/test-data-layer.R +++ b/bindings/r/tests/testthat/test-data-layer.R @@ -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") diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index bb1821dc..1a7dc4aa 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -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, +} + +#[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 { + 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 + } +} diff --git a/bindings/wasm/tests/data_layer.test.js b/bindings/wasm/tests/data_layer.test.js index 4656c6a3..6ba2d198 100644 --- a/bindings/wasm/tests/data_layer.test.js +++ b/bindings/wasm/tests/data_layer.test.js @@ -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() { diff --git a/examples/c/data_layer_test.c b/examples/c/data_layer_test.c index e66a2d20..414b3f55 100644 --- a/examples/c/data_layer_test.c +++ b/examples/c/data_layer_test.c @@ -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); } diff --git a/examples/rust/src/bin/gen_golden.rs b/examples/rust/src/bin/gen_golden.rs index 5591118b..810d43d4 100644 --- a/examples/rust/src/bin/gen_golden.rs +++ b/examples/rust/src/bin/gen_golden.rs @@ -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 = 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]) { diff --git a/testdata/golden/data_csv.csv b/testdata/golden/data_csv.csv new file mode 100644 index 00000000..c4ffe3e3 --- /dev/null +++ b/testdata/golden/data_csv.csv @@ -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 diff --git a/testdata/golden/data_csv_candles.csv b/testdata/golden/data_csv_candles.csv new file mode 100644 index 00000000..e3a7c571 --- /dev/null +++ b/testdata/golden/data_csv_candles.csv @@ -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