Skip to content

Modeled overnight margin-interest (financing) cost for adaptive-paper runs - #243

Merged
seathatflowsinourveins merged 1 commit into
mainfrom
claude/margin-financing-20260925
Sep 25, 2026
Merged

seathatflowsinourveins merged 1 commit into
mainfrom
claude/margin-financing-20260925

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator

What

A modeled margin-interest (financing) cost for adaptive-paper runs that end holding positions overnight. It answers the open question from the fee review: leverage held overnight has a financing cost that the engine did not model. No shipped config enables overnight_holds, so no current run's output changes.

Sources (retrieved 2026-09-25; cited in financing.py and README-safety.md)

  • Alpaca, Margin and Short Selling.
    • Annual rate: 6.50%, or 5.00% for Elite; the page points to the Brokerage Fee Schedule for the latest rate.
    • Charged only on the end-of-day settlement-date debit balance: debit × rate / 360 per day, accrued daily and posted monthly.
    • A settlement-date debit at Friday's close is charged for 3 days.
    • Worked example: deposit $10,000, buy $15,000, hold overnight; $5,000 borrowed costs $0.90 a day.
  • Alpaca, Paper Trading.
    • Paper does not simulate regulatory fees or dividends.
    • Borrow fees are "⛔️ (Coming Soon!)" for paper.
    • It does not say whether paper posts margin interest. So this is a modeled cost that is never reconciled against the paper account.

Change

File Change
adaptive-paper/financing.py (new, pure, Decimal) MARGIN_RATES, DAY_COUNT = 360 and RATE_SOURCE. settlement_date is T+1 via sessions.next_trading_day. settled_debit_schedule counts cash flows from their settlement date, one row per calendar day. margin_interest returns a per-day breakdown at full precision and rounds only on request. overnight_financing_projection treats the book as sold at the next session and charges the calendar days from the buy's settlement to the sale's.
adaptive-paper/runner.py An optional financing_plan config key, "standard" by default and validated in load_config. A modeled_financing block ("evidence_class": "modeled") is added to the outcome only when a run ends held_overnight. Every shipped config has overnight_holds false, so their outputs stay byte-identical.
README-safety.md A "Financing costs (modeled)" section: sources, formula, settlement convention, what is not modelled, and that paper does not verify it.

Settlement convention. Interest runs on the settlement-date balance, so the settlement lag moves the charged days:

Buy day Buy settles Sale settles Days charged
Thursday Friday Monday 3 (Fri, Sat, Sun): the documented Friday case
Friday Monday Tuesday 1

Checks

Check Result
tests.test_adaptive_paper_financing + source-hashes manifest (system Python) 19 tests OK
Pinned rc5 runtime: tests.test_adaptive_paper_financing + tests.test_adaptive_paper_runner 190 tests OK, 0 skipped. The runner test covers both the off and on paths, using real safety.Position objects.
Mutation check: 8 one-line breaks, each failing a test The rate; the day walk; flows keyed by trade date instead of settlement date; the sale's settlement date; the debit floor; plan validation; premature rounding; the held-overnight gate
All five runner.load_config configs load unchanged with no financing_plan key Pass
validate.py, evidence_manifest.py --check, validate_convergence.py --all-recorded Pass
gitleaks, git mode over the branch commit No leaks. The two generic-api-key hits from a plain .diff scan are source-hashes.json digest lines covered by the path-scoped allowlist.

Not covered

  • Bank-holiday settlement. Days when NYSE is open but settlement isn't (Columbus Day, Veterans Day) are not modelled as non-settlement days. It's documented, and 2026's NYSE holidays are all bank holidays too.
  • Other costs. Borrow fees (hard-to-borrow shorts) and dividends are not modelled.
  • Never run end to end. The held_overnight path has not run inside a live run_native, since no config enables it. It's covered by unit tests with real Position objects.
  • The debit is a projection. It uses the current book's cost basis against the trial's starting cash; it is not a reconstructed intraday settled-cash path. The docstrings say so.

