Data and numerical conventions
These are the rules every module follows. Where a function deviates, its own documentation says so.
Frames and time
The native fincore::dataframe is a hmdf::StdDataFrame whose index is a signed
64-bit UTC nanosecond timestamp. The representable range is therefore
1677-09-21T00:12:43.145224192Z to 2262-04-11T23:47:16.854775807Z; a calendar
value outside it raises input_error rather than wrapping. Sub-day precision is
preserved exactly, and utc_year floors toward earlier time — one nanosecond
before the epoch is year 1969, not 1970.
Index requirements belong to each API, and they differ on purpose:
Requirement |
Where it appears |
|---|---|
Strictly ascending, duplicate-free |
Risk estimation, backtests and walk-forward validation — a duplicated or unsorted index raises |
Sortedness not required, rows positional |
Simulation paths and analysis inputs — the caller’s row order is preserved and the index is copied as-is |
Uniqueness by adjacency |
Attribution alignment, where a sorted index is an unstated premise (recorded as F10-D11) |
Do not assume that a function sorts or deduplicates for you. If an API needs a particular order, it validates it and says so.
Returns, units and annualization
Return series are decimal simple returns unless a function states otherwise:
0.01 means one percent. daily_annualization is 252. Ratios that scale by
sqrt(annualization) use the factor directly; annualization_factor(frequency)
maps daily/weekly/monthly/quarterly/yearly to 252/52/12/4/1 and returns
an explicit override untouched — including zero or a negative value, because
validation belongs to the caller.
Weights, prices and amounts carry their call-site units and are never converted implicitly. A function that expects basis points says so.
Missing values and alignment
NaN is the native missing numeric marker; it is not a small number and it is
not zero. Three behaviours coexist and each function documents which one it uses:
Drop the observation (pairwise where two series are involved). Most scalar risk and return metrics do this.
Reject non-finite input entirely. Stress testing and some risk kernels refuse a
NaNreturn rather than skipping it.Propagate: a window that touches a missing value yields
NaNfor that window instead of a partial statistic.
Alignment is equally explicit: strict equality, common index, inner join, or a documented outer policy. A missing label is never silently replaced with zero.
When loading columns, hmdf::nan_policy::dont_pad_with_nans keeps the value as
given; padding policies insert NaN for absent positions. Pick the one that
matches the frame’s own contract, and remember that an empty result is often
returned as an empty typed column, not as a missing column.
Ownership and lifetime
make_seriesandload_columncopy or take ownership; a frame returned by a function owns its values.validate_returnsreturns a copy, so writing through the result never touches the input.portfolio_inputscopies at ingestion, andmaterialize()returns fresh frames on every call.Some accessors return views (a column span, a
std::string_view). A view is valid only while its frame is alive and unmodified.analysis_contextcaches computed metrics, which makes one instance single-threaded by construction. Share frames freely; share a context only behind your own lock.
Determinism and digests
Results that need to be compared, stored or replayed carry a digest instead of raw floats:
Runtime digests length-prefix every field and tag each value with its declared type, so neither separators nor a
bool-versus-1collision can make two different payloads hash the same.Float values are serialized as lowercase IEEE-754 bit patterns, so
NaNpayloads and signed zeros survive a round trip. Some writers normalize negative zero to positive zero because the bytes are hashed.A digest that covers inputs answers “same inputs?”; one that covers outputs answers “same result?”. The factor model fingerprint hashes inputs, so two runs agree before their numbers are compared.
Determinism is per function, not global: a seeded simulation reproduces a stream inside this library, and only functions that document a NumPy mapping promise the same numbers as the reference implementation.
Errors
All library errors derive from fincore_error and carry a category, a code and
structured fields:
Situation |
Type |
|---|---|
Bad argument, missing or mis-typed column, embedded NUL, invalid index |
|
Malformed factor panel or configuration |
|
Domain validation in metrics (for example an out-of-range cutoff) |
returns |
Out-of-range values inside a well-formed frame are usually answered with NaN
(for example a cutoff outside (0, 1)), while a structural problem throws. Tests
compare rejection messages exactly, so two different diagnoses cannot collapse
into one passing assertion.
Reports and files
A report document has its own schema version, separate from the runtime serialization envelope and from artifact-bundle manifests. Writers reject unsafe destinations — an empty path, a directory that cannot be created, a pre-existing file where the contract forbids one — and clean up partial output where they say they do. CSV is an exchange format; it does not carry the provenance of a result model, which is what the digest and the JSON manifest are for.
Frequencies and labels
Input period names are canonical words; letter aliases such as ME, QE, YE
are produced on output so a converted frequency means the same boundary in a
modern date range. Label ordering is part of the contract: weekly aggregation
orders rows by calendar year and ISO week label rather than by bucket start,
which can place a January observation after a December row of the same year.