# `ZenQuant.Recorder.JSONL`
[🔗](https://github.com/ZenHive/zen_quant/blob/v0.8.1/lib/zen_quant/recorder/jsonl.ex#L1)

JSONL snapshot recorder for trading data.

Writes timestamped snapshots (tickers, OI, greeks, walls) as one JSON object
per line for replay and backtesting analysis.

## Entry Format

    {"ts":"2026-02-23T12:00:00Z","type":"ticker","data":{"symbol":"BTC/USDT","last":95000.5}}
    {"ts":"2026-02-23T12:00:00.123456Z","type":"greeks","data":{"delta":0.55}}

- `ts` — ISO8601 timestamp
- `type` — optional snapshot label (omitted when not provided)
- `data` — the payload (any JSON-serializable term)

## Usage

    # Write snapshots
    JSONL.append("/tmp/session.jsonl", %{symbol: "BTC/USDT", last: 95000.5}, type: :ticker)
    JSONL.append("/tmp/session.jsonl", %{delta: 0.55}, type: :greeks)

    # Read back
    {:ok, entries} = JSONL.read_all("/tmp/session.jsonl")

    # Stream lazily
    "/tmp/session.jsonl"
    |> JSONL.stream()
    |> Stream.filter(&match?({:ok, _}, &1))
    |> Enum.each(fn {:ok, entry} -> process(entry) end)

    # File metadata
    {:ok, info} = JSONL.info("/tmp/session.jsonl")
    # => %{count: 2, first_ts: ~U[...], last_ts: ~U[...], duration_ms: 123, types: %{"ticker" => 1, "greeks" => 1}}

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `info` | 1 | Return metadata about a JSONL file (count, timestamps, duration, type frequencies). | `path: value` |
| `read_all` | 1 | Read all valid entries from a JSONL file (skips malformed lines). | `path: value` |
| `stream` | 1 | Stream entries lazily from a JSONL file. | `path: value` |
| `write_many` | 2 | Write multiple entries to a JSONL file in a single operation. | `path: value`, `entries: value` |
| `append` | 3 | Append a single entry to a JSONL file. | `path: value`, `data: value` |
| `decode_entry` | 1 | Decode a JSONL line string into an entry map with atom keys. | `line: value` |
| `encode_entry` | 2 | Encode a data payload into a JSONL line string. | `data: value` |

# `append`

```elixir
@spec append(Path.t(), term(), keyword()) :: :ok | {:error, term()}
```

Appends a single entry to a JSONL file.

Creates the file and parent directories if they don't exist.

## Options

Same as `encode_entry/2`: `type:` and `ts:`.

## Examples

    :ok = JSONL.append("/tmp/session.jsonl", %{last: 95000.5}, type: :ticker)

# `decode_entry`

```elixir
@spec decode_entry(String.t()) :: {:ok, map()} | {:error, term()}
```

Decodes a JSONL line string into an entry map with atom keys.

Known keys (`:ts`, `:type`, `:data`) are converted to atoms.
The `:ts` value is parsed back to a `DateTime`.

## Examples

    {:ok, entry} = JSONL.decode_entry(~s({"ts":"2026-02-23T12:00:00Z","type":"ticker","data":{"last":95000.5}}))
    entry.type
    # => "ticker"
    entry.ts
    # => ~U[2026-02-23 12:00:00Z]

# `encode_entry`

```elixir
@spec encode_entry(
  term(),
  keyword()
) :: String.t()
```

Encodes a data payload into a JSONL line string.

Returns a single-line JSON string (no trailing newline).

## Options

- `type:` — atom label (`:ticker`, `:orderbook`, etc.), converted to string
- `ts:` — DateTime override (default: `DateTime.utc_now()`)

## Examples

    entry = JSONL.encode_entry(%{symbol: "BTC/USDT", last: 95000.5}, type: :ticker)
    # => ~s({"ts":"2026-02-23T12:00:00Z","type":"ticker","data":{"symbol":"BTC/USDT","last":95000.5}})

    entry = JSONL.encode_entry(%{delta: 0.55})
    # => ~s({"ts":"...","data":{"delta":0.55}})

# `info`

```elixir
@spec info(Path.t()) :: {:ok, map()} | {:error, term()}
```

Returns metadata about a JSONL file.

## Return Value

    %{
      count: integer(),
      first_ts: DateTime.t() | nil,
      last_ts: DateTime.t() | nil,
      duration_ms: integer(),
      types: %{String.t() => pos_integer()}
    }

## Examples

    {:ok, info} = JSONL.info("/tmp/session.jsonl")
    info.count
    # => 150
    info.types
    # => %{"ticker" => 100, "greeks" => 50}

# `read_all`

```elixir
@spec read_all(Path.t()) :: {:ok, [map()]} | {:error, term()}
```

Reads all valid entries from a JSONL file.

Malformed lines are skipped silently. Returns only successfully decoded entries.
For strict parsing, use `stream/1` and handle errors explicitly.

## Examples

    {:ok, entries} = JSONL.read_all("/tmp/session.jsonl")

# `stream`

```elixir
@spec stream(Path.t()) :: Enumerable.t()
```

Streams entries lazily from a JSONL file.

Each element is `{:ok, entry_map}` or `{:error, reason}`.
Raises `File.Error` if the file doesn't exist (streaming a nonexistent file
is a programming error).

Blank lines are skipped. Malformed lines yield `{:error, reason}`.

## Examples

    "/tmp/session.jsonl"
    |> JSONL.stream()
    |> Stream.filter(&match?({:ok, _}, &1))
    |> Enum.each(fn {:ok, entry} -> process(entry) end)

# `write_many`

```elixir
@spec write_many(Path.t(), [{term(), keyword()}]) :: :ok | {:error, term()}
```

Writes multiple entries to a JSONL file.

Creates the file and parent directories if they don't exist.
Each entry is a tuple of `{data, opts}` matching `encode_entry/2` args.

## Examples

    entries = [
      {%{last: 95000.5}, type: :ticker},
      {%{delta: 0.55}, type: :greeks}
    ]
    :ok = JSONL.write_many("/tmp/session.jsonl", entries)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
