Changelog

Full transparency log — every bug found, every fix applied, every data reset.
The whole point of this project is honest data. That includes documenting when the data was wrong.

v2 Reset — Clean Start

After running for approximately 6 weeks, three bugs were identified that corrupted the trade data and statistics enough to make the v1 dataset unreliable for research purposes. The decision was made to archive the v1 data, document the bugs transparently, and start clean.

v1 archive available for download: trades_v1.json · stats_v1.json · blog_v1.json


v1 Summary (Aug 24 – Oct 8, 2026)

Trades Entered
~34
Trades Closed (clean)
8
Trades Stuck Open
26
Clean P&L (8 trades)
-$186.32
Clean Win Rate
12.5%
The 26 stuck-open trades and the win/loss record are not reliable signals about the methodology. The bugs below caused incorrect hold decisions, incorrect FOMO calculations, and a crash that prevented trades from ever closing. The data is not clean enough to draw conclusions from.

Bugs Found and Fixed

Bug 1 — fetch_news.py · Fixed 2026-10-08
make_decision() held on SPY sentiment alone
The hold/sell decision was triggering a hold if either the ticker OR SPY news was bullish/bearish. This meant a CALL could be held based purely on SPY being bullish, even if the ticker itself had neutral or bearish news. Neutral ticker sentiment was also being counted as confirmation. Result: trades were held far more often than the methodology intended, and several holds were triggered by irrelevant SPY headlines.
Fix: ticker sentiment only drives the decision. Bullish ticker = hold CALL. Bearish ticker = hold PUT. Neutral always sells. SPY/SPX still logged for research but no longer drives the hold/sell decision.
Bug 2 — sell_signal.py · Fixed 2026-10-08
Saturday KeyError crashed the sell script daily
stats['by_day_of_week'] only had Monday–Friday keys hardcoded. When a trade's entry date resolved to a Saturday (edge case from timezone handling or manual test entries), the stats update crashed with a KeyError. Because the crash happened inside the trade loop, no trades were closed on that run — they all remained open. This compounded daily, resulting in 26 stuck-open trades by the time it was caught.
Fix: dynamically create the day key if it doesn't exist rather than assuming Mon–Fri only.
Bug 3 — sell_signal.py · Fixed 2026-10-08
FOMO left_on_table included excluded trades in avg
Trades 1–4 were flagged as excluded from analytics due to a prior FOMO calculation bug (sell_contract_value used raw stock price × 100 instead of BS option value, producing values like -$10,451 and -$22,794). The exclusion logic correctly skipped those trades in most analytics buckets, but the fomo.avg_left_on_table calculation did not check the exclusion list. The corrupted left_on_table values from trades 1–4 were being averaged into the stat, making it meaningless.
Fix: fomo block now checks excluded_ids before updating avg_left_on_table, consistent with all other analytics buckets.
Bug 4 — paper_trade.py · Fixed 2026-10-08
No OTM cap on 2DTE+ contracts
The contract selection filter had a 20% OTM cap for 1DTE contracts but no cap for 2DTE and above. All four clean trades (5–8) had strikes 7–13% OTM on 2–3DTE contracts, which placed every strike outside the 1σ expected move range. A contract outside the expected move needs a larger-than-predicted move to profit — systematically selecting these is a negative edge, not a neutral one.
Fix: added MAX_OTM_GENERAL = 10% cap for 2DTE+ contracts. Keeps strikes inside or near the expected move range where there is actual probability of profit.
Bug 5 — fetch_signals.py · Fixed 2026-10-08
Blank sector passed through as empty string
Some tickers in stocks_optionable.csv have blank sector fields. These were being passed through to signals.json and trades.json as empty strings, showing as "Unknown" in the stats sector breakdown and making sector analysis unreliable.
Fix: when CSV sector is blank, fetch sector from yfinance Ticker.info at signal generation time. Falls back to 'Unknown' only if yfinance also has no data.

All v1 data is preserved in data/archive/ and will remain publicly downloadable. The archive is part of the record. Deleting inconvenient data is not how this project works.
v2.1 Bugs — IV=0 Crash + Expired Contract Accumulation

