Releases: markudevelop/msts-trader
Releases · markudevelop/msts-trader
v0.25.2
Fixed
- CHANGELOG version headings weren't clickable (links stopped at 0.16.2). The Keep-a-Changelog link-reference footer was maintained by hand and silently drifted — every version from 0.17.0 through 0.25.1 had no
[x.y.z]:definition, so those headings rendered as plain text instead of linking to their compare diff. Regenerated the full footer from the headings (each version → compare from its nearest tagged predecessor; the never-tagged 0.21.0 spans v0.20.1...v0.22.0), and added a test (test_every_version_heading_has_a_link_definition) that fails if any heading lacks a link definition, so it can't drift again.
Full Changelog: v0.25.1...v0.25.2
v0.25.1
Fixed
- Self-heal could double a position when
positions()lags the fill. Post-trade verify rebuilt the diff purely frombroker.positions()after a 2s settle; on an eventually-consistent broker (e.g. Tastytrade's REST, which can lag the fill by several seconds) a just-filled market order still read as residual, so self-heal re-executed it as a second market order that filled immediately — doubling the position. This is the same bug class as the MOC self-heal double-fire (0.25.0) but it bites normal market orders on any lagging broker. Self-heal now never re-trades a leg that was already cleanly sent in the same run (a market order isn't idempotent; a residual there is almost certainly a position-read lag, not a real miss). Genuinely-failed legs (rejected/skipped/resting) were never recorded as clean sends, so they still heal; a true miss on a just-sent leg reconciles on the next run. - Release notes were always the generic fallback. The release workflow extracted the CHANGELOG section with an inline
awkthat fed## [x.y.z]to a regex match — the brackets were read as a character class, so it never matched and every GitHub release showed "See CHANGELOG.md for details." instead of the real notes. Extraction moved to an import-tested helper (msts_trader/changelog.py, literal-heading match) and the release now also runs--generate-notes, so each release shows the changelog plus clickable commit/PR links and a "Full Changelog" compare link.
Tests / robustness
tests/test_self_heal.py: self-heal skips a just-traded leg when positions lag (no double-trade) and still heals a leg that was not traded this run.tests/test_changelog.py: release-notes extraction (bracketed heading, tag form, missing/blank version, last-section-to-EOF, and a guard that the repo's own CHANGELOG yields non-empty notes for the current version).tests/test_cli.py: MOC + limit-chase mutual-exclusion is refused; a live MOC run inside the ~15:50 ET cutoff is refused. (Companion to the MOC self-heal fix in 0.25.0 — locks the guards so they can't regress.)msts_trader/changelogCLI writes UTF-8 bytes so the em-dashes/arrows in the changelog can't crash it on a legacy Windows console.
Full Changelog: v0.25.0...v0.25.1
v0.25.0
Added
liquidatecommand — flatten an account to cash via the patient limit-chase. Sells every long / buys back every short, largest position first, working each line as a LIMIT pegged to the mid (chased over rungs) with a MARKET mop-up for the unfilled / fractional remainder — captures the spread instead of crossing it. RTH-only for live execution;--dry-runpreviews any time. Knobs:--only/--exclude,--aggression(negative = more passive/better price),--retries,--interval,--pace(delay between names),--no-fallback,--json. Live execution requires confirmation or--yes. Resting protective stops on names being sold are cancelled first. New modulemsts_trader/liquidate.py(planner + runner) reuses the existingchaseengine and broker adapter primitives.
Fixed
- MOC orders no longer double-execute (IBKR / any MOC broker). A market-on-close order fills only in the closing auction, but the post-trade verify ran with self-heal enabled and re-checked convergence ~2s after submission. Seeing the book still "unconverged" (the MOC hadn't filled), self-heal re-executed the residual as a plain MARKET order that filled immediately — so each position got one immediate market fill plus the resting MOC. Self-heal is now disabled for MOC runs (convergence is only meaningful after the auction); a one-line note explains why. Affected IBKR, Alpaca, Schwab, and paper MOC runs.
Tests
tests/test_liquidate.py: planner (longs→SELL, shorts→BUY, largest-first, only/exclude, flat/fractional handling), runner fills, dry-run sends nothing, fractional remainder routes to the market mop-up, and config aggression/fallback wiring.tests/test_self_heal.py::test_moc_run_never_self_heals: a MOC run withself_heal=Truereports only and never re-executes.
Full Changelog: v0.24.4...v0.25.0
v0.24.4
Fixed
paper-resetnow honors configured starting cash. The command always reset to the hardcoded $100k default even when login (orPAPER_STARTING_CASH) stored a different amount — contradicting the existing-book login hint. Reset now resolves cash from env/creds-file, then keychain, then the default.
Improved
- Schwab headless creds accept optional
SCHWAB_ACCOUNT_HASH(oraccount_hashin a creds file) so automated runs can skip re-discovering the account hash.
Tests
- 2028 NYSE holiday + early-close calendar coverage.
- Paper login existing-book UX,
paper-resetkeychain cash, andPaper.reset()explicit/default starting cash. - Schwab
SCHWAB_ACCOUNT_HASHenv + creds-file alias.
Full Changelog: v0.24.3...v0.24.4
v0.24.3
Fixed
- Schwab adapter: login no longer reaches into private
_account_hash. The class now exposes a stable publicaccount_hashattribute (used for token-file reuse). Login flow + test mocks updated. - Paper broker: removed dead
raise BrokerErrorsignalling code inreset()(never reached by the CLI and would incorrectly raise on some explicit-starting-cash calls).reset()now cleanly succeeds. - Paper login UX: when running
msts-trader login --broker paperagainst an existing book, the entered starting cash is now clearly noted as applying only to brand-new state files (with hint to usepaper-reset).
Improved
- Market hours: holiday + early-close calendars extended through 2028 using official NYSE dates. New Year 2028 (falls on Saturday) correctly has no observed trading holiday. Comment + README updated.
- Development: added
ruff formatexample +[tool.ruff.format]config. Main source package (msts_trader/) is now consistently formatted. - Ruff format run on the library source (24 files) as part of release hygiene.
What's Changed
- fix(stops): reconcile protective stops on no-trade and duplicate days by @markudevelop in #8
- fix: NYSE half-day early closes + atomic/locked runstate writes by @markudevelop in #9
Full Changelog: v0.24.2...v0.24.3
v0.24.2
Fixed
- Backfilled protective stops now anchor on the live quote, not the position's
price. Some adapters (Tradier) report a position's price as average cost,
not market; a stop re-placed for a held-but-untraded name was therefore set at
cost × (1 − stop_pct)— the wrong distance (too loose on a name that ran up,
possibly above market on one underwater). The backfill now prefers a fresh quote
and only falls back to the position price if no quote is available. Fresh-fill
anchoring (a name bought this run) is unchanged. - A Hyperliquid market order that only RESTS (unfilled remainder on a thin book)
is no longer counted as a clean fill. It's now treated as not-done, so the run
isn't marked complete (idempotency stays open for a re-run) and the post-trade
verify / self-heal chases the unfilled remainder. Other adapters are unaffected
(they never return arestingstatus).
What's Changed
- fix(adapters): live-quote stop anchor + resting-order not a clean fill (0.24.2) by @markudevelop in #7
Full Changelog: v0.24.1...v0.24.2
v0.24.1
Fixed
- Idempotency fingerprint now includes the execution params (allocation,
rebalance-scope, sweep, threshold, threshold-mode, whole-shares, min-weight),
not just broker/account/targets. Before, a deliberate second same-day run that
changed how the book is executed (e.g.--rebalance-scope per-ticker,
--no-sweep, a different--allocation) hashed identically to the first and
was silently skipped as a duplicate. Such a run is a different plan and now
executes. Same-plan re-runs still dedupe;--forcestill overrides. - Post-trade verify is now buying-power-aware under
--margin-aware. A fully
invested / margin-limited book no longer reports "🔴 NOT converged" for a
residual buy it cannot fund, and self-heal no longer re-submits unaffordable
buys every pass. A residual buy that can't be funded from buying power (+ any
residual sell proceeds) is treated as converged (the book is as-deployed-as
-possible) and removed so self-heal skips it; an affordable residual buy still
flags non-convergence, and a failed close (residual SELL) is never excused.
Audit
- Production-readiness sweep of the execution path found no critical
sign/rounding/double-execution bugs; the core sizing math is sound. Remaining
open items (broker-adapter stop round-trip specifics for Schwab/Tradier/
Hyperliquid) need live-API confirmation and are tracked separately.
What's Changed
- fix: production-readiness audit fixes — idempotency fingerprint + buying-power-aware verify (0.24.1) by @markudevelop in #6
Full Changelog: v0.24.0...v0.24.1
v0.24.0
Added
rebalance --no-sweep(orsweep = falsein the config / per[[account]]):
touch only the tickers listed in the CSV and leave every other held
position untouched — for running a strategy sleeve inside a mixed account.- Default (
--sweep) is unchanged: the CSV is the complete book, so any held
ticker not in it is liquidated (the account-wide sweep). - Under
--no-sweep, close a rotated-out name by listing it explicitly with
weight 0 (handled by the existing weight-0 exit path). Held-but-unlisted
positions are surfaced in the preview askept — not in targets (--no-sweep)
with no order, and never trigger a whole-book snap. - Threaded through
rebalance,multi, and the post-trade verify/self-heal path.
- Default (
Note
- This addresses a long-standing footgun: pointing the rebalance at an account
that holds positions outside the target CSV would liquidate them.--no-sweep
makes that opt-out explicit. Companion change in msts-live: the published
weights feed can emitweight=0rows for rotated-out names (theremovedset
it already computes) so liquidations are explicit rather than sweep-implicit.
What's Changed
- feat(rebalance): --no-sweep — leave unlisted positions untouched (0.24.0) by @markudevelop in #5
Full Changelog: v0.23.0...v0.24.0
v0.23.0
Added
rebalance --rebalance-scope whole-book|per-ticker(orrebalance_scope
in the config / per[[account]]): choose how the drift threshold is applied.whole-book(NEW DEFAULT): the threshold is a trigger — if any line
breaches it, the entire book is snapped to target. Mirrors msts-live's
all-or-nothing dead zone. On a full-period backtest of the PNL Unified book
(2015–2026) this delivered higher gross and net CAGR (+~9pp net) than
per-ticker, at ~1.3× the turnover.per-ticker: trade only the breaching lines, leave the rest at their
drifted value (the prior behavior). Lower turnover, better Sharpe/drawdown.- Orthogonal to
--threshold-mode(which only sets the drift denominator).
Changed
- BREAKING (default behavior): the rebalance now defaults to
whole-book
scope. Existing books that relied on the old per-line partial behavior should
setrebalance_scope = "per-ticker"(config) or pass--rebalance-scope per-ticker. A pleasant side effect of the new default: small sleeves below the
drift gate are no longer frozen on a fresh account — once any line breaches,
the whole book (including sub-threshold sleeves above the $1 dust floor) is
established in one pass. - The post-trade verify / self-heal path honors the same
rebalance_scope, and
themulticommand now threads bothrebalance_scopeandthreshold_mode
(top-level or per[[account]]).
What's Changed
- feat(rebalance): selectable execution scope — whole-book (default) vs per-ticker (0.23.0) by @markudevelop in #4
Full Changelog: v0.22.1...v0.23.0
v0.22.1
Docs
- README now documents post-trade verify, self-heal, and their flags (
--no-verify,
--no-self-heal,--heal-passes). - Clarified that protective stops are OPT-IN. The 0.20.0
--stop-pctnote read as if stops are
always applied; they are not. With no per-rowstop_pctand no--stop-pct, no stops are
placed, and a rebalance never strips an existing stop off a still-held position (only orphan
stops with no position are cancelled).--stop-pctis a blanket default you opt into for feeds
that omit stops but want every leg protected; a mixed book (e.g. some held-forever positions)
should rely on per-rowstop_pctinstead.
Tests
- Added opt-in coverage: no
stop_pctanywhere → zero stops placed; a held position's existing
stop survives a no-stop_pctrebalance.
What's Changed
- docs+test(stops): document verify/self-heal; clarify stops are opt-in (0.22.1) by @markudevelop in #3
Full Changelog: v0.22.0...v0.22.1