Brummel 0f5e6655b9 Replace flaky concurrent-access test with deterministic equivalent
test_real_concurrent_access depended on /mnt data (skipped otherwise)
and asserted equal counts over a partial 10-chunk read, which thread
scheduling could make uneven. The SymbolGuard ownership model closes
the underlying races, so concurrent consumers reading the same symbol
to completion read an identical, fixed record set regardless of
scheduling.

Replace it with a self-contained inline test: eight threads each fully
drain a stream over temp files and must read identical counts. Runs
everywhere with no /mnt dependency; verified deterministic across 30
consecutive runs.
2026-06-04 17:51:14 +02:00

data-server

High-performance, thread-safe loader and cache for binary market data files (Pepperstone M1 / tick format). Each file is a ZIP archive containing one .bin entry of tightly packed records; it is read and parsed exactly once, then shared lock-free across many readers via Arc<[T]> chunks.

Standalone leaf crate — depends only on chrono, regex, zip.

Add as dependency

data-server = { git = "http://192.168.178.103:3000/Brummel/data-server.git", branch = "main" }

Interface

Entry points

Item Description
DataServer::new(base_path) -> DataServer Scan a directory and build the symbol index. Wrap in Arc to stream (see below).
init_data_server(base_path) -> Arc<DataServer> Initialize the global singleton. Panics if called twice.
init_default_data_server() Initialize the global from DEFAULT_DATA_PATH if that dir exists. Idempotent / safe to call repeatedly.
get_data_server() -> Option<Arc<DataServer>> The global instance, or None if not initialized.
DEFAULT_DATA_PATH: &str "/mnt/tickdata/Pepperstone".

Querying & streaming (DataServer)

Method Returns
has_symbol(symbol) -> bool Whether the symbol has any files.
symbols() -> Vec<Arc<str>> All known symbols, sorted.
file_count(symbol, DataFormat) -> Option<usize> Number of files for that symbol/format.
stream_m1(symbol) / stream_tick(symbol) Option<SymbolChunkIter<_>> over all data.
stream_m1_windowed(symbol, from_ms, to_ms) / stream_tick_windowed(...) Same, restricted to an inclusive [from_ms, to_ms] Unix-millisecond window (Option<i64> bounds; files outside are skipped without I/O).

The stream_* methods take self: &Arc<Self> (background prefetch needs a shared owner), so the server must be an Arc<DataServer>. They return None if the symbol is unknown (windowed: also if no file falls in the window).

Consuming chunks (SymbolChunkIter<T>)

next_chunk(&mut self) -> Option<Arc<[T]>> yields successive chunks (≤ CHUNK_SIZE = 1024 records) in chronological order, prefetching the next file in the background. This is an inherent method, not the Iterator trait — drive it with a while let loop. While the iterator is alive the symbol's data is pinned in the cache; on Drop the symbol is released and, if no other consumer holds it, evicted.

Record types (data_server::records)

  • M1Parsed { time_ms: i64, open, high, low, close, spread: f64, volume: i64 }
  • TickParsed { time_ms: i64, ask: f64, bid: f64 }
  • enum DataFormat { M1, Tick }
  • trait HasTimestamp { fn time_ms(&self) -> i64; } (implemented by both parsed types)
  • delphi_to_unix_ms(f64) -> i64, unix_ms_to_year_month(i64) -> (u16, u8)
  • RawM1Record / RawTickRecord: the on-disk #[repr(C, packed)] layouts, plus M1Parsed::from_raw / TickParsed::from_raw.

All timestamps are Unix milliseconds (converted from the Delphi TDateTime epoch on load).

Lower-level modules

  • data_server::loaderload_m1_file / load_tick_file (ZIP → parsed chunks), CHUNK_SIZE.
  • data_server::cacheFileCache, FileKey, ChunkVec<T>. The many-readers / rare-writer cache used internally; rarely needed directly.

Example

use std::sync::Arc;
use data_server::DataServer;

let server = Arc::new(DataServer::new("/mnt/tickdata/Pepperstone"));

if server.has_symbol("EURUSD") {
    let mut it = server
        .stream_m1_windowed("EURUSD", Some(from_ms), Some(to_ms))
        .expect("symbol has data in window");

    while let Some(chunk) = it.next_chunk() {
        for r in chunk.iter() {
            // r: M1Parsed — r.time_ms, r.open, r.high, r.low, r.close, ...
        }
    }
}

Concurrency

Designed to back hundreds of single-threaded VMs in parallel: DataServer is Send + Sync, shared as Arc<DataServer>. Each file loads once; concurrent requests for the same file block on a Condvar rather than loading twice.

Data files

Files must be named SYMBOL_YYYY_MM.m1 or SYMBOL_YYYY_MM.tick (the symbol may contain dots, e.g. AAPL.US_2006_08.m1) and be ZIP archives containing a single *.bin entry of packed records.

S
Description
No description provided
Readme 107 KiB
Languages
Rust 100%