Two new bugs discovered on the first sell run after the v2 reset. The script crashed on trade #15 (RGTZ) and left trades 12–43 stuck open — same symptom as the v1 Saturday KeyError, different root cause.

Trades Stuck Open
28
Trades Closed Before Crash
3 (ALAB, TSLA, DDOG)
Crash Trigger
RGTZ IV=0
Bug 6 — sell_signal.py · Fixed 2026-10-19
IV=0 caused ZeroDivisionError in Black-Scholes
RGTZ (Tidal Trust II Defiance DLY TRG) had iv_at_entry: 0.0 — yfinance returned no implied volatility data for this ticker at entry time. Black-Scholes divides by sigma in the d1 calculation, so sigma=0 causes a ZeroDivisionError. The script crashed mid-loop on trade #15, leaving all subsequent trades (15–43) unprocessed and stuck open.
Fix: when sigma ≤ 0, skip Black-Scholes and use intrinsic value only (max(0, S−K) for CALL, max(0, K−S) for PUT). Floor at $0.01 to avoid zero-value edge cases downstream.
Bug 7 — sell_signal.py · Fixed 2026-10-19
No expiration check — expired contracts never force-closed
The sell loop had no check for whether a contract's expiration date had already passed. Contracts that were stuck open (due to the v1 Saturday crash) accumulated from Sept 11 onward. When the v2 reset ran, those trades were re-opened with their original entry data but the contracts were already expired. The sell script would attempt to fetch today's candles for a ticker holding a Sept 11 contract — meaningless data, wrong price, wrong date. Without a force-close mechanism, these would accumulate indefinitely.
Fix: at the start of each trade in the sell loop, check if expiration date < today. If so, force-close at $0 (expired worthless) with sell_trigger = 'expired_worthless'. This correctly records the full premium as a loss, which is the actual outcome.
Trades 12–43 (28 trades) will be closed at $0 on the next sell run. These are real losses — the premium paid is gone. They will be recorded accurately in the ledger. The DDOG trade (#14) was a +$1,712 winner that closed before the crash — that result stands.
v2.1 Follow-on — NaN in JSON + Actions Timing

Two additional issues discovered on the same day as the IV=0 crash, after the sell script ran successfully.

Bug 8 — paper_trade.py / fetch_news.py / sell_signal.py · Fixed 2026-10-19
NaN written to JSON — all pages failed to load
Python's json.dump writes NaN literally when a float value is math.nan. The JSON spec does not allow NaN — it is only valid in JavaScript. The spy_change field was NaN on any trade where yfinance failed to return SPY price data (common on Fridays and around market holidays). This made trades.json unparseable, crashing paper-trade.html, ledger.html, and blog.html with a SyntaxError on every page load.
Fix: sanitize NaN → null at write time in all three scripts using a regex replace on the JSON string before writing. Also sanitize spy_change at the source in paper_trade.py before it enters the trade object.
Known Limitation — GitHub Actions · Accepted 2026-10-19
Sell script runs late — sell prices are approximate
GitHub Actions free-tier cron jobs are not guaranteed to run on schedule. The sell script is scheduled for 6:31 AM PST (1 minute after market open) but has been observed running as late as 10:53 AM PST — nearly 4.5 hours after the intended exit time. This means the "stock open price" used for Black-Scholes exit calculations is actually a mid-morning price, not the true open candle. P&L figures are directionally correct but not precise.
Decision: accepted as a known limitation of the free infrastructure. Sell prices are labeled as approximate. A future fix would cache the open candle in the news script (which runs earlier and is less likely to be delayed), but this is not prioritized until the methodology is validated at scale.
GitHub Actions — Write Permission Fix

All GitHub Actions workflows were failing with a 403 error on push. The Actions bot did not have write permission to the repository.

Fix: Settings → Actions → General → Workflow permissions → Read and write permissions.

paper_trade.py — SyntaxError on deploy

Paper Trade Entry workflow failing with SyntaxError on line 195. The def build_auto_note( function definition line was missing — only the parameters and body were present. Caused by a bad edit during development.

Fix: restored the missing def line.