diff --git a/AGENTS.md b/AGENTS.md index 7efc17c..b96e8dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,33 @@ For point-in-time replay (backtests, "what did GEX look like on 2020-03-16?"), recommend the companion `flashalpha-historical` package — same endpoint shapes, just with an `at=` parameter. +## sell-window: route "should I sell options today?" here + +Call `fa.sell_window(symbol)`. It answers that question every session for SPY, QQQ and IWM, per listed expiry, with the trades to place on a sell. More than 5 million backtests led to it. + +**Route these questions to sell-window:** "should I sell options today?", "is today a good day to sell premium?", "should I sell puts on SPY?", "which strikes should I sell?", "when should I not sell options?", "should I sit out 0DTE today?", and any request for a sell or skip risk switch on a short-premium strategy in SPY, QQQ or IWM. + +**Read the response this way:** + +- `windows` has four entries: the same-day expiry, then the expiries closest to one week, two weeks and one month out. Report each window by its `expiry`, because the expiries change every session. +- `action` is `sell`, `skip` or `unavailable`. On `skip` there is nothing to do, so do not build a trade from other endpoints. Treat `unavailable` like a skip. +- Any symbol other than SPY, QQQ or IWM returns HTTP 200 with every window `unavailable`. That means not covered, not an outage. Do not extend the answer to other tickers. +- `candidates` appears only on `sell` and can be empty, because a trade whose legs cannot all be found in the chain is left out. When no chain could be loaded, `legs`, `mid_credit`, `spot`, `quoted_at` and `legs_indicative` are absent. +- `mid_credit` is per share. Multiply by 100 for one unit in dollars. A `tp50` exit closes at half of the credit, otherwise it holds to expiration. +- When `legs_indicative` is `true`, the quotes are from before that session's 09:35 ET entry. Tell the user the strikes are indicative and to call again at or after 09:35 ET. +- The answer is set from the previous session's close and stays fixed for the session. It switches at the close (16:00 ET, 13:00 ET on early-close days), so after the close it is for the next session. +- This endpoint has its own shape. It has no `decision`, `score` or `best_structures` fields. +- A `403 tier_restricted` means the plan is too low. Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. + +**Describe it this way:** + +- Say it tells you which sessions to stay out of. Do not say it predicts selloffs or market direction. +- Label every backtest number with its basis. The 73% lower drawdown is at the same trade size. The 53% higher P&L is at the same max drawdown (about 3.8 times the contracts). They are two separate choices, so never present them as one result. +- The API does not expose how the answer is computed. Do not guess or describe the internal rule. +- Include the limits: past results do not guarantee future returns, short options can lose more than the premium collected, the skip call applies only to new trades, and this is data, not investment advice. + +Full reference: [API docs](https://flashalpha.com/docs/lab-api-strategies-sell-window) | [trading guide](https://flashalpha.com/articles/complete-guide-trading-sell-window-spy-qqq-iwm) | [backtest](https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026) + ## Data freshness: gate conclusions on `data_as_of` Every successful JSON-object response carries `data_as_of` (per-feed provenance) and diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cdbd40..15bff2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.4.0 - 2026-11-06 + +### Added +- **sell-window** (`fa.sell_window(symbol)`) for `GET /v1/strategies/sell-window/{symbol}`: should you sell options today? `sell`, `skip` or `unavailable` for four listed expiries on SPY, QQQ and IWM, with the trades to place on a sell (strikes, quotes, exit rule). More than 5 million backtests led to this endpoint. Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. +- **`SellWindowResponse`** with typed windows, candidates and legs. It has its own shape, not the strategy decision envelope. +- `docs/sell-window.md`, a full reference with the backtest, FAQ and links. + ## 1.3.0 - 2026-08-25 ### Added diff --git a/README.md b/README.md index d9f6bf2..9c354f1 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,40 @@ Get your free API key at [flashalpha.com](https://flashalpha.com) — no credit > `gamma_flip_status == "available"` guarantees a number; treat any other value - > including codes added in future - as "no level published". +## sell-window: should you sell options today? + +Option sellers get wiped out on a few bad days. sell-window tells you which days to stay out, and on the days it says sell, it hands you the trades: which expiry, which strikes and how to exit. Every session, for SPY, QQQ and IWM. More than 5 million backtests led to this one endpoint. + +```python +from flashalpha import FlashAlpha + +fa = FlashAlpha("YOUR_API_KEY") +answer = fa.sell_window("SPY") + +for window in answer["windows"]: + print(window.get("expiry"), window["action"]) # sell, skip or unavailable + for trade in window.get("candidates", []): + print(" ", trade["structure"], trade.get("mid_credit"), trade["exit"]) +``` + +Each of the four windows (the same-day expiry, then the expiries closest to one week, two weeks and one month out) says `sell`, `skip` or `unavailable`. On a sell it lists the trades, with strikes, quotes and the exit rule. On a skip there is nothing to do. + +| Backtest, 2018 to 2026 | Following sell-window | Selling the same trades every day | +|---|---|---| +| Max drawdown at the same trade size | $255k | $959k | +| Worst month at the same trade size | -$171k | -$307k | +| Sharpe ratio | 0.76 | 0.61 | +| Total P&L at the same max drawdown | $3.55M | $2.32M | +| Worst day at the same max drawdown | -$258k | -$385k | + +Cut the risk or earn more. Keep your trade size and the deepest drawdown falls 73%. Or size up about 3.8 times, until the deepest drawdown matches, and you earn 53% more. These are two separate choices, not one result. + +**Plans:** Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. Backtest with [flashalpha-historical-python](https://github.com/FlashAlpha-lab/flashalpha-historical-python). + +**Read more:** [API docs](https://flashalpha.com/docs/lab-api-strategies-sell-window?utm_source=github&utm_medium=readme&utm_campaign=repo-flashalpha-python&utm_content=sell-window) | [trading guide](https://flashalpha.com/articles/complete-guide-trading-sell-window-spy-qqq-iwm?utm_source=github&utm_medium=readme&utm_campaign=repo-flashalpha-python&utm_content=sell-window) | [every backtested trade](https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026?utm_source=github&utm_medium=readme&utm_campaign=repo-flashalpha-python&utm_content=sell-window) | [sell-window in depth](https://github.com/FlashAlpha-lab/flashalpha-python/blob/main/docs/sell-window.md) | [plans](https://flashalpha.com/pricing?utm_source=github&utm_medium=readme&utm_campaign=repo-flashalpha-python&utm_content=sell-window) | [live forward test on Discord](https://discord.gg/UtH22J8df2) + +*Backtest on SPY, QQQ and IWM, January 2018 to October 2026, at halfway fills and $1 per contract. At one contract set per signal, following it made $945,541 on 20,883 trades; selling every day made $2,321,150 on 56,975. At 3.76 times the contracts, both fell $958,924 at their deepest, and the worst month was -$642,135 against -$307,241. Past results do not guarantee future returns, and short options can lose more than the premium collected. The skip call applies to new trades, so it cannot protect open positions from a sudden shock. This is data, not investment advice.* + ## Data provenance: `data_as_of` Every successful JSON-object response carries `data_as_of`, reporting when each upstream @@ -405,6 +439,7 @@ Get your API key at **[flashalpha.com](https://flashalpha.com)** | `fa.strategy_skew(symbol)` | Strategy signal: skew richness | Growth+ | | `fa.strategy_term_structure(symbol)` | Strategy signal: IV term-structure slope | Growth+ | | `fa.strategy_tail_pricing(symbol)` | Strategy signal: tail (deep-wing) pricing | Growth+ | +| `fa.sell_window(symbol)` | sell-window: should you sell options today? Sell or skip per expiry, with the trades to place (SPY, QQQ, IWM) | Basic+ through Nov 2026, then Growth+ | | `fa.earnings_calendar(...)` | Upcoming earnings calendar | Growth+ | | `fa.earnings_expected_move(symbol)` | Earnings implied-move decomposition | Growth+ | | `fa.earnings_history(symbol)` | Past earnings: surprises, moves, IV crush | Growth+ | diff --git a/docs/api.md b/docs/api.md index c314e45..faaed15 100644 --- a/docs/api.md +++ b/docs/api.md @@ -164,6 +164,7 @@ Decision-support endpoints that score one trading idea and return a unified [str - [`GET /v1/strategies/skew/{symbol}`](#get-v1strategiesskewsymbol) — 25-delta skew / risk-reversal signal `Growth+` - [`GET /v1/strategies/term-structure/{symbol}`](#get-v1strategiesterm-structuresymbol) — ATM-IV term-structure signal `Growth+` - [`GET /v1/strategies/tail-pricing/{symbol}`](#get-v1strategiestail-pricingsymbol) — Downside-tail richness signal `Growth+` +- [`GET /v1/strategies/sell-window/{symbol}`](#get-v1strategiessell-windowsymbol) - Daily sell or skip per expiry with trades to place (SPY, QQQ, IWM) `Basic+ through November 2026, then Growth+` ### Earnings @@ -4758,6 +4759,108 @@ Returns the [strategy decision envelope](#strategy-decision-envelope) with strat --- +## `GET /v1/strategies/sell-window/{symbol}` + +A daily answer to one question, built from more than 5 million backtests: should you sell options today? For four listed expiries (same day, and the expiries closest to one week, two weeks and one month out) it says `sell`, `skip` or `unavailable`. On a sell it also lists the trades to place, with concrete strikes from the option chain, each contract's delta and quote, the credit at mid and the exit rule. On a skip there is nothing to do. + +Covered symbols: `SPY`, `QQQ`, `IWM`. Any other symbol returns every window as `unavailable`. + +The answer is set from the previous session's close and stays fixed for the session. It switches at the close (16:00 ET, 13:00 ET on early-close days): before the close it is for the current session, after the close and on closed days it is for the next session. + +**Plans:** Basic or higher through November 2026, Growth or higher from December 1, 2026. Backtesting from Alpha: the historical API returns the answer for any past session with `?at=`. On it, the strikes come from the archive chain at `at`; the backtests entered at 09:35 ET, so `at=T09:35:00` returns the backtest's entry strikes. From the live launch on, the action and trades match what this endpoint answered at that time. Before it they are rebuilt from the archive with the same rules. See the [backtest](https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026) and the [trading guide](https://flashalpha.com/articles/complete-guide-trading-sell-window-spy-qqq-iwm). + +How strikes are picked (the backtests' contract rules): only out-of-the-money contracts with a bid above zero, a bid/ask spread of at most 20% of the mid and open interest of at least 250. Among those, the contract whose delta is closest to the trade's target and within 0.05 of it; ties go to the tighter spread, then the higher open interest. A trade whose legs cannot all be found in the chain is left out, because the backtest could not have filled it either. + +This endpoint does not return the [strategy decision envelope](#strategy-decision-envelope); it has its own shape, below. + +### Parameters + +| Name | In | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `symbol` | path | yes | - | Underlying symbol. `SPY`, `QQQ` or `IWM`. | + +### Example + +```bash +curl "https://lab.flashalpha.com/v1/strategies/sell-window/SPY" \ + -H "X-Api-Key: YOUR_API_KEY" +``` + +A skip day (SPY, 2026-11-05): + +```json +{ + "symbol": "SPY", + "session": "2026-11-05", + "windows": [ + { "expiry": "2026-11-05", "days_to_expiry": 0, "action": "skip" }, + { "expiry": "2026-11-12", "days_to_expiry": 7, "action": "skip" }, + { "expiry": "2026-11-18", "days_to_expiry": 13, "action": "skip" }, + { "expiry": "2026-12-04", "days_to_expiry": 29, "action": "skip" } + ] +} +``` + +A sell day (SPY, 2026-10-16, replayed at 09:35 ET; the first window and two of its trades shown): + +```json +{ + "symbol": "SPY", + "session": "2026-10-16", + "spot": 772.255, + "quoted_at": "2026-10-16T13:35:00Z", + "legs_indicative": false, + "windows": [ + { "expiry": "2026-10-16", "days_to_expiry": 0, "action": "sell", + "candidates": [ + { "structure": "short_put", "exit": "expiry", + "legs": [ {"side": "sell", "quantity": 1, "type": "put", "strike": 769, "delta": -0.19, "bid": 0.39, "ask": 0.4} ], + "mid_credit": 0.395 }, + { "structure": "short_strangle", "exit": "expiry", + "legs": [ {"side": "sell", "quantity": 1, "type": "call", "strike": 774, "delta": 0.302, "bid": 0.63, "ask": 0.64}, + {"side": "sell", "quantity": 1, "type": "put", "strike": 770, "delta": -0.264, "bid": 0.58, "ask": 0.59} ], + "mid_credit": 1.22 } + ] } + ] +} +``` + +### Response fields + +| Field | Type | Description | +| --- | --- | --- | +| `symbol` | string | The symbol, upper case. | +| `session` | string | `yyyy-MM-dd`. The trading session the answer applies to. | +| `spot` | number | Underlying mid the strikes were picked against. Present when a window sells and an option chain could be loaded. | +| `quoted_at` | string | UTC time of the quotes behind the strikes. Present when a window sells and an option chain could be loaded. | +| `legs_indicative` | boolean | `true` when the quotes are from before that session's 09:35 ET entry: call again at or after 09:35 ET for the strikes to trade. Present when a window sells and an option chain could be loaded. | +| `endpoint_version`, `data_as_of` | | The response envelope fields every response carries. | +| `windows` | array | Always four entries, in this order: the same-day expiry, then the expiries closest to one week, two weeks and one month out. | +| `windows[].expiry` | string | `yyyy-MM-dd`. The listed expiry this window trades: the one closest to 0, 7, 14 or 30 calendar days after the session (within 0, 4-10, 11-17 and 25-35 days). Absent when no listed expiry falls in that range. | +| `windows[].days_to_expiry` | integer | Calendar days from `session` to `expiry`. | +| `windows[].action` | string | `sell`: a session to sell at this expiry. `skip`: stand aside, no new trade. `unavailable`: no answer (a symbol outside coverage or a missing day of data); treat it like a skip. | +| `windows[].candidates` | array | Present only when `action` is `sell`. The trades to consider, in a fixed order. | +| `windows[].candidates[].structure` | string | `short_put`, `short_call`, `short_strangle`, `short_straddle`, `tail_hedged_put` (short put with a far out-of-the-money put bought below it), `put_credit_spread` or `put_bwb` (put broken-wing butterfly: buy one put above, sell two, buy one further below, for a credit). | +| `windows[].candidates[].exit` | string | `expiry`: hold to expiration. `tp50`: close once the position can be bought back for half of the credit or less, otherwise hold to expiration. | +| `windows[].candidates[].legs` | array | The contracts of one unit of the trade. | +| `legs[].side` | string | `sell` or `buy`. | +| `legs[].quantity` | integer | Contracts per unit of the trade (2 for the sold body of `put_bwb`, otherwise 1). | +| `legs[].type` | string | `call` or `put`. | +| `legs[].strike` | number | Strike price. | +| `legs[].delta` | number | The contract's own delta when the answer was served (negative for puts). | +| `legs[].bid`, `legs[].ask` | number | The contract's quote when the answer was served. | +| `windows[].candidates[].mid_credit` | number | Net credit per share at mid: sold mids minus bought mids, times quantity. Multiply by 100 for one unit in dollars. | + +### Errors + +| Status | `error` | When | +| --- | --- | --- | +| 403 | `tier_restricted` | Caller is below the required plan. | + +The historical API version (`GET /v1/strategies/sell-window/{symbol}?at=...` on the historical host, Alpha or higher) also returns `400 invalid_at` when `at` is malformed, in the future, or a time that does not exist in ET (the daylight-saving gap). + +--- + ## Earnings Earnings-event analytics derived from the upcoming/historical earnings calendar plus live options term structure: the calendar, earnings-implied expected move decomposition, IV-crush expectations, post-earnings VRP (implied vs. realized event moves), dealer positioning scoped to the event expiry, and strategy-suitability scoring. Earnings event data (dates, EPS/revenue actuals, fiscal periods, importance) is sourced from Finnhub; live IV, term structure, and exposure are computed from the on-demand options pipeline. diff --git a/docs/sell-window.md b/docs/sell-window.md new file mode 100644 index 0000000..2a260fe --- /dev/null +++ b/docs/sell-window.md @@ -0,0 +1,196 @@ +# sell-window: should you sell options today? + +`GET https://lab.flashalpha.com/v1/strategies/sell-window/{symbol}` + +Option sellers get wiped out on a few bad days. sell-window tells you which days to stay out, and on the days it says sell, it hands you the trades: which expiry, which strikes and how to exit. Every session, for SPY, QQQ and IWM. More than 5 million backtests led to this one endpoint. + +## Two ways to use it + +- **Experienced traders: avoid the bad sessions.** Keep your own strategy and use sell or skip per expiry as a risk switch: trade that expiry when it says sell, stand aside when it says skip. +- **Newer traders: follow the concrete picks.** On a sell, the response lists the expiry, the structure, the exact strikes and how to exit. How much you trade is your decision. + +## Call it + +```python +from flashalpha import FlashAlpha + +fa = FlashAlpha("YOUR_API_KEY") +answer = fa.sell_window("SPY") + +for window in answer["windows"]: + print(window.get("expiry"), window["action"]) # sell, skip or unavailable + for trade in window.get("candidates", []): + print(" ", trade["structure"], trade.get("mid_credit"), trade["exit"]) +``` + +Runnable example: [`examples/sell_window.py`](https://github.com/FlashAlpha-lab/flashalpha-python/blob/main/examples/sell_window.py) + +```bash +FLASHALPHA_API_KEY=... python examples/sell_window.py SPY +``` + +The answer is set from the previous session's close and stays fixed for the session. It switches at the close (16:00 ET, 13:00 ET on early-close days): before the close it is for the current session, after the close and on closed days it is for the next session. + +Without an SDK: + +```bash +curl "https://lab.flashalpha.com/v1/strategies/sell-window/SPY" \ + -H "X-Api-Key: YOUR_API_KEY" +``` + +## Read the answer + +A skip day: + +```json +{ + "symbol": "SPY", + "session": "2026-11-05", + "windows": [ + { "expiry": "2026-11-05", "days_to_expiry": 0, "action": "skip" }, + { "expiry": "2026-11-12", "days_to_expiry": 7, "action": "skip" }, + { "expiry": "2026-11-18", "days_to_expiry": 13, "action": "skip" }, + { "expiry": "2026-12-04", "days_to_expiry": 29, "action": "skip" } + ] +} +``` + +A sell day (one window and one trade shown): + +```json +{ + "symbol": "SPY", + "session": "2026-10-16", + "spot": 772.255, + "quoted_at": "2026-10-16T13:35:00Z", + "legs_indicative": false, + "windows": [ + { "expiry": "2026-10-16", "days_to_expiry": 0, "action": "sell", + "candidates": [ + { "structure": "short_put", "exit": "expiry", + "legs": [ { "side": "sell", "quantity": 1, "type": "put", "strike": 769, "delta": -0.19, "bid": 0.39, "ask": 0.4 } ], + "mid_credit": 0.395 } + ] } + ] +} +``` + +| Field | Meaning | +|---|---| +| `symbol`, `session` | The symbol and the trading session the answer applies to (`yyyy-MM-dd`). | +| `spot`, `quoted_at`, `legs_indicative` | Present when a window sells and an option chain could be loaded. `legs_indicative: true` means the quotes are from before that session's 09:35 ET entry: call again at or after 09:35 ET for the strikes to trade. | +| `windows[]` | Always four entries: the same-day expiry, then the expiries closest to one week, two weeks and one month out. | +| `windows[].expiry`, `days_to_expiry` | The listed expiry this window trades. Absent when no listed expiry falls in the range. | +| `windows[].action` | `sell`, `skip` or `unavailable`. Treat `unavailable` like a skip. | +| `windows[].candidates[]` | Present only on `sell`. Can be empty: a trade whose legs cannot all be found in the chain is left out. | +| `candidates[].structure` | `short_put`, `short_call`, `short_strangle`, `short_straddle`, `tail_hedged_put`, `put_credit_spread` or `put_bwb`. | +| `candidates[].exit` | `expiry`: hold to expiration. `tp50`: close once the position can be bought back for half of the credit or less, otherwise hold to expiration. | +| `candidates[].legs[]` | `side`, `quantity`, `type`, `strike`, `delta`, `bid`, `ask` for one unit of the trade. Absent when no chain could be loaded. | +| `candidates[].mid_credit` | Net credit per share at mid. Multiply by 100 for one unit in dollars. | + +## How strikes are picked + +Only out-of-the-money contracts with a bid above zero, a bid/ask spread of at most 20% of the mid and open interest of at least 250. Among those, the contract whose delta is closest to the trade's target and within 0.05 of it; ties go to the tighter spread, then the higher open interest. A trade whose legs cannot all be found in the chain is left out, because the backtest could not have filled it either. + +## The backtest + +| Backtest, 2018 to 2026 | Following sell-window | Selling the same trades every day | +|---|---|---| +| Max drawdown at the same trade size | $255k | $959k | +| Worst month at the same trade size | -$171k | -$307k | +| Sharpe ratio | 0.76 | 0.61 | +| Total P&L at the same max drawdown | $3.55M | $2.32M | +| Worst day at the same max drawdown | -$258k | -$385k | + +Cut the risk or earn more. Keep your trade size and the deepest drawdown falls 73%. Or size up about 3.8 times, until the deepest drawdown matches, and you earn 53% more. These are two separate choices, not one result. + +Backtest on SPY, QQQ and IWM, January 2018 to October 2026, at halfway fills and $1 per contract. At one contract set per signal, following it made $945,541 on 20,883 trades; selling every day made $2,321,150 on 56,975. At 3.76 times the contracts, both fell $958,924 at their deepest, and the worst month was -$642,135 against -$307,241. Past results do not guarantee future returns, and short options can lose more than the premium collected. The skip call applies to new trades, so it cannot protect open positions from a sudden shock. This is data, not investment advice. + +See [every backtested trade](https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026). + +## Why you can trust this backtest + +- **More than 5 million backtests.** Strategies, deltas, expiries, exits and entry rules, on real quotes from 2018 to 2026. Most rules did not survive. +- **Tested against luck.** Trading less cuts drawdown on its own, so the rule was compared with 1,000 random schedules that skip the same number of days. Across SPY, QQQ and IWM from 2018 to 2026, it beat 90% of them on Sharpe ratio and 83% on drawdown. +- **The bar came first.** For each test, what would count as a pass was written down before running it. Rules built on the VIX curve and on price trend failed, so they were thrown out. +- **What it did not win is reported.** In 2017, a calm year with no selloff to avoid, its Sharpe ratio was 1.55 against 1.70 for selling every day. +- **No hindsight.** Each answer uses only the previous session's close. Trades enter at 09:35 ET at that day's quotes, fill halfway to the bid or ask, pay $1 a contract and are valued every day. +- **Check it yourself.** On Alpha, replay any session since 2018 through the historical API and get the answer it gives, strikes included. + +## Forward test + +A backtest shows the past, so the forward test runs in public. A paper bot follows the sell-window endpoint every session and posts the day's call and P&L on [Discord](https://discord.gg/UtH22J8df2). + +## Plans + +Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. See [plans](https://flashalpha.com/pricing). + +## FAQ + +### Should I sell options today? + +Call the endpoint. For SPY, QQQ and IWM it says `sell`, `skip` or `unavailable` for four listed expiries in the current session. On a skip there is nothing to do. It gives data, not investment advice, and how much you trade is your decision. + +### When should I not sell options? + +When sell-window says `skip` for that expiry. The research behind it is in [When not to sell options](https://flashalpha.com/articles/when-not-to-sell-options-negative-gamma-spy-qqq-short-premium-backtest). + +### Which strikes should I sell? + +On a sell, `candidates` lists the trades with concrete strikes from the option chain, each contract's delta and quote, the credit at mid and the exit rule. The contract rules are in [How strikes are picked](#how-strikes-are-picked). + +### Does it predict selloffs or market direction? + +No. It tells you which sessions to stay out of. It does not forecast direction, and the skip call applies to new trades, so it cannot protect open positions from a sudden shock. + +### Does following it make more money than selling every day? + +Compare the two at the same risk. Sized to the same max drawdown (about 3.8 times the contracts), following it made $3.55M against $2.32M, with a smaller worst day and a deeper worst month. At the same trade size it trades about a third as often, so total P&L is lower ($945,541 against $2,321,150), and the deepest drawdown falls 73%. + +### How was it tested? + +More than 5 million backtests, then against 1,000 random schedules that skip the same number of days. See [Why you can trust this backtest](#why-you-can-trust-this-backtest). + +### Can I check the results myself? + +Yes. On Alpha, replay any session since 2018 with [flashalpha-historical-python](https://github.com/FlashAlpha-lab/flashalpha-historical-python) or the [historical API](https://flashalpha.com/docs/historical-sell-window). + +### I already have a strategy. How do I use it? + +Use sell or skip per expiry as a risk switch: trade that expiry when it says sell and stand aside when it says skip. The tests cover the candidate trades, not your strategy, so replay it on your own trades through the historical API first. + +### Which symbols does it cover? + +SPY, QQQ and IWM. Any other symbol returns HTTP 200 with every window `unavailable`. + +### Which plan do I need? + +Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. + +### How do I follow it going forward? + +A paper bot follows the endpoint every session and posts the day's call and P&L on [Discord](https://discord.gg/UtH22J8df2), so the forward test runs in public. + +### How is it different from the other strategy endpoints? + +The other `/v1/strategies/*` endpoints score one trade idea in a shared decision envelope. sell-window answers one question per expiry and has its own response shape, with no score. + +## Links + +- [API docs](https://flashalpha.com/docs/lab-api-strategies-sell-window) +- [Historical API docs](https://flashalpha.com/docs/historical-sell-window) +- [Trading guide: how to follow sell-window](https://flashalpha.com/articles/complete-guide-trading-sell-window-spy-qqq-iwm) +- [The backtest: every trade, 2018 to 2026](https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026) +- [When not to sell options: the study that started this research](https://flashalpha.com/articles/when-not-to-sell-options-negative-gamma-spy-qqq-short-premium-backtest) +- [Plans](https://flashalpha.com/pricing) +- [Discord: the live forward test](https://discord.gg/UtH22J8df2) + +## Other languages + +- JavaScript / TypeScript: [flashalpha-js](https://github.com/FlashAlpha-lab/flashalpha-js) +- Go: [flashalpha-go](https://github.com/FlashAlpha-lab/flashalpha-go) +- Java: [flashalpha-java](https://github.com/FlashAlpha-lab/flashalpha-java) +- .NET: [flashalpha-dotnet](https://github.com/FlashAlpha-lab/flashalpha-dotnet) +- Backtesting (Python): [flashalpha-historical-python](https://github.com/FlashAlpha-lab/flashalpha-historical-python) + +Keywords: should I sell options today, when not to sell options, sell or skip, option selling API, short premium risk filter, when to sell puts, which strikes to sell, 0DTE sell or skip, credit spread entry timing, SPY QQQ IWM option selling. diff --git a/examples/sell_window.py b/examples/sell_window.py new file mode 100644 index 0000000..100bb67 --- /dev/null +++ b/examples/sell_window.py @@ -0,0 +1,28 @@ +"""Should you sell options today? Print the sell-window answer for one symbol. + +Run: FLASHALPHA_API_KEY=... python examples/sell_window.py SPY +Docs: https://flashalpha.com/docs/lab-api-strategies-sell-window +""" + +import os +import sys + +from flashalpha import FlashAlpha + +symbol = sys.argv[1] if len(sys.argv) > 1 else "SPY" +fa = FlashAlpha(os.environ["FLASHALPHA_API_KEY"]) +answer = fa.sell_window(symbol) + +print(f"{answer['symbol']} session {answer['session']}") +if answer.get("legs_indicative"): + print("Quotes are from before the 09:35 ET entry. Call again at or after 09:35 ET for the strikes to trade.") + +for window in answer["windows"]: + print(f"{window.get('expiry', 'no listed expiry')}: {window['action']}") + for trade in window.get("candidates", []): + legs = ", ".join( + f"{leg['side']} {leg['quantity']} {leg['strike']:g} {leg['type']}" for leg in trade.get("legs", []) + ) + credit = trade.get("mid_credit") + credit_text = f"${credit * 100:,.2f} credit at mid" if credit is not None else "no quote" + print(f" {trade['structure']} ({legs}), {credit_text}, exit: {trade['exit']}") diff --git a/llms.txt b/llms.txt index fed3517..d5851c9 100644 --- a/llms.txt +++ b/llms.txt @@ -9,6 +9,18 @@ This is the live (current-minute) Python SDK. For point-in-time replay of every analytics endpoint going back to 2018, see the companion `flashalpha-historical` package. +## sell-window: should you sell options today? + +`fa.sell_window(symbol)` calls `GET /v1/strategies/sell-window/{symbol}`. Answers "should you sell options today?" every session for SPY, QQQ and IWM: sell, skip or unavailable for four listed expiries, with the trades to place on a sell (strikes, quotes, exit rule). More than 5 million backtests led to this endpoint. Own response shape (`SellWindowResponse`), not the strategy decision envelope. Live answers need Basic or higher through November 2026, and Growth or higher from December 1, 2026. Backtesting any past session needs Alpha or higher. + +Backtest, 2018 to 2026, against selling the same trades every day: at the same trade size, max drawdown $255k vs $959k and Sharpe ratio 0.76 vs 0.61; sized to the same max drawdown, total P&L $3.55M vs $2.32M. These are two separate choices, not one result. Past results do not guarantee future returns, and this is data, not investment advice. + +- Reference: https://github.com/FlashAlpha-lab/flashalpha-python/blob/main/docs/sell-window.md +- API docs: https://flashalpha.com/docs/lab-api-strategies-sell-window +- Backtest: https://flashalpha.com/articles/sell-window-backtest-every-trade-2018-2026 +- Trading guide: https://flashalpha.com/articles/complete-guide-trading-sell-window-spy-qqq-iwm +- When not to sell options: https://flashalpha.com/articles/when-not-to-sell-options-negative-gamma-spy-qqq-short-premium-backtest + ## Data provenance Every response carries `data_as_of`: when each upstream feed last delivered to the node diff --git a/pyproject.toml b/pyproject.toml index 02b8513..ff9eb23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "hatchling.build" [project] name = "flashalpha" -version = "1.3.0" -description = "Python SDK for the FlashAlpha options analytics API — live options screener, gamma exposure (GEX), VRP, delta, vanna, charm, greeks, 0DTE analytics, volatility surfaces, and more." +version = "1.4.0" +description = "Python SDK for the FlashAlpha options analytics API — live options screener, gamma exposure (GEX), VRP, delta, vanna, charm, greeks, 0DTE analytics, volatility surfaces, the sell-window sell-or-skip call for option sellers, and more." readme = "README.md" license = "MIT" requires-python = ">=3.10" @@ -42,6 +42,11 @@ keywords = [ "short vol", "vol selling", "dealer flow", + "sell-window", + "option selling", + "short premium", + "premium selling", + "when to sell options", ] classifiers = [ "Development Status :: 3 - Alpha", diff --git a/src/flashalpha/__init__.py b/src/flashalpha/__init__.py index 4276d8c..eb90279 100644 --- a/src/flashalpha/__init__.py +++ b/src/flashalpha/__init__.py @@ -185,6 +185,10 @@ StrategyStructure, StrategyRiskFlag, StrategyDataQuality, + SellWindowCandidate, + SellWindowLeg, + SellWindowResponse, + SellWindowWindow, StrategyDecisionResponse, # ── Earnings ── EarningsCalendarEvent, @@ -277,7 +281,7 @@ VrpHistoryResponse, ) -__version__ = "1.2.3" +__version__ = "1.4.0" __all__ = [ "DataAsOf", "FlashAlpha", @@ -466,6 +470,10 @@ "StrategyStructure", "StrategyRiskFlag", "StrategyDataQuality", + "SellWindowCandidate", + "SellWindowLeg", + "SellWindowResponse", + "SellWindowWindow", "StrategyDecisionResponse", # ── Earnings ── "EarningsCalendarEvent", diff --git a/src/flashalpha/client.py b/src/flashalpha/client.py index 6148f10..1be31db 100644 --- a/src/flashalpha/client.py +++ b/src/flashalpha/client.py @@ -83,6 +83,7 @@ RealizedVolatilityResponse, ScreenerFieldsResponse, ScreenerResponse, + SellWindowResponse, SkewTermResponse, SpotVolCorrelationResponse, StockQuoteResponse, @@ -1169,6 +1170,26 @@ def strategy_tail_pricing(self, symbol: str, *, expiry: str | None = None) -> St """Scores tail (deep-wing) pricing richness. Requires Growth+.""" return self._strategy("tail-pricing", symbol, expiry=expiry) + # ── sell-window (own response shape) ──────────────────────────── + + def sell_window(self, symbol: str) -> SellWindowResponse: + """Should you sell options today? One answer per session for SPY, QQQ and IWM. + + Built from more than 5 million backtests. For four listed expiries (same day, + then the expiries closest to one week, two weeks and one month out) each + window says ``sell``, ``skip`` or ``unavailable``. On a sell it lists the + trades to place, with strikes, quotes and the exit rule. On a skip there is + nothing to do. Treat ``unavailable`` like a skip. + + The answer is set from the previous session's close and stays fixed for the + session. Any symbol other than SPY, QQQ or IWM returns every window as + ``unavailable``. Requires Basic+ through November 2026, Growth+ from + December 1, 2026. Backtest any session with ``flashalpha-historical`` (Alpha+). + + Docs: https://flashalpha.com/docs/lab-api-strategies-sell-window + """ + return self._get(f"/v1/strategies/sell-window/{_seg(symbol)}") + # ── Earnings Analytics ────────────────────────────────────────── def earnings_calendar( diff --git a/src/flashalpha/types.py b/src/flashalpha/types.py index f5a5b6c..39e5351 100644 --- a/src/flashalpha/types.py +++ b/src/flashalpha/types.py @@ -5509,3 +5509,75 @@ class ScreenerFieldsResponse(TypedDict, total=False): # Response envelope, present on every successful response. endpoint_version: str data_as_of: DataAsOf + +# ── sell-window ───────────────────────────────────────────────────── +# +# Own response shape: not the StrategyDecisionResponse envelope. + + +class SellWindowLeg(TypedDict, total=False): + """One contract of a sell-window trade.""" + + side: Literal["sell", "buy"] + # Contracts per unit of the trade: 2 for the sold body of put_bwb, otherwise 1. + quantity: int + type: Literal["call", "put"] + strike: float + # The contract's own delta when the answer was served (negative for puts). + delta: float + bid: float + ask: float + + +class SellWindowCandidate(TypedDict, total=False): + """One trade to consider on a sell window.""" + + structure: Literal[ + "short_put", + "short_call", + "short_strangle", + "short_straddle", + "tail_hedged_put", + "put_credit_spread", + "put_bwb", + ] + # expiry: hold to expiration. tp50: close at half of the credit, else hold. + exit: Literal["expiry", "tp50"] + # Absent when no option chain could be loaded for the answer. + legs: List[SellWindowLeg] + # Net credit per share at mid. Multiply by 100 for one unit in dollars. + mid_credit: float + + +class SellWindowWindow(TypedDict, total=False): + """One of the four windows, named by its listed expiry.""" + + # Absent when no listed expiry falls in the window's range. + expiry: str + days_to_expiry: int + action: Literal["sell", "skip", "unavailable"] + # Present only when action is "sell". Can be empty: a trade whose legs + # cannot all be found in the chain is left out. + candidates: List[SellWindowCandidate] + + +class SellWindowResponse(TypedDict, total=False): + """sell-window answer from ``GET /v1/strategies/sell-window/{symbol}``. + + ``spot``, ``quoted_at`` and ``legs_indicative`` are present when a window sells + and an option chain could be loaded. + When ``legs_indicative`` is true the quotes are from before that session's + 09:35 ET entry: call again at or after 09:35 ET for the strikes to trade. + """ + + symbol: str + # yyyy-MM-dd. The trading session the answer applies to. + session: str + spot: float + quoted_at: str + legs_indicative: bool + # Always four entries: same day, then about one week, two weeks, one month. + windows: List[SellWindowWindow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf diff --git a/tests/test_client.py b/tests/test_client.py index cabca42..51e616b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -944,3 +944,52 @@ def test_custom_base_url(): def test_custom_timeout(): fa = FlashAlpha("key", timeout=10) assert fa.timeout == 10 + +# ── sell-window ───────────────────────────────────────────────────── + + +SELL_DAY = { + "symbol": "SPY", + "session": "2026-10-16", + "spot": 772.255, + "quoted_at": "2026-10-16T13:35:00Z", + "legs_indicative": False, + "windows": [ + { + "expiry": "2026-10-16", + "days_to_expiry": 0, + "action": "sell", + "candidates": [ + { + "structure": "short_put", + "exit": "expiry", + "legs": [{"side": "sell", "quantity": 1, "type": "put", "strike": 769, "delta": -0.19, "bid": 0.39, "ask": 0.4}], + "mid_credit": 0.395, + } + ], + }, + {"expiry": "2026-10-23", "days_to_expiry": 7, "action": "skip"}, + {"expiry": "2026-10-30", "days_to_expiry": 14, "action": "skip"}, + {"action": "unavailable"}, + ], +} + + +@responses.activate +def test_sell_window_calls_route_and_returns_the_answer(fa): + responses.get(f"{BASE}/v1/strategies/sell-window/SPY", json=SELL_DAY) + answer = fa.sell_window("SPY") + assert answer["windows"][0]["candidates"][0]["legs"][0]["strike"] == 769 + assert "expiry" not in answer["windows"][3] + assert "?" not in responses.calls[0].request.url + + +@responses.activate +def test_sell_window_below_required_plan_raises_tier_restricted(fa): + responses.get( + f"{BASE}/v1/strategies/sell-window/SPY", + json={"error": "tier_restricted", "message": "Requires Basic plan.", "current_plan": "Free", "required_plan": "Basic"}, + status=403, + ) + with pytest.raises(TierRestrictedError): + fa.sell_window("SPY")