Tessera

A declarative format for market strategies, an engine that backtests them, and a runtime that trades them — the same document for both. This is the working architecture reference: what is built, what is decided, and why.

Updated 2026-09-13 Decisions D1–D32 Specs 16 Phase P1 foundations Source opens with the v0 licence
Orientation

What it is

Testing a market idea shouldn't require building an engine first. Three obstacles stand in the way, and the middle one is where most attempts die: getting the data, making it usable, and expressing the strategy. Tessera addresses all three as one system.

The product

A strategy format

Declarative, composed from a small closed algebra over an open vocabulary. Every node carries a unit, so a nonsensical comparison is a validation error with a field path — not a six-hour surprise.

Open source

A backtest engine

Runs on your machine against your data. Same inputs, same version, same numbers. Coverage ships with every result; look-ahead is prevented structurally.

Same document

Live execution

What you backtested drives real orders — through a separate runtime, never the backtester's assumptions, behind a risk gate the strategy cannot bypass.

Your data

Bring your own source

Vendor files or a broker API behind one capability model. Ticks are stored, bars are derived. Conversion runs locally; the data never leaves your machine.

Foundations

The four principles everything follows from

These are not aspirations. Each one has cost a design decision somewhere in this document, and each rejects an approach that would otherwise have been easier.

  • Wrong answers are worse than no answers. A backtest that produces plausible-but-wrong numbers is the worst thing this software can do — and with live execution, that number has money attached. Errors are a closed catalogue; there is no "log it and carry on".
  • Local first. The default install runs entirely on your machine: no server, no network. Cloud is an optional topology of the same software, never a requirement. This principle alone has vetoed several otherwise-attractive designs.
  • The format is the product. The specification is public, versioned and precise. If someone writes a better engine against it, the format still won.
  • Written for agents as well as people. A stable vocabulary, a published schema, and errors that name the offending field — so a machine can author a strategy and correct itself from the rejection.
Architecture

Layers, and which way they point

The kernel sits at the bottom of the dependency graph and imports nothing above it. That is not a convention — an architecture test enforces it, failing the build on any internal import or any standard-library import outside an explicit allow-list.

Conversion P4 · adapters Evaluation P6 · P7a–c Execution live runtime CLI / API P9 Storage · Store interface P3 · files (default) | Postgres (shared) Kernel — pkg/m3 Paise · Instant · InstrumentRef · Unit · error catalogue · Coverage imports No arrow points upward — the kernel knows nothing of storage, adapters, evaluation or the API.
Dependency direction. Everything imports the kernel; the kernel imports nothing internal. Enforced by TestArch_KernelImportsNothingInternal, which fails on an injected net/http import.

The kernel holds only the types every part must agree on — money, time, instrument identity, units, the error catalogue. It deliberately contains no record layout, no adapter, no evaluator and no strategy grammar, so a change in any of those cannot ripple through the type everyone shares.

Architecture

How data moves

stored truth — permanent, archival derived — regenerable, disposable
Vendor CSV 48,226 files Normalise ₹→paise · IST→UTC Tick store Parquet · ZSTD the stored primitive Bar cache 24-byte fixed grid delete → rebuild Evaluator selection + replay read write derive · Σ per minute hot path · µs intrabar · tick mode Ticks are written once and never edited. Every bar is a pure function of them.
The write path runs left to right; bars fall out of the tick store as a cached aggregation. The evaluator rides the cache for speed and drops to ticks when it needs sub-bar truth — a basket-scoped stop, or a tick-fidelity run.
Storage

Why there are two formats

Backtesting has two access patterns that pull in opposite directions, and this is the single fact the storage design turns on. The system being replaced kept two stores for exactly this reason — that was not accidental duplication, it was the patterns asserting themselves.

time → 375 minutes in a session contracts ~800 SELECTION one instant, every contract REPLAY one contract, every instant Served by the bar cache — O(1) computed offset, ~800 seeks in one open file. Served by the tick store — one contiguous run, exactly what Parquet is best at.
The same data, read two incompatible ways. A column is a snapshot; a row is a series. No single layout is good at both, which is why the primitive and the cache take opposite formats rather than compromising on one.

That split is measured, not assumed. On one instrument-day of real data (800 contracts, 2,018,361 ticks), an mmap'd fixed-width binary read a single contract in 7 µs against Parquet's 2.36 ms — but Parquet compressed to 2.37 bytes/row against the raw record's 16, and beat a purpose-built per-contract ZSTD binary by 2.4×.

The decision that follows

Ticks take Parquet because the archival layer is footprint-dominant and must open in DuckDB, polars or pandas with no tooling of ours. The bar cache keeps a custom binary because point access is Parquet's weakness — and because the cache is regenerable, its format is an internal detail we can change without a compatibility promise.

Storage

The fixed grid

The bar cache stores no timestamps. Position is the timestamp: record i is minute i from the session open, so finding any bar is arithmetic rather than a search.

