Date: 2026-07-30
Status: Draft
When
we
list
a
dated
future
on
EP3,
we
currently
set
the
EP3
maturity_date to
the
contract's
true
expiry
T
—
future_attributes_for
in rs/admin-cli/src/instruments.rs:44-58
carries
the
real
contract_date
into EP3
"so
EP3
expires
them
correctly"
(perpetuals
keep
the
far-future
2099-12-31 sentinel).
This
is
wrong
for
how
we
actually
retire
an
instrument.
EP3
does
not auto-resolve
futures:
at
maturity
it
flips
the
instrument
to
Expired
and leaves
positions
untouched
—
we
must
book
the
final
settlement.
Our
delist orchestrator
(admin-cli delist,
rs/admin-cli/src/delist.rs)
does
that
by booking
one
EP3
two-sided
block
trade
per
holder
against
the
house
settlement account,
then
setting
the
EP3
state
to
Terminated.
Those
closeout
block trades
require
EP3
to
still
consider
the
instrument
open.
If
EP3
has
already expired
the
instrument
at
T,
the
delist
procedure
can't
book
the
closing trades
—
the
exchange-side
expiry
races
the
very
procedure
that
settles
it.
AX already has its own source of truth for the real expiry and lifecycle:
instruments.expiration
(TIMESTAMPTZ,
mutable)
—
the
true
T (db/postgres/1.sql).
instruments.is_closing
—
freezes
client
order
entry
and
modification (cancels
allowed)
while
positions
are
closed
out
ahead
of
delisting; reversible.
Terminal
state
is
delisted_at,
stamped
by
the
delist
run.
So
the
EP3
maturity
date
doesn't
need
to
be
the
real
expiry
at
all
—
clients never
see
it
(the
public
expiration
field
is
served
from
our
DB
row).
It
only needs
to
be
late
enough
that
EP3
never
expires
the
instrument
out
from
under the
delist
procedure.
List
dated
EP3
instruments
with
an
EP3
maturity_date
of
T+7
days (T
=
the
true
expiry,
i.e.
the
nominal
contract_date).
The
buffer
is
a backstop,
not
a
schedule:
the
normal
path
is
that
admin-cli delist
runs shortly
after
T
and
terminates
the
EP3
instrument
well
before
T+7.
EP3's own
expiry
should
never
fire.
At
the
true
T,
an
automated
process
moves
the
instrument
to
closing state:
set
is_closing = TRUE
on
the
AX
row
while
the
EP3
instrument stays
open.
From
T
onward
the
instrument
is
exactly
in
today's
pre-delist posture
—
clients
can
cancel
and
be
closed
out
but
not
open
new
exposure
— until
ops
runs
the
delist
within
the
buffer
window.
The lifecycle becomes:
| Time | EP3 state | AX state |
|---|---|---|
| listing → T | Open | trading
normally,
expiration = T |
| T (automated) | Open (maturity T+7 not yet reached) | is_closing = TRUE |
| delist run (T … T+7) | closeouts
booked,
then
Terminated |
delisted_at
stamped,
is_closing
cleared |
| T+7 | never reached in the normal path | — |
Perpetuals
(contract_date = 0)
are
unaffected
—
they
keep
the
2099
sentinel and
have
no
automated
closing
step.
future_attributes_for
(rs/admin-cli/src/instruments.rs):
dated
contracts get
contract_date + 7 days
as
the
EP3
maturity
instead
of
contract_date. Day
arithmetic
must
roll
month/year
boundaries
correctly
(use
chrono,
not field
math
on
the
YYYYMMDD
designator).
expiration <= now(), is_closing = FALSE,
and
delisted_at IS NULL,
and
sets is_closing = TRUE.
Same
effect
as
the
existing
manual
admin
action,
just automated
at
T;
idempotent
and
safe
to
rerun.
expiration
is
already
the
true
T
and
is_closing
is already
the
closing
flag.
InsertTwoSidedBlockTrade?
The
proposal
assumes closeouts
require
an
open
(non-Expired)
instrument
—
confirm
with Connamara
whether
Expired
also
rejects
block
trades,
or
merely
rejects regular
orders.
If
Expired
happens
to
allow
block
trades,
the
buffer
is still
cheap
insurance
but
the
motivation
weakens.
is_closing
has
been
set
for more
than
N
days
without
a
delist,
and/or
have
the
task
push
the
EP3 maturity
out
further
while
the
instrument
remains
undelisted.
Alerting
alone is
probably
enough
for
a
7-day
window.