This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.1.29 instead.
Reason given by maintainers: superseded by 0.1.27; the fetch loop this fixed was still reachable another way
arctic-incr-cache
ArcticDB-backed time series cache with incremental updates.
First call fetches the full window from your data source and stores it in ArcticDB. Subsequent calls only fetch the gap between the cached tail and the requested end — then merge and upsert. Incomplete (still-updating) bars are automatically excluded from storage so they never overwrite finalised data.
Install
pip install arctic-incr-cache
# or
uv add arctic-incr-cache
Quick start
import datetime
import arcticdb as adb
from zoneinfo import ZoneInfo
from arctic_incr_cache import IncrCache
arctic = adb.Arctic("lmdb://data/arcticdb")
lib = arctic.get_library("ohlcv-1d", create_if_missing=True)
cache = IncrCache(
lib,
fetch=lambda symbol, end, count: your_api.get_daily_bars(symbol, end=end, count=count),
get_tz=lambda symbol: ZoneInfo("America/New_York"),
)
df = cache.get("AAPL", end=datetime.date(2024, 6, 1), count=60)
- First call — fetches 60 bars from your API, stores in ArcticDB, returns.
- Second call (same or later end) — serves from ArcticDB; fetches only the gap if the cache is stale.
Intraday data
Set bar_minutes to the bar width and provide get_tz to return the market
timezone:
from zoneinfo import ZoneInfo
intraday = IncrCache(
lib,
fetch=lambda symbol, end, count: your_api.get_minute_bars(symbol, end=end, count=count),
bar_minutes=1,
default_count=390 * 5,
get_tz=lambda symbol: ZoneInfo("America/New_York"),
)
Concurrency
By default writes run in a daemon thread. Pass spawn and lock_class for
gevent or other async runtimes:
import gevent
import gevent.lock
cache = IncrCache(
lib,
fetch=my_fetch,
get_tz=lambda symbol: ZoneInfo("America/New_York"),
spawn=gevent.spawn,
lock_class=gevent.lock.BoundedSemaphore,
)
Timezone handling
When get_tz returns a timezone for a symbol:
fetchreturn — must be tz-aware. Timestamps are converted to the configured market timezone internally.- Storage — data is stored in ArcticDB as tz-aware in the configured timezone.
- Return —
get()returns a tz-aware DataFrame in the configured timezone. endparameter —datebecomes end-of-day in market timezone; naivedatetimeis interpreted as local timezone, then converted; tz-aware is converted directly.
Interval convention
end is a bar timestamp (a point), not a range boundary.
- Filter — closed:
index <= end.fetch()must follow the same rule (start <= ts <= end); a strict<silently dropsbar@end. - Freshness:
- Daily — closed:
last.date() >= end.date(). - Intraday — right-open:
last >= end - bar_width. An intraday bar attcovers[t, t+bar_width), sobar@enddoesn't exist at session boundaries (e.g. 16:00 close, 20:00 POST end).
- Daily — closed:
A still-updating bar (now daily; within bar_width intraday) counts as one bar older for freshness — mirroring its exclusion from storage.
Continuity
The cache never invents continuity the source doesn't have — but it must not
lose bars the source does have (a partial write, a fetch that skipped the
cached tail). Mid-series hole detection is opt-in: supply is_holey and
every delivered window is put to it.
import math
import exchange_calendars as xcals
xnys = xcals.get_calendar("XNYS")
def is_holey(symbol: str, df: pd.DataFrame) -> bool:
expected = len(xnys.sessions_in_range(df.index[0].date(), df.index[-1].date()))
return expected - len(df) > max(math.ceil(expected * 0.05), 1)
cache = IncrCache(
lib,
fetch=my_fetch,
get_tz=lambda symbol: ZoneInfo("America/New_York"),
is_holey=is_holey,
)
is_holey(symbol, df)— True whendfmisses bars its source should hold over its own span,df.index[0]..df.index[-1], tz-aware in the configured timezone. Build it on a trading calendar (e.g. exchange_calendars) or a heuristic. Frames under 2 bars have no interior and never reach it. Defaults to never holey, which disables the check.
The cache keeps no tolerance of its own — how much the source may legitimately miss is a property of that source, so the whole verdict lives in your hook.
A hole has two consequences:
- Read side — a delivered window missing more bars than tolerated triggers a full re-fetch, validated the same way.
- Write side — no frame that fails the check is written, whatever path
produced it (first fetch, backfill, gap merge, repair). It is still
served; it just never reaches the store, because
updatewould delete the good rows sitting inside its gaps (see below). - The seam — a gap fetch is checked before its overlap row with the cached tail is deduplicated, so a hole between the cached tail and the first genuinely new bar is caught too. That hole lives in neither frame alone, only in the join.
A gap fetch that fails any of these is upgraded to a validated full re-fetch. One part of the gate needs no hooks at all: if the fetch window covered the cached tail but the earliest bar returned lands after it, the source skipped that tail and storing the result would fabricate a hole.
Calibrate is_holey against what the source really delivers. Call a frame
holey that the source can never improve on and every window looks corrupt:
the data is served but never cached, and the backfill floor that normally
suppresses hopeless re-fetches is never recorded — costing one full re-fetch
per result-TTL window, indefinitely. When in doubt, widen the slack or leave
is_holey at its default.
Storage semantics
Stores use ArcticDB update, which replaces the entire span between the
first and last timestamp of the stored frame: cached rows inside that span
that the new fetch doesn't contain are deleted. Every write is therefore a
potential deletion, which is why a frame failing the continuity
check is refused rather than written.
With continuity disabled (the default) there is nothing to check against:
fetch is the source of truth, and a fetch with holes (e.g. an upstream
returning partial data) erases the cached rows in those holes. Make fetch()
return complete data for the range it covers, or return empty on failure
rather than a partial result.
Index convention
fetch() must return a DataFrame with a DatetimeIndex as the index — this is
the time axis for all cache operations (querying, merging, freshness checks).
ArcticDB's date_range queries operate on the index, so no column name
configuration is needed. If your data source returns time as a regular column,
call df.set_index("date") (or similar) inside your fetch function.
Anything else — a tz-naive index, or an index that isn't a DatetimeIndex at
all — raises ValueError before the frame can be stored.
Constructor parameters
| Parameter | Required | Description |
|---|---|---|
library |
yes | ArcticDB library instance |
fetch(symbol, end, count) |
yes | Fetch raw data from upstream; must return tz-aware timestamps |
get_tz(symbol) |
yes | Market timezone (tzinfo) for each symbol |
is_holey(symbol, df) |
no | True when the frame misses bars over its own span; default False disables continuity checks |
bar_minutes |
no | Bar width in minutes (default 1440 = daily) |
default_count |
no | Bars returned when count is omitted (default 252) |
spawn |
no | Fire-and-forget callable for async writes (default: daemon thread) |
lock_class |
no | Lock constructor (default: threading.Lock) |
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arctic_incr_cache-0.1.25.tar.gz.
File metadata
- Download URL: arctic_incr_cache-0.1.25.tar.gz
- Upload date:
- Size: 79.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00bf5d49eafcd09e072f9b961bfbe7c995bc820aa520ddff68a53d52a8e4cc58
|
|
| MD5 |
e7a984dde10a863b2fdcf47161651848
|
|
| BLAKE2b-256 |
855b31b5da40a7b3be0092dbfc1e67c6738fa39e32270d3cd932a3d619f9b4d0
|
File details
Details for the file arctic_incr_cache-0.1.25-py3-none-any.whl.
File metadata
- Download URL: arctic_incr_cache-0.1.25-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ab5435fec6c578afe2caba3036d8e93c17be103f16ef3b3927d0357f94e8e1f
|
|
| MD5 |
859f2c8322d9ba3e10c95149d500911b
|
|
| BLAKE2b-256 |
685b8ebe2b4f9a30cd4ef291171c52f3a5825bb38f5207a2c662b2de7f47681a
|