09:1509:1609:17 14:2315:2815:29 · · · · · · 012 308 373374 slot 0 7392 8976 byte final slot is closed at both ends — [15:29:00, 15:30:00] holds the closing print The entire lookup idx = 14:23 − 09:15 = 308 offset = 308 × 24 = 7392 ReadAt(buf[24], 7392) one seek · no index · no scan 375 slots × 24 bytes = 9,000 bytes per contract-day. An untraded minute is INT32_MIN — which is not the same as "no data".
Addressing is a multiplication. The closed-closed final slot is not a detail: NSE prints a closing trade at exactly 15:30:00, and a half-open last bar would either spill it into a 376th slot — breaking the arithmetic — or silently drop the closing mark.
Storage

The two record shapes

Confusing these is easy, so the distinction is worth stating plainly: one is truth, the other is a convenience that can always be rebuilt.

Tick — the primitiveCache record — derived
What it isone actual printone 1-minute aggregation
Is it truth?yes — archivalno — recomputable
FormatParquet, open24-byte binary, internal
Timestampstored, plus seqnot stored — position is time
Widthvariable, compressedfixed 24 bytes
Accesssequential scanO(1) computed offset
If deleteddata lossregenerate from ticks

Money is an integer count of minor units throughout, never a float; the scale is venue data and is enforced to be a power of ten. Timestamps are parsed against an explicit venue location and stored as absolute UTC, so a run cannot depend on the host's clock settings.

message tessera_tick {                          struct CacheBar {   // 24 bytes, LE
  required int32  contract_id;   // dict+RLE      int32 open, high, low, close;
  required int64  ts (TIMESTAMP(MICROS,utc));     int32 volume;
  required int32  seq;           // RLE           int32 oi;
  required int64  ltp_paise;                    }
  required int64  ltq;
  required int64  oi;                           // empty minute = INT32_MIN
}                                               // offset = start + idx*24
Why seq exists

The vendor timestamps to the whole second and prints many ticks within one. Rather than fabricate microseconds we don't have, seq is the ordinal within (contract, second) — so (ts, seq) is a total order that survives a sort in any external tool and reproduces byte-for-byte.

Storage

One contract, all the way through

Real rows from the sample — a liquid at-the-money weekly call on 2022-10-03 — at each stage. Note the first four ticks share a second, which is exactly the case seq exists for.

filename   NIFTY22100617100CE.csv
key        OPT:NIFTY:20221006:W:CE:1710000

raw        20221003,09:15:00,132.65,1,2967350
           20221003,09:15:00,132.65,1,2967350
           20221003,09:15:00,93.85,1,2967350
           20221003,09:15:01,101.70,11700,2967350

tick       contract_id  ts (UTC)              seq   ltp_paise     ltq        oi
           4711         2022-10-03T03:45:00Z    0       13265       1   2967350
           4711         2022-10-03T03:45:00Z    1       13265       1   2967350
           4711         2022-10-03T03:45:00Z    2        9385       1   2967350
           4711         2022-10-03T03:45:01Z    0       10170   11700   2967350

bar        09:15  open 13265  high 13275  low 9000  close 13160  vol 1461803
           15:29  open  4640  high  4650  low 4560  close  4560  vol  442700

bytes      d1 33 00 00  db 33 00 00  28 23 00 00  68 33 00 00  2b 4e 16 00  36 47 2d 00
           open=13265   high=13275   low=9000     close=13160  vol=1461803  oi=2967350

result     21,843 ticks → 375 one-minute bars → 9,000 bytes

The index spot series is not a special case. A vendor's BANKNIFTY.csv is the underlying series, so it becomes EQ:BANKNIFTY and lives in the same file and the same tables as that day's contracts, with volume and open interest at zero because an index has neither. There is deliberately no separate index asset class.

Correctness

Four classes, and no fifth

Every failure is classified, and a condition that fits none of the first three fails loudly. There is no "log it and continue" — that habit is what let the system being replaced return a result whether all, some or none of its work succeeded.

ClassWhenEffectThe trap it closes
Rejectvalidation, before worknothing runsCarries a field path in schema-validator shape, so an agent that wrote the document can correct itself.
Allowper unit, during workrecord a hole, continueMust not fail the job — otherwise a non-trading day is retried for weeks.
Warnwork succeededsurface, continueA caveat is not a failure and must not affect completeness.
Defectanywherefail loudlyNo value-shaped way to express it, so it cannot be quietly downgraded.

Partial success is unrepresentable as success. A function that can partly fail returns a result carrying its holes, each naming which unit failed — never a bare slice with a nil error. The check is called Complete() rather than OK() precisely so it is never read as a nil-error test.

Coverage is a first-class output of every run, and counts correct non-entry separately from failure. A selective strategy that enters 38 times across five years produces thousands of no-trigger candidates; folding those into holes would report a 97% failure rate for a system working exactly as intended.

Correctness

Reproducibility is a property, not an aspiration

