Date: 2026-06-25 (rev. 2026-07-03) Status: Draft — for review Author: (you)
Replace
the
single
once-daily
funding
settlement
on
a
hand-picked
subset
of
US stock
perps
with
13
intraday
fundings
—
every
30
minutes
from
10:00
to
16:00 US/Eastern
inclusive.
Each
funding
is
1/slot_count
(1/13
on
a
full
day)
of
the usual
premium
formula,
computed from
a
30-minute
full-interval
TWAP
of
our
perp
mark
against
a
30-minute
TWAP
of the
Databento
consolidated
feed,
and
capped
at
±8
bps
per
interval.
Ship
a daily
funding
bar
in
the
GUI
plus
an
API
surface
that
exposes
the
day's
13 slots
(realized
+
projected).
Goal:
make
funding
predictable
and
tractable
— small,
frequent,
bounded
increments
instead
of
one
opaque
end-of-day
charge.
This is an experiment, scoped to a designated set of instruments, fully reversible by flipping those instruments back to the existing daily schedule.
US equity perps settle funding once per day at 16:00 ET. The mechanics (mapped in detail during research):
instruments.funding_schedule
(JSONB, FundingRateSchedule),
rs/sdk/src/types/funding_rate_schedule.rs:11.
The
struct already
holds
a
Vec<FundingTime>
and
next_funding_time()
already
iterates all
of
a
day's
times
—
multiple
fundings
per
day
is
a
config
change,
not
a
new primitive.
EquitiesRunner (rs/settlement-engine/src/equities.rs:59),
cron "0 15 16 * * Mon-Fri (America/New_York)"
—
fires
at
16:15
ET
once
Databento publishes
the
official
close,
settles
at
the
16:00
ET
mark.
rs/sdk-internal/funding-and-settlement/src/funding.rs:66): funding_amount = clamp(settlement_price − benchmark_price, [min_cap, max_cap]), funding_rate = funding_amount / benchmark_price,
where
caps
come
from instruments.funding_rate_cap_upper_pct
/
funding_rate_cap_lower_pct.
settlement_price
=
mark
=
simple
mean
of
per-minute
marks
over
a
trailing 5-min
MarkWindow
(compute_settlement_price,
common.rs:149; rs/sdk-internal/funding-and-settlement/src/mark_window.rs).
Each
per-minute mark
(ChMarketSnapshotRow::mark_price,
from
the
1-min
market_snapshots written
by
marketdata-publisher)
is
a
0.3 × book mid + 0.7 × that-minute trade VWAP
blend
when
both
exist;
mid
alone
if
the
minute
had
no
trades; trade
VWAP
alone
if
the
book
is
one-sided;
previous
minute's
mark
carried forward
if
neither.
The
trade-heavy
blend
is
deliberate
manipulation resistance
—
moving
the
mark
requires
printing
real
volume,
not
just
posting quotes.
benchmark_price
=
Databento
EQUS.SUMMARY
daily
close (rs/settlement-engine/src/equities.rs:63
SOURCE = "DATABENTO").
rs/settlement-engine/src/common.rs:45): amount = −(funding_amount × signed_position),
bundled
into
per-account NewTransactions,
written
via
TransactionEngine::insert_transactions(),
recorded in
ClickHouse
funding_rates
and
transactions
(transaction_type = 'funding', reference_id = settlement://{symbol}/{ts_ns}).
common.rs:345)
and
continuously
by recon-engine
(rs/recon-engine/src/checks/funding_is_zero_sum.rs).
rs/settlement-engine/src/estimated_funding.rs)
publishes
a live
rate
to
Redis
~every
60s
using
a
time-weighted
EWMA
(5-min
half-life)
of
mark and
Pyth
index.
underlying-publisher
now
runs
a
Databento
EQUS.MINI
live
source (rs/underlying-publisher/src/databento.rs):
consolidated
ohlcv-1m
bars
for every
equities
/
energy_etfs
perp,
written
to
ClickHouse
underlying_prices with
source = "DATABENTO",
calendar-gated
to
the
tape's
sessions
(04:00–20:00 ET),
with
per-source
staleness
health.
See
Dependencies
for
the
one
delta
vs.
this RFC's
original
sketch
(bar
closes
vs.
BBO
mid).
funding_rates
and
the
single
estimated
rate,
but
there's
nothing
that
presents "here
are
today's
13
fundings,
here's
the
running
total."
Goals
Non-goals
generate_settlement_instructions()
unchanged,
just
call
it
13×/day.
| Question | Decision | Rationale |
|---|---|---|
| Averaging window | 30-min full-interval TWAP | Tiles 10:00–16:00 with no gaps/overlap; every trade counts exactly once; max manipulation-resistance. |
| Sampling method | Simple time-weighted TWAP | Fully reproducible client-side; "tractable." Not EWMA. |
| Mark ("settlement price") | Mean
of
per-minute
marks
over
the
30-min
window;
each
=
0.3 × mid + 0.7 × minute trade VWAP |
Exactly
today's
mark
machinery
(mark_price
blend
+
compute_settlement_price
mean),
just
over
30
min
instead
of
5.
Trade-heavy
blend
resists
quote-only
manipulation. |
| Benchmark ("which market") | Databento EQUS.MINI consolidated feed, 30-min TWAP of 1-min bar closes | Live intraday, direct-prop-feed consolidated tape; zero license fees + free redistribution, so it's safe to display to clients. Ingestion already merged (#2872) — see Dependencies. |
| Cadence | Every 30 min, 10:00–16:00 US/Eastern inclusive = 13 fundings/day | Per spec. All 13 slots sit inside the regular session (first window opens at the 09:30 bell, last closes at the 16:00 close). |
| Per-interval scaling | × 1/slot_count (1/13 full day, 1/7 on a 13:00 half-day, …) | Day sums to ≈ one basis charge on every day, including half-days, matching today's once-daily semantics. |
| Cap | ±8 bps per interval | Daily bound ≈ ±104 bps ≈ today's ±1% daily cap (proportionally smaller on half-days). |
| First slot | Included
—
window
(09:30, 10:00]
from
the
opening
bell |
Full-session coverage; opening-auction noise is damped by the 30-min mean and the trade-heavy mark blend. |
| Benchmark sample floor | ≥ 20 of 30 one-minute bars, else skip the slot | Don't settle cash on a stale/gappy benchmark (halts, thin names). |
Per
enrolled
instrument,
set
funding_schedule
to
13
times
in
America/New_York:
{
"timezone": "America/New_York",
"times": [
{ "days_of_week": [1,2,3,4,5], "time_of_day": {"hours": 10, "minutes": 0, "seconds": 0} },
{ "days_of_week": [1,2,3,4,5], "time_of_day": {"hours": 10, "minutes": 30, "seconds": 0} },
"... 10:00, 10:30, 11:00, …, 15:30, 16:00 (13 entries) ..."
],
"exceptions": [ /* equity holidays + half-days, reused from existing calendar */ ]
}next_funding_time()
already
returns
the
earliest
future
time
across
all
entries,
so no
schedule-engine
change
is
required.
Half-days
collapse
to
fewer
entries
via FundingException.times.
For funding interval i (closing at time tᵢ, opening at tᵢ₋₁ = tᵢ − 30m):
markTWAP_i = mean of per-minute marks over (t_{i-1}, t_i]
// per-minute mark = 0.3 × book mid + 0.7 × that-minute trade VWAP
// (existing mark_price blend + compute_settlement_price mean,
// window widened 5 min → 30 min)
idxTWAP_i = mean of Databento consolidated 1-min bar closes over (t_{i-1}, t_i]
N = slot_count for the day // 13 full day; fewer on half-days
premium_i = (markTWAP_i − idxTWAP_i) / idxTWAP_i // dimensionless basis
funding_rate_i = clamp( premium_i × (1/N), −0.0008, +0.0008 ) // ±8 bps
funding_amount_i = funding_rate_i × idxTWAP_i // cash, per unit
Application
sign
convention
is
unchanged
(common.rs:58):
long
pays
on
positive funding,
short
receives,
etc.
Both
sides
of
the
basis
are
means
of
up-to-30 per-minute
observations
over
the
same
interval
—
symmetric
and
fully
reproducible from
public
data
(our
published
book/trades
and
vendor
1-min
bars).
Why
1/N.
premium_i
is
an
instantaneous-style
basis,
not
time-integrated. Charging
the
full
basis
N×/day
would
be
~N×
today's
once-daily
charge.
Scaling
by 1/slot_count
makes
the
day's
fundings
sum
to
≈
one
basis
charge
on
every
day, including
half-days,
preserving
current
economics
while
spreading
it
into
small steps.
Why
the
cap
rarely
binds.
With
the
1/13
scale,
the
±8
bps
cap
only
activates
when |premium_i| > 104 bps
on
a
single
30-min
interval
—
a
genuine
dislocation.
In
normal conditions
each
interval
is
a
few
bps
or
less,
and
the
day's
total
lands
within
≈ ±104
bps,
matching
the
existing
±1%/day
envelope.
The
cap
is
a
tail
guard,
not
a normal-state
clamp.
idxTWAP = 100.00,
markTWAP = 100.50
→
premium = +0.50%
(perp
rich).
funding_rate = clamp(0.005 / 13, ±0.0008) = clamp(0.0003846, …) = +3.85 bps.
0.0003846 × 100 × 200 = $7.69
this
interval;
short
receives
it.
Enrollment must be explicit, queryable, and reversible. Options considered:
funding_schedule
shape
only
—
an
instrument
is
"intraday"
iff
its schedule
has
13
times.
Rejected:
implicit/brittle;
the
runner
can't
cheaply distinguish,
and
it
overloads
a
field
that's
also
the
benchmark
timezone
source.
instruments.funding_variant
(enum: daily_close
default
|
intraday_twap).
Recommended.
Explicit,
indexable, drives
runner
selection
and
the
API
variant
field,
trivially
reversible (UPDATE … SET funding_variant = 'daily_close').
additional_product_specs
—
works
but
unindexed
and
easy
to typo.
Decision:
(B).
Add
funding_variant
to
the
instruments
table (db/postgres/1.sql)
and
DbInstrument
(rs/sdk-internal/db/src/entities.rs). Enrollment
is
a
one-row
update
per
symbol;
the
experiment's
instrument
set
is
whatever has
funding_variant = 'intraday_twap'.
Dataset:
EQUS.MINI.
Databento's
consolidated,
real-time
US
equities
feed, blended
from
direct
prop
feeds
across
NMS
exchanges
+
ATSs
(not
the
SIP).
Critically it
ships
with
zero
license
fees,
free
redistribution
rights,
and
full
exchange compliance
/
no
per-user
fees,
which
is
what
makes
it
safe
to
surface
as
a client-visible
benchmark
on
the
funding
bar
and
API.
What
landed
(rs/underlying-publisher/src/databento.rs):
ohlcv-1m
—
consolidated
1-minute
bars;
the
bar
close
is
written
as a
point
into
ClickHouse
underlying_prices
(source = "DATABENTO", ChUnderlyingPriceSource::Databento),
stamped
at
bar
close.
This
differs
from
this RFC's
original
sketch
(mbp-1
BBO-mid
samples).
Decision:
accept
bar-close
TWAP as
the
benchmark.
idxTWAP_i
=
simple
mean
of
the
(up
to
30)
1-min
closes
in
the interval.
This
is
more
client-reproducible
than
a
BBO-mid
TWAP
—
1-min
bars
are
a commodity
any
client
can
fetch
from
any
vendor,
whereas
replaying
our
mid
sampling is
not
—
at
the
cost
of
print-gappiness
in
thin
names
(bars
are
only
emitted
for minutes
with
trades),
which
the
skip
rule
below
already
handles.
settlement_category ∈ {equities, energy_etfs}, Databento
raw
symbol
derived
by
the
-PERP
strip;
one
live
subscription
covers
all. Calendar-gated
to
the
tape's
sessions
(pre/regular/post,
04:00–20:00
ET
on
trading days)
via
the
shared
equities
holiday
calendar
—
comfortably
covers
all
13
slots.
What was the long pole is now done; remaining feed work is nil unless we reverse the bar-close decision (Open Questions).
IntradayEquitiesRunnerSettlementRunner
(sibling
of
EquitiesRunner,
equities.rs:59)
for funding_variant = 'intraday_twap'
symbols.
underlying_prices
—
same
reason
the
daily runner
fires
at
16:15,
scaled
down.
Each
fire:
MarkWindow
(tᵢ − 30m, tᵢ]
—
generalize
MarkWindow
/ FundingWindowShape
to
a
30-min
trailing
window
for
this
category
instead
of
the fixed
5-min
equities
window
(funding.rs:20,
mark_window.rs).
markTWAP_i
via
the
existing
realized
path
— get_market_snapshots
+
compute_settlement_price
(common.rs:116,149),
a simple
mean
of
per-minute
blended
marks
(not
the
estimator's
EWMA)
—
and idxTWAP_i
(mean
of
the
interval's
underlying_prices
rows, source = "DATABENTO",
via
ChUnderlyingPrice::query_for_symbol;
skip
the slot
if
<
20
bars).
premium × 1/slot_count
(from
the
day's
resolved
schedule),
clamp
±8
bps.
generate_settlement_instructions()
(common.rs:381)
→ transactions,
funding_rates
row,
zero-sum
check.
EquitiesRunner
continues
to
serve
funding_variant = 'daily_close' symbols
untouched.
funding_rate_cap_*_pct
set
to
±0.08
on enrolled
instruments,
so
no
new
cap
field
is
needed
—
reuse
existing
plumbing.
EstimatedFundingPublisher
stays
on
Pyth
for
v1
(decided
2026-07-03)
—
no benchmark
switch
for
enrolled
symbols
yet.
Consequence:
"projected"
bar
cells
are driven
by
a
Pyth-based
estimate
while
realized
slots
settle
against
the
Databento TWAP,
so
small
projected-vs-realized
discrepancies
are
expected;
acceptable
for
the experiment.
A
later
iteration
can
switch
the
read
at
estimated_funding.rs:503 (currently
hardcoded
ChUnderlyingPriceSource::Pyth)
to
Databento
for
enrolled symbols
and
align
the
window.
Scaling
gap
(identified
during
PR
4,
2026-07-06):
the
publisher
computes clamp(full premium, caps)
with
no
1/slot_count
scaling.
Enrollment
sets
the caps
to
the
per-slot
±8
bps,
so
for
an
enrolled
symbol
/estimated-funding-rate (REST
and
the
order-gateway
WS
request)
would
report
the
unscaled
premium clamped
to
the
per-slot
cap
—
neither
the
next-slot
rate
nor
the
daily
total, pinned
at
the
cap
whenever
the
premium
exceeds
8
bps.
/funding-slots
is
immune (it
recomputes
per-slot
from
the
estimate's
raw
prices),
but
the
publisher
must be
made
slot-aware
—
scale
by
the
day's
resolved
slot_count
before
clamping, for
enrolled
symbols
only
—
before
any
symbol
enrolls.
Natural
home:
the same
estimated_funding.rs
change
as
the
benchmark
switch
above
(SoW
PR
6).
Per
the
multiple-order-gateways
/
multiple-settlement
constraint:
13
fires/day
×
N instances
must
not
double-settle.
Each
slot
settlement
must
be
idempotent
on (symbol, settlement_time)
—
a
CAS/unique-key
insert
on
the
funding_rates
/ settlement
reference
so
a
second
instance
for
the
same
slot
no-ops.
(Today's once-daily
runner
already
keys
on
settlement_time;
preserve
that
guarantee
at
30-min granularity.)
Test
client
disconnect,
runner
restart
mid-day,
and
replay
of
a
missed slot.
New
REST
endpoint
(api-gateway,
alongside
get_funding_rates
/ get_estimated_funding_rate
in
rs/api-gateway/src/auth_routes.rs):
GET /funding-slots?symbol={symbol}&date=YYYY-MM-DD (default: today, schedule tz)
(Flat
kebab-case
path
with
the
symbol
as
a
query
param,
matching
the
rest
of the
API
—
no
/v1
prefix
or
path
params.)
{
"symbol": "AAPL-USD-PERP",
"date": "2026-06-25",
"timezone": "America/New_York",
"variant": "intraday_twap",
"interval_count": 13,
"cap_bps": 8,
"slots": [
{
"index": 1,
"funding_time": "2026-06-25T10:00:00-04:00",
"status": "realized", // realized | projected | skipped | pending
"mark_twap": "100.50",
"underlying_twap": "100.00", // naming per the index→underlying rename (#2413)
"premium_bps": "50.0",
"funding_rate_bps": "3.85",
"capped": false
}
// … 13 entries; future slots status="projected" from estimated funding,
// past-with-insufficient-data status="skipped"
],
"realized_sum_bps": "21.4", // sum of realized slots so far
"projected_eod_bps": "49.9" // realized + projected remaining
}
funding_rates.
For
the
daily_close
variant
the
same
endpoint
returns
a
single
slot
—
one
surface for
both,
with
interval_count
distinguishing
them.
A compact horizontal bar of 13 cells near the instrument header:
realized_sum_bps,
projected_eod_bps).
Check
@architect-xyz/ui-components
for
an
existing
bar/sparkline
primitive
before building
(per
GUI
code-style
note);
feed
it
from
the
endpoint
above.
(09:30, 10:00]
opens
at
the
bell,
the
last
closes
at
the
16:00
close. No
post-market
slots.
FundingException;
fewer
slots
that
day
→
interval_count
<
13
in
the
API;
bar renders
fewer
cells.
Scaling
is
1/slot_count
re-derived
from
the
day's
resolved schedule
(decided
2026-07-03),
so
a
7-slot
half-day
charges
premium × 1/7
per slot
and
the
day
still
sums
to
≈
one
basis
charge.
status: "skipped"),
don't
settle
on
stale
data;
zero-sum
trivially
holds
(no txns).
(symbol, slot_time);
a
runner
that missed
11:30
can
backfill
that
one
slot
from
historical
TWAP
without
disturbing later
slots.
Test
server-initiated
disconnect
and
reconnect
across
slot
boundaries.
funding_variant
column
+
IntradayEquitiesRunner
behind
it;
no
instrument enrolled
→
zero
behavior
change.
a-3665
rename
migration
(index_prices
→ underlying_prices)
—
as
of
2026-07-03
prod
ClickHouse
still
has
index_prices only,
so
the
feed's
writes
would
fail
there.
Then
validate
30-min
bar-close
TWAPs vs
known
prints
in
underlying_prices
for
a
week
(no
settlements).
funding_rates
(works
for
daily variant
immediately
—
derisks
the
surface
before
intraday
goes
live).
funding_rates,
don't
post
transactions),
reconcile
sums.
funding_variant = 'daily_close'
→
back
to
16:00
daily.
America/New_York);
the Central
reading
in
the
earlier
draft
was
a
mistake.
Last
slot
=
the
16:00
close.
EQUS.MINI,
zero
license fees
/
free
redistribution,
so
client-visible
is
fine.
The
merged
feed
(#2872)
uses ohlcv-1m
bar
closes,
not
the
originally-sketched
mbp-1
mid;
this
RFC
now adopts
bar-close
TWAP
as
the
benchmark.
Confirm:
(a)
OK
with
lit-venue-only coverage
(no
TRF
off-exchange
prints);
(b)
OK
with
bar-close
TWAP
vs.
BBO-mid
TWAP (reproducibility
argues
for
bars;
thin-name
gappiness
is
handled
by
the
skip
rule).
1/slot_count so
half-days
also
sum
to
≈
one
basis
charge.
funding_rate_cap_lower_pct
must
be ≤
0
—
fine.)
(09:30, 10:00]
opening
window;
auction
noise
is
damped
by
the
30-min
mean
and trade-heavy
mark
blend.
db/postgres/1.sql
—
instruments.funding_variant
enum
column.
rs/sdk-internal/db/src/entities.rs
—
DbInstrument.funding_variant.
rs/sdk-internal/funding-and-settlement/src/{funding.rs,mark_window.rs}
—
30-min full-interval
window
shape
+
× 1/slot_count
scaling.
rs/settlement-engine/src/intraday_equities.rs
—
new
IntradayEquitiesRunner.
rs/settlement-engine/src/estimated_funding.rs
—
consolidated-feed
benchmark
for enrolled
symbols
(swap
ChUnderlyingPriceSource::Pyth
→
Databento
at
:503)
+ 1/slot_count
scaling
before
the
clamp
(see
the
estimated-funding
scaling
gap above;
required
before
enrollment
—
the
endpoint
has
API
consumers,
REST
and
WS, beyond
the
GUI
widget).
rs/underlying-publisher/src/databento.rs
(#2872).
underlying_prices
(source = "DATABENTO").
rs/api-gateway/src/auth_routes.rs
—
GET /funding-slots.
gui/
—
daily
funding
bar
component.
rs/recon-engine/src/checks/funding_is_zero_sum.rs
—
verify
still
holds
at
30-min cadence
(no
logic
change
expected).