Daily rotation engine for BTC-treasury preferred stocks: OLS signal models, Sharpe-weighted softmax allocation, and a 35-check QC protocol, packaged as a Claude skill with a standalone Python regression core.
I rotate daily between three preferred stocks issued by bitcoin-treasury companies (STRC, STRD, SATA) plus cash. Each instrument pays a fat dividend on a fixed cycle, and the price carves a repeatable pattern around every ex-dividend date: run-up before, drop on the date, recovery after. The cycles are offset from each other, so on most days one instrument has better expected return per unit of volatility than the others. Capturing that spread by hand meant re-deriving the same regressions every morning and hoping I didn't fumble a coefficient.
So I turned the whole routine into a protocol an AI assistant can execute: ingest yesterday's prices, refit the models, run quality control, and produce an allocation. The skill in this repo is that protocol. The Python script is its computational core, and it runs fine on its own without any AI in the loop.
I publish it as a working reference for two things: how to model dividend-cycle mean reversion with small, interpretable regressions, and how to structure a Claude skill around a real, stateful, money-adjacent workflow without letting the model improvise.
Given a price database and today's quotes, one run produces:
- Per-instrument expected returns from OLS hold models on two predictors: premium-to-par and days-to-ex-dividend (
ptp,dtex). STRD and SATA get quadratic/interaction terms because their monthly return profile is U-shaped over the cycle, and a linear fit mis-prices the middle. - Ex-dividend event handling. An event study measures the average drop per instrument. If the drop exceeds the dividend, a "drop gate" opens and the engine refuses to hold through the date. A separate ride model can re-enter during the recovery, but only when its fit clears an R² threshold. Both gates are computed from data on every run. Nothing is hardcoded.
- A Sharpe-weighted softmax allocation across STRC, STRD, SATA and cash, with temperature τ=2.
- A 35-check QC battery across six tiers (lifecycle, data, model, comparison, signals, persistence). 23 checks are hard stops: if one fires, the run halts and reports instead of producing trades. I added most of these after a bug bit me, so the list is a fossil record of past mistakes — phantom dividends in windows, softmax re-applied to an already-reduced position, ex-div dates inferred from the wrong table row.
- A widget config block (
const MODELS = {...}) ready to paste into a local dashboard, plus a portfolio snapshot that persists state between sessions.
The skill layer wraps this in a step-by-step protocol: which data to collect, how to validate it, when to halt, and how to hand confirmed positions back for persistence. The assistant acts as the analyst; the human stays the only one who places trades.
preferreds-rotation-engine/
├── SKILL.md # entry point: instrument table, workflow, hard rules
├── references/
│ ├── update-protocol.md # the 7-step daily cycle, QC checks embedded in-line
│ ├── model-architecture.md # model specs + calibration tables (single source of truth)
│ ├── data-collection.md # where prices come from, quality rules
│ └── qc-checklist.md # index of all 35 checks, severity map
├── scripts/
│ ├── portfolio_regression.py # the core: parsing, OLS, event study, softmax, QC
│ ├── generate_synthetic_db.py # builds a fake-but-realistic price database
│ └── render_previews.py # renders the PNGs in examples/
├── assets/templates/
│ └── portfolio_price_database.template.md # blank DB with zeroed snapshot
└── examples/ # synthetic DB, a full regression run, preview images
Three models per instrument, all ordinary least squares on purpose. With 60–140 observations per instrument I want every coefficient inspectable, and I want a misfit to show up as a bad R² rather than hide inside something fancier.
| Model | Predicts | Active when |
|---|---|---|
| Hold | expected monthly return from ptp, dtex (+ quadratic terms where the cycle demands it) |
always |
| Drop | mean ex-div price drop (event study) | gate opens if |drop| > dividend |
| Ride | post-drop recovery return | only if ride R² ≥ 0.50 |
State lives in a PORTFOLIO_SNAPSHOT block inside the price database file: positions, cash, last-known model values, drop-zone flags. The protocol reads it at the start of a session and writes it back after the human confirms actual positions in chat. That confirmation step is deliberate. The engine never assumes its own suggestions were executed.
One full daily cycle, with its QC gates and the state loop:
flowchart TD
DB[("portfolio_price_database.md<br/>+ PORTFOLIO_SNAPSHOT")] --> S1["1 · Read state<br/>lifecycle checks L1–L3"]
S1 --> S2["2 · Fetch prices, EUR/USD, BTC"]
S2 --> S3["3 · Append rows to database"]
S3 --> Q1{"Data QC<br/>D1–D5"}
Q1 -- fail --> HALT["HALT — report, no trades"]
Q1 -- pass --> S4["4 · Regression rebuild<br/>hold · ride · drop gates · T-1 refs"]
S4 --> Q2{"Model QC<br/>D7, M1–M7"}
Q2 -- fail --> HALT
Q2 -- pass --> S5["5 · Compare vs last run<br/>drift warnings M2/M4/M5"]
S5 --> S6["6 · Rebuild widget<br/>MODELS · T1_REFS · positions"]
S6 --> Q3{"Signal QC<br/>S, DM, O checks"}
Q3 -- fail --> HALT
Q3 -- pass --> S7["7 · Human reviews allocation,<br/>places trades, confirms positions"]
S7 --> Q4{"Persistence QC<br/>P1–P5"}
Q4 -- fail --> HALT
Q4 -- pass --> DB
The loop closing back into the database is the point: a session that doesn't persist a confirmed snapshot didn't happen, and a hard-stop anywhere on the path means no allocation gets delivered.
The instruments, for context:
| Ticker | Dividend | Cycle | Window | Hold spec |
|---|---|---|---|---|
| STRC | $0.9583/mo | monthly, ex-div ~15th | 30d | linear |
| STRD | $2.50/qtr | quarterly, Mar/Jun/Sep/Dec 15 | 90d | + dtex² + ptp·dtex |
| SATA | $1.0625/mo | monthly, last trading day | 30d | + dtex² |
- Python 3,
numpyonly for the core script.matplotlibif you want the preview renders. - Markdown for the database, the skill, and the references. The whole state of the system is plain text you can diff.
- The skill format follows Anthropic's Claude skill conventions (SKILL.md + references + assets + scripts).
No pip package, no server, no config framework. Clone and run.
git clone <repo-url>
cd preferreds-rotation-engine
pip install numpy
# generate a synthetic price database to play with
python3 scripts/generate_synthetic_db.py --out examples/synthetic_price_database.md
# run the engine against it
python3 scripts/portfolio_regression.py \
--db examples/synthetic_price_database.md \
--strc_p 99.88 --strc_d 5 \
--strd_p 84.13 --strd_d 5 \
--sata_p 94.09 --sata_d 20 \
--btc_price 105000 \
--btc_table "1:50000,5:62000,15:74000,25:84000,40:98000,50:108000,60:121000,75:136000,85:148000,95:182000,99:230000" \
--eurusd 1.0900 \
--force_skip_strd 20 --force_skip_sata 30 \
--today 2026-06-11To use it as a Claude skill, install the packaged .skill file (or point Claude at this folder) and say "run an update". The skill expects you to supply real prices yourself; see references/data-collection.md for the collection protocol and its rules.
To run it on real money, you replace the synthetic database with your own price history and your own snapshot. That part I can't ship, and wouldn't.
The full output of the command above is in examples/regression_output.txt. The shape of it:
SOFTMAX ALLOCATION (τ=2, active model):
STRC 42.7%
STRD 9.4%
SATA 41.8%
Cash 6.0%
const MODELS = {
STRC: {
bh: [1.03389, -0.92284, -0.00764], // Hold R²=0.9272 n=141
holdSpec: 'simple',
...
dropGate: false, // |drop|=0.849% ≤ div=0.948%
},
STRD: {
bh: [-4.95888, -0.36990, 0.01819, 0.00013, 0.00220], // Hold R²=0.7315 n=79
holdSpec: 'strd',
...
dropGate: true, // |drop|=5.927% > div=2.887%
},
...
};
On this synthetic run, STRD's drop gate is open (its simulated ex-div drop far exceeds the dividend) so the engine shifts weight toward STRC and SATA. The hold-model fits against the synthetic data:
Every number above comes from generated data. The generator (scripts/generate_synthetic_db.py, seed 33) simulates mean reversion toward a discount target, pre-dividend run-up, the ex-div drop, and a front-loaded recovery, plus a shared market-sentiment shock. It self-tests the structural invariants (negative ptp coefficient, convex dtex term) and refuses to write a database that a sane model wouldn't fit.
Working prototype. It runs my live portfolio every day, which is a stronger claim than "tests pass" and a weaker one than "audited". The QC battery exists because daily use kept finding new failure modes; I expect it to keep growing.
Known limits and next steps:
- Walk-forward backtest harness. The current backtest applies final coefficients to the whole history, which leaks future information. Its result is an upper bound, useful for sanity, useless for performance claims. A proper walk-forward refit is the main thing missing.
- Reference dashboard widget. The
MODELSblock targets a local HTML dashboard I rebuild per session and haven't frozen into shippable form yet. A reference implementation belongs in this repo eventually. - More instruments. The model specs are per-instrument config, so adding a fourth preferred is mostly a data problem.
I wrote this with AI assistance: the code was developed with Claude under my direction, and I specify the behavior, review the changes, operate it daily, and maintain it. The skill document doubles as the system's persistent memory, which means the documentation is load-bearing and kept current by necessity.
Price data comes from manual collection (stockanalysis.com history pages, Yahoo Finance for cross-checks). Respect their terms of service; the repo ships no market data, only the synthetic generator.
None of this is investment advice. Preferred stocks of bitcoin-treasury companies are a niche, volatile corner of the market, and a regression with 79 data points has every right to be wrong. The engine proposes; the human disposes.
Built by David Sanz.