Same document, same data, same engine version means identical numbers on any machine. Several rules exist only to hold that line:

  • Money is integer minor units; rates and percentages are exact rationals. One function rounds, half-to-even, in 128-bit intermediate precision. No float64 appears anywhere on the money path.
  • Timestamps are parsed against an explicit venue location — never the host zone — and an architecture test fails the build if any source file calls the timezone-defaulting parse function.
  • Anything rendered from a map sorts its keys first, so iteration order cannot leak into a report and make two runs differ.
  • Ticks are written once and never edited. Bars are re-derived from them, never patched.
Project

Build status

built & tested   in progress   specified, no code   decision open

  • P1 foundations Money, time, instrument taxonomy, units and the error catalogue are built and tested — 9 of 14 acceptance criteria. Identity and config remain; the provider seam is deferred to P4 by decision.
  • P2 venue & catalog A minimal calendar slice is scheduled before P4: expiry-bearing instruments cannot be constructed without it.
  • P3 storage Records, file store and catalog schema specified. The tick section still describes the pre-D30 binary format and needs reconciling to Parquet.
  • P4 conversion Adapter specified. Test data extracted and verified: 48,226 files across four trading days.
  • P5–P10 Strategy format, resolution, evaluation, reporting and CLI all have written specs with acceptance criteria; no code yet.
  • Licence Open-core split agreed — Apache for the format and SDK, AGPL plus CLA for the engine. Pending qualified legal sign-off.
  • Source repository Not public yet. The design, the decision log and the specs are published here; the repository opens once the licence above is settled, so that what is opened is opened under the right terms rather than retroactively.
  • Regulatory posture Blocking for live execution. Needs counsel, not inference.
Verified against the sample, not assumed

Futures carry only a month in the filename, and options carry a date but not whether it is the weekly or the monthly rung — so a venue calendar gates ingest of every expiry-bearing instrument. Separately, all 546 files in the vendor's continuous-futures folder are byte-identical duplicates of contract files; ingesting both would double-count every future.

Project

Decision log

Every decision is recorded with its reasoning and, where one was overturned, what replaced it. Reversals are kept rather than edited away.

#DecisionStatus
D1Strategies are a cross-product, not a listsettled
D2ATM is not a primitivesettled
D3Inter-leg references are in scopesettled
D4Units close the selector spacesettled
D5Resolution returns a typed contract, not a pathsettled
D6Errors are a closed catalogue with four classessettled
D7ipjson is provenancesettled
D8Go, with sum types via protobuf oneofreversed → D14
D9Sidekiq-style job modelsuperseded → D13
D10Closed algebra, open vocabularysettled
D11Windows, and look-ahead prevented structurallysettled
D12Entry triggerssettled
D13The engine is local; the queue mostly leaves itsettled
D14Authoring format is JSON/YAML with JSON Schemasettled
D15Local conversion largely defuses redistribution risknoted
D16Two storage modes behind one interfacesettled
D17Fixed time grid with implicit timestampssettled via D29/D30
D18Instrument taxonomy as a sum typesettled
D19Venue behaviour is datasettled
D20One artifact, three deployment topologiessettled
D21Remote data sources are read-only and TLS-onsettled
D22Planning phase, documents onlysettled
D23Live execution via broker APIsettled
D24The six unread services have been readsettled
D25Evaluation semantics are a v0 concernsettled
D26Contribution and working-process rules are written downsettled
D27Data sources are a Provider capability modelsettled
D28Canonical format + Postgres-mode schemapartly superseded → D29
D29Ticks are the primitive; bars are derived, cachedsettled
D30Ticks as Parquet; bar cache as fixed-grid binarysettled
D31Open-core split, open adapters, open live runtimelicence pending legal
D32Transaction-cost and settlement figures verifiedsettled
Project

Roadmap

The risk is not difficulty, it is never finishing anything end to end. So v0 is one of everything, proven all the way through before any axis widens.

VersionWhat it adds
v0The vertical slice: strategy format, tick storage with derived bars, one source, evaluator, CLI. Reproduce a real strategy byte-for-byte on two machines.
v0.1Observables, windows and entry triggers.
v0.2A second source adapter, and the first API source.
v0.3Postgres backend behind the same Store interface.
v0.4–0.5Compose, an API, and a web UI.
v0.6–0.7Equities and corporate actions; a second venue.
v0.8Remote sources.
v0.9Live execution: risk gate, idempotency, reconciliation, kill switch, paper-first.
v1.0An optional cloud topology of the same engine.
Project

What is still open

Recorded rather than resolved, because a written unknown is safer than an assumed answer.

  • Are raw ticks actually cold? Keeping Parquet for ticks rests on the evaluator riding the bar cache. If real strategies replay raw ticks heavily, the latency gap matters and a custom binary becomes competitive again. Needs a profile of a real run.
  • Licence. The open-core split is agreed in shape; the exact terms need qualified legal advice before release.
  • Regulatory posture for algorithmic trading. Blocking for live execution. It governs who may run it and what compliance is required — not whether the code is open.
  • The catalog locator against Parquet. Byte offsets are a contiguous-extent idiom that maps awkwardly onto row groups; the addressing needs pinning down before the first tick file is written.
  • Trigger timing. Evaluate the partial candle, or trigger on close and enter at the next open. Different strategies, different results.