Review

  • Implementation: a Claude builder, working in its own worktree from d9c2c6e.
  • Coordinator review (Claude Opus 5.5): the settlement arithmetic, which reproduces the Thursday and Friday cases above; serialization (the outcome writer uses default=str, and the only unguarded json.dumps prints a small not_started dict); the real Position.cost_basis_usd and SessionInfo.session_date attributes; and the overnight_holds gate.
  • Codex cross-family review: unavailable until 2026-09-30, so it was skipped.

🤖 Generated with Claude Code

Add financing.py (pure, Decimal, no I/O, style of leverage.py): settlement
date via sessions.next_trading_day (T+1, NYSE trading-day calendar, not the
separate SIFMA settlement calendar), a settlement-date debit schedule and
margin-interest sum at full Decimal precision, and a simple next-session-sale
projection for the currently held book. Rates and the daily-charge formula
are Alpaca's "Margin and Short Selling" docs (5.00% Elite / 6.50% standard,
debit * rate / 360); paper-trading docs do not confirm whether paper posts
margin interest at all, so every value stays a documented, unreconciled
projection.

Integrate minimally into runner.py: load_config validates an optional
"financing_plan" key ("standard" default, any other value refused, same
pattern as the other opt-in feature keys); run_native's new
_finalize_status_and_financing (factored out for direct testing, same
rationale as _run_native_status) attaches an outcome "modeled_financing"
block, labelled "evidence_class": "modeled", only when a run just ended
"held_overnight". Every shipped config leaves overnight_holds False, so no
existing outcome gains a key -- verified both by the code path (the flag
gates _honest_overnight_hold's very first check) and by loading every
shipped config plus a dedicated runner-level test.

Tests (tests/test_adaptive_paper_financing.py, stdlib only) cover the docs'
worked example at both rates, a Friday balance's 3-day weekend accrual, T+1
settlement offset, a settle-to-settle window spanning a weekend and the 2026
Labor Day holiday, no-debit coverage, unknown-plan refusal and kept Decimal
precision; each was confirmed to fail on a deliberately broken variant of
the corresponding source line before being restored. Register financing.py
and its test in source-hashes.json and manifests/evidence.json alongside
the refreshed runner.py/test_adaptive_paper_runner.py digests.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@seathatflowsinourveins
seathatflowsinourveins merged commit 764bf79 into main Sep 25, 2026
27 of 28 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/margin-financing-20260925 branch September 25, 2026 11:44
seathatflowsinourveins added a commit that referenced this pull request Sep 25, 2026
… incentive board and data lake, and the recounted WSL host (#288)

- simulation: 866 to 884 fills per min at 70 ms exact release, 30 of 30
  minutes, and 672 at 1 s (#254, sim-capacity/receipts/20260925-elite-tier.json).
- paper: adds the margin financing model (#243); still blocked on the
  paper keys (HTTP 401).
- mover-early-entry-studies: v1 to v3, with the v3 core merged (#190) and not
  frozen.
- New gates:
  - incentive-board-forward (#187, #220): 0 orders;
  - data-lake-source (#208): minute-bar pilot not run.
- wsl-workstation-host: the foundation lane's wording. Counts were recounted
  on main 0074a0c by the last independent_session verdict per receipt,
  superseded excluded: 69 receipts, 56 agree, 1 needs changes, 0 awaiting
  review, 12 superseded (#285).

The foundation lane acked the strings and the recount rule.

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Sep 26, 2026
#242 and #284 (#350)

* Paper engine: fix the retroactive cross-family review findings on #221, #242 and #284

GPT-6 (gpt-6-astra, effort max) reviewed three merged paper-engine PRs on main ca8e37f; #243 passed.
- #221: activity-based gap recovery booked fills natively without persisting them to the durable ledger (native 3,
  durable 1); a continuing LULD pause auto-cleared after twelve minutes and let cancel_expired cancel the exit it
  protected; cash and realized P&L depended on REST-snapshot versus stream delivery order.
- #242: replay_compare scheduled a named cancel stamped before its submit and silently skipped it; cancelling the
  transport task cleared the cancel identity before the HTTP worker sent the DELETE.
- #284: a clean exporter stop released the port before deregistering, so it could remove a replacement exporter's
  target; configure.py's initialization could overwrite a concurrent registration; malformed nested labels passed
  validation and would make Prometheus reject the whole target list.

Each fix was implemented test-first by a GPT-6 implementer in its own worktree; every new regression failed before
its fix. Coordinator integration: source-hashes.json and evidence pins refreshed; the 20260923g sim-paper-compare
receipt replayed from its retained pages with the new code (identical apart from run-local fields and runner_sha256,
updated as in #242); sim-capacity's frozen replay_compare source re-pinned with a dated limitation (AST check: the
8 definitions sim-capacity uses are unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Paper engine repair round: persist recovered fills through the transport's real observation hook

Independent verification of cad7faf found #221 finding 1 unresolved in production: reconcile_fills persisted recovered
fills only through a port.controller attribute that no real Alpaca port has (AlpacaPaperTransport and the native-faults
FaultTransport are wired with sink_observation=controller.observe); only the test-only SimulatedPort carried it.
- native_adapter.py: persist through the port's sink_observation (the hook the transport already uses for stream rows),
  after validating that the recovered activities tile the fill contiguously (fail closed otherwise).
- simulation.py: SimulatedPort exposes the same sink_observation hook, so tests and production share one path.
- Tests: a real-shaped port (no .controller) regression that failed on cad7faf with Decimal('1') != Decimal('3'), restart
  persistence, incomplete tiling; the two accounting-replay guards in safety.py; fast build_decisions-level tests for
  the cancel-before-submit refusal and its equality boundary.
Implemented by a GPT-6 implementer test-first; 514 tests across the five affected modules pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Sep 26, 2026
…ealized-loss delivery-order major; follow-up to #350) (#355)

* Paper engine: fix the retroactive cross-family review findings on #221, #242 and #284

GPT-6 (gpt-6-astra, effort max) reviewed three merged paper-engine PRs on main ca8e37f; #243 passed.
- #221: activity-based gap recovery booked fills natively without persisting them to the durable ledger (native 3,
  durable 1); a continuing LULD pause auto-cleared after twelve minutes and let cancel_expired cancel the exit it
  protected; cash and realized P&L depended on REST-snapshot versus stream delivery order.
- #242: replay_compare scheduled a named cancel stamped before its submit and silently skipped it; cancelling the
  transport task cleared the cancel identity before the HTTP worker sent the DELETE.
- #284: a clean exporter stop released the port before deregistering, so it could remove a replacement exporter's
  target; configure.py's initialization could overwrite a concurrent registration; malformed nested labels passed
  validation and would make Prometheus reject the whole target list.

Each fix was implemented test-first by a GPT-6 implementer in its own worktree; every new regression failed before
its fix. Coordinator integration: source-hashes.json and evidence pins refreshed; the 20260923g sim-paper-compare
receipt replayed from its retained pages with the new code (identical apart from run-local fields and runner_sha256,
updated as in #242); sim-capacity's frozen replay_compare source re-pinned with a dated limitation (AST check: the
8 definitions sim-capacity uses are unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Paper engine repair round: persist recovered fills through the transport's real observation hook

Independent verification of cad7faf found #221 finding 1 unresolved in production: reconcile_fills persisted recovered
fills only through a port.controller attribute that no real Alpaca port has (AlpacaPaperTransport and the native-faults
FaultTransport are wired with sink_observation=controller.observe); only the test-only SimulatedPort carried it.
- native_adapter.py: persist through the port's sink_observation (the hook the transport already uses for stream rows),
  after validating that the recovered activities tile the fill contiguously (fail closed otherwise).
- simulation.py: SimulatedPort exposes the same sink_observation hook, so tests and production share one path.
- Tests: a real-shaped port (no .controller) regression that failed on cad7faf with Decimal('1') != Decimal('3'), restart
  persistence, incomplete tiling; the two accounting-replay guards in safety.py; fast build_decisions-level tests for
  the cancel-before-submit refusal and its equality boundary.
Implemented by a GPT-6 implementer test-first; 514 tests across the five affected modules pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Paper engine: book realized P&L and gross loss per exact execution in accounting replay

The accounting replay summed a status delta's executions into one notional
before computing P&L, so a sell filled at a gain and a loss inside one delta
netted the loss away, and the result depended on whether the stream or the
REST snapshot arrived first. Replay now books each execution that tiles the
delta separately (QuantConnect/Lean 985ef30: SecurityPortfolioModel.cs per-fill
closing P&L; TradeStatistics.cs accumulates losses separately), falling back to
the delta notional only while executions are incomplete.

Two regressions: mixed-profit gross loss across stream-first, REST-first,
duplicate and restart deliveries, and basis preservation at each booking.
With the fix reverted, 4 cases fail (rest_first, duplicates, restart, basis).
Paper suites (safety, runner, native, transport): 407 tests OK.
Implemented by GPT-6 (gpt-6-astra, effort max) test-first; verified by the
coordinator. Pins: source-hashes.json safety.py and manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Paper engine repair round (#355 review): replay executions in execution order, versioned marker

Independent review of c81a3d5 (needs_changes):
- Finding 1 (major): the replay booked fills in observation order. Stream fills
  now carry Alpaca's own trade_updates timestamp (event_at_ns; FILL recovery
  forwards transaction_time_ns), the ledger keeps it in a new optional
  executions.at_ns column (added on open for existing ledgers), and replay
  fills each symbol's timed execution slots in execution order, tie-broken by
  event id then cumulative quantity. Adoptions, untimed executions and
  uncovered provisional bookings keep their journal positions.
- Finding 3: the marker is execution_accounting:v2:<client_id>, so ledgers
  replayed under the observation-order rule re-derive once.
- Finding 4: a test for an uncovered REST-only booking beside another order's
  late executions.
- Finding 2 (adopted open orders) is covered by #356's startup persistence.

The A/B/C case gives cash 70, realized -15, gross loss 25, and 1 share at 115
under stream, REST-ahead, recovered, late-buy and nanosecond deliveries.
Reverting the three production files fails 10 cases (4 new tests, 2 strengthened).
Paper suites: 411 OK; trading gates 26 OK; validate.py passed.
Implemented by GPT-6 (gpt-6-astra, effort max) test-first; verified by the
coordinator. Pins: source-hashes.json (safety, transport, native_adapter) and
manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Re-pin the changed paper test files in adaptive-paper/source-hashes.json

source-hashes.json also pins test files; CI's
test_adaptive_paper_source_hashes_manifest caught the stale entries. Pins only,
no code change: the changed tests' entries and their manifests/evidence.json
registration.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Reduce #355 to the reviewed-safe subset; defer execution-order replay

The re-check of cc6d7cb found two new majors in the execution-order replay:
- Moving an earlier-executed fill into another order's slot can make an
  uncovered REST-only sell go short. The replay then raises, the fill rolls
  back and the transport freezes, although the real history is consistent.
- Removing the early exit let that failure block fills in every symbol and
  added a full replay to every fill.
Under the bounded review loop (one review, one repair, then land with
residuals), this commit lands the parts both reviews confirmed.

It keeps:
- c81a3d5's per-execution P&L and gross loss within a delta;
- the versioned execution_accounting:v2 marker (F3);
- the uncovered-fallback test (F4), with its oracle stated for
  observation-order replay.

It reverts:
- the slot-substitution replay and the early-exit removal;
- the executions.at_ns column, which also avoids the unversioned schema
  change (re-check F3);
- the stream and recovery timestamp plumbing.

Execution-time ordering is a recorded follow-up, with the reviewer's
constraints: never reorder into a short (fall back to observation order), a
targeted replay trigger, and a schema version bump.

Revert checks: the v1 marker fails the F3 test; skipping the uncovered
fallback errors the F4 test. Paper suites: 409 OK; source-hashes and trading
gates: 33 OK; validate.py passed. Pins: safety.py and its test file only.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant