Files
OpenBB/openbb_terminal/stocks/options/options_chains_model.py
Theodore Aptekarev 8b95d19b43 OpenBB Platform v4 (#5225)
* change location

* check something

* add cache

* fixed auth w/@IgorWounds and @montezdesousa

* fixes for econometrics

* include Darren auth suggestions

* some routers included if dev + chg hub backend var

* coverage router should not be here

* update docker readme

* forgot to check auth here

* Change extension imports

* update docstrings and readme

* headers function wasnt being called, just referenced

* relative import

* econometrics add headers everywhere

* Fix QA

* Fix TA

* Fix test

* Fix python interface QA and TA

* Lint

* Cleanup

* Change path

* make the changes and some etf models

* tests for command_runner

* ruff

* solving naming issues on pytest

* Update some docstrings, fix typos and lint

* solving econometrics instantiating wrong openbb

* chaging the way pytest runs

* messing around with the imports

* ruff

* Change template to add platform considerations (#5533)

* change template to add platform considerations

* @the-praxs suggestions

* solving __pycache__ issue

* removing typo

* ci yml

* making linters happy (i guess)

* Improve Omega Ratio and docstrings

* chaging pro validation hash

* ruff

* Fix OLS Summary and lint

* Add OECD Provider for economy extension (#5534)

* Add record to docs

* update a couple of cboe models

* Fix econometrics bugs. Make tests pass. Lint.

* black

* ruff

* pydantic v2: remove deprecated Extra

* pydantic v2: self.dict -> self.model_dump

* revert pydantic

* revert the revert

* try 1: pin urllib3 < 2

* Revert "try 1: pin urllib3 < 2"

This reverts commit 3736c311cb.

* Revert dep

* Fix

* re-lock

* Skip all tests when doing cspell in pre-commit

* Resolve linter errors

* Add OECD to install script

* Add OECD

* Resolve more linter errors

* Exclude static files of v4 python from ruff

* More linting

* Another attempt to resolve even more linting errors

* Change how Literal is used

* ticker. symbol. consistent. @piiq

* change ticker field in Stock Search to is_symbol

* Update Cboe model for stocks_search -> is_symbol parameter.

* Fix issue

* Lint

* Small QA to rename SDK to Platform (#5544)

* SDK -> Platform in docs

* renamed in core

* renamed in openbb `init`

* `sdk` variable renamed as `platform`

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>

* `validator` -> `field_validator` in standard models

* Standard convention for multi-ticker symbols  (#5549)

* standardize symbol input param in forex and crypto historical data

* static

* linting

* Fix stock_search integration test params

* Update dependencies and linting on the CI (#5550)

* Remove ruff and black from platform dependencies

* Update codespell configuration

* Lint python code

* Add toml and tomli to root dev dependencies

* Update requirements.txt files

* Fix buggy docstring in python package methods (#5548)

* fix buggy docstring for methods

* remove space

* fix typing

* avoid name collision with validator

* Revert "avoid name collision with validator"

This reverts commit 8a30a44555.

* typing

* rebuild

* python 3.8, ParamSpec not available

* lint

* ruff noqa

* Merge with develop (#5551)

* Charting extension integration tests (#5547)

* initial proposal for integration tests on the charting extension

* int tests for charting generation

* python generator

* ruff

* boilerplate templates

* python tests

* tests for the api

* using body instead so data don't get redefined

* unit tests for coverage

* ruff

* docstrings

* unnecessary access to keys()

* Add trading economics economic calendar

* dev install

* Data Models disposition on the website (#5538)

* pydantic v4

* flat structure

* md files

* Revert "md files"

This reverts commit bd2170e2ad.

* improve command descriptions

* nice title and description

* human readble data model names on left panel

* implementation details section

* content markdown files

* markdown versioned files

* showcase page

* updates

---------

Co-authored-by: tehcoderer <me@tehcoderer.com>

* everything except tests

* fix `datetime.date` import in fetcher test

* modified url to remove `api_key` placeholder

* record test

* api key is called c here

* fixed fetcher test for api key

* recorded tests

* refactor

* life is ruff

* codespell

* Make Intrinio options chains great again (#5540)

* add multithreading

* cleanup

* linting

* use dateutil.parser

* removed `TICKER_EXCEPTIONS`

* modified `date` and `type` fields in standard model

* set alias for `date` and `type` to match standards

* static

* static

* Hotfix/fix post tests (#5555)

* Fix econometrics

* Fix ta

* Fix qa. Remove intrinio to not be rate limited

* Lint

* Fix typo

* obbject tests (#5558)

* obbject tests

* ruff

* fix dev_install

* fix unittests

* black on nox

* black on nox

* nox

* fix fetcher test so it can compare dates

* fix intrinio tests

* ruff

* change to model_dum() instead (#5561)

* Passing tests (#5560)

* Passing tests

* fix options

* Fix most tests and bugs

* fix forex missing params

* make symbol upper in polygon fx pairs

* fix fixedincome integration tests (#5564)

* Fixing charting api integration tests (#5562)

* fixing charting api integration tests

* black

* fix stocks less five API fails

* fix the 5 failing stocks integration tests

* recapture fmp stock news unit test

* fix fmp forex api test

* couple more forex integration fixes

* changing the request to always use the legacy session. @jmaslek this is was broken for 3.8 and 3.9; with this change apparently all versions work - lemme know if you see any issue with this change

* ruff

* fix ta

* add missing param

* fix testers for the api

* fix globalnews to use images instead of image

* remove unncessary pandas import

* econcal int tests

* update gitignore

* fix get_querystring to support lists

* add missing parameters

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
Co-authored-by: Henrique Joaquim <h.joaquim@campus.fct.unl.pt>

* Stock Historical standardization and UX improvements (#5457)

* polygon add interval standard param

* attempt at standardization

* Alpha Vantage params standardization

* rename `get_intervals` and fixed bug for extracting wrong value

* missed changing it in `extract_data`

* linting

* polygon cleanup

* modified `start_date` & `end_date` type to `datetime` from `dateType`

* standardized intrinio stock historical params

* fixed yfinance

* linting

* revert start and end date to dateType from datetime

* fix alpha vantage

* changed param

* fix intrinio infinite loop

* `validator` is `field_validator`

* revert time params and fixed intervals

* `outputsize` alias set in `Field`

* set interval description using in-build dict

* yfinance stock param standardization

* static

* intrinio cleanup

* Polygon add api params as `PrivateAttr`

* modified yfinance params to `PrivateAttr`

* intrinio remove overriding date params

* fixed description for AV adjusted param

* fix alpha vantage intraday not working

* re-record and pass unit tests

* pass integration tests

* Update test

---------

Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>

* Fix econcal endpoint (#5567)

* fix econcal for python interface

* fix econcal tests

* fix non supported json output

* Fixes FMP index endpoint to get 1 day interval data (#5535)

* fix FMP endpoint for getting index data for `1day` interval

* fix `open` type to prevent validation error

* modifed `validator` to `field_validator`

* type of fields is `StrictFloat`

* re-record and pass test

* linting

* `validator` -> `field_validator`, a3'd everything

* Fix regression in tests. Lint

* updating the integration test actions

* Fix typo

* Fix typo

* Fix import

* Publish versions `a3` to Pypi (#5568)

* bump versions and script to publish

* small improvements

* uncomment exception

* openbb

* publishing guidelines

* improve docs

* add providers to economy api

* make symbol required in stock_quote

* Fix keys

* Fix oecd

* add comma

* revert

* Add quotes

* Sleep

* Improve tests

* Improve

* Feature/query transform (#5566)

* inject query into transform_data

* swap query and data

* update missing fetchers

* last test failing

* sig test fetcher

* black

* av add `query` to `transform_data` call

* transform_data: missing refactoring

* fix uvicorn command in the doc

* Automated examples (#5576)

* Automated examples

* Fix bug

* Lint

* static assets built on 3.8

* Revert "static assets built on 3.8"

This reverts commit fb82ff7bd9.

* remove assets

* static assets on 3.8

* static assets on 3.8

* Wrap in quotes

* Improve wrap

* minor fix

* rebuild 3.8

* Fix bug

* Fix edge case

* Lint

* rebuild

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Diogo Sousa <montezdesousa@gmail.com>

* use SecretStr for sensitive fields (#5578)

* use SecretStr for sensitive fields

* fix

* docstring

* Add tests for provider (#5583)

* Add tests for provider

* Test standard models

* Lint

* Fix standard model issues.

Co-authored-by: @the-praxs

* linting

* `symbol` -> `is_symbol` in `economy.index_search`

* static

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* feature/v4-biztoc-provider: Adds Biztoc provider and GlobalNews (#5546)

* add biztoc provider

* duplicate field

* black

* request headers

* recapture test

* move stuff around

* add integration tests

* duplicate OECD entry in dev_install

* duplicate entry for oecd in pyproject.toml

* Add Biztoc to workflow

* update readme: credentials

* Add Preferences to README

* improve tests

* fix biztoc

* obbject pylint

* Bug fix: serializ SecretStr before dumping to json

* export credentials as json (#5592)

* Check for wrong, changed, or outdated params. (#5597)

* Improving `economy.cpi` for the FRED data (#5602)

* remove multiindex using coding magic

* Update field descriptions

---------

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Improve error message.

* Improve

* Test tests (#5607)

* Test tests

* Add test

* 4.0.0a4 PyPI publishing (#5608)

* changes to publish

* fix mini conflict

* typo

* cleaned `descriptions.py`

* fix description in `cpi.py`

* Add ETF extension to v4 (#5609)

* Add etf extension with search data model

* Add FMP ETF search

* Add fetcher tests

* Add ETF historical price from yfinance (#5610)

* Add etf extension with search data model

* Add FMP ETF search

* Add fetcher tests

* Add etf historical price standard model and route

* Add etf historical data from yfinance

* Add unit tests

* Fix linter error. Organize imports

* Update test data

* Revert common descriptions erased by CPI PR

* remove `symbol` from data for `stocks.ca.peers`

* Test docstrings (#5613)

* Test docstrings

* fix minor bugs w/@IgorWounds

* static only w/@IgorWounds

* fix `importance` param in `economy.econcal`

* right static only w/@IgorWounds

* Fix econcal and add tests to noxfile

* fix econcal static

* re-record

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* Clean up the newly created ETF extension (#5611)

* Add ETF to dev_install bundle

* Add integration tests for openbb-etf

* Patch integration test generator scripts to produce lintable code

* Add py.typed markers for extensions

* user preferences descriptions (#5593)

* user preferences descriptions

* typo

* Feature/v4-sec-provider: Add SEC provider to V4 (#5556)

* coerce string type, add default=None

* add sec provider and FTD function

* update description

* ruff

* data model definitions in standard model already

* duplicate oecd entries in dev_install and pyproject

* actually commit

* integration test

* add stock search to SEC

* add company filings for SEC provider

* integration tests

* remove duplicated line

* ruff

* **kwargs in Transform Data

* some helper functions for later

* black

* add helper for mutual fund and etf lookup

* add some SEC lookup helpers to a new regulators extension

* black

* ruff

* fix test param

* take institution filter out of sec stock_search

* ruff

* remove URL that codespell doesn't like

* add cik-to-ticker map

* add litigations rss feed

* make openbb-regulators a dependency of openbb-sec

* add SIC search

* improve ftd and remove BeautifulSoup from SEC dependencies

* fix parsing the zip files pre-2009

* ruff

* update descriptions

* recapture ftd test

* tests params

* typo

* change limit param to 1 in test_sec_stock_ftd

* Add didiers to_records method (#5588)

* Add didiers to_records

* unused import

* Bring back polars + do what igor suggested

* Make the output consistent with what it was.

* Black ?

* spelling.  oops.  almost like we have a pre commit or something

* Feature/v4-price-performance: % returns for various horizons, comparable across multiple tickers. (#5618)

* coerce string type, add default=None

* add price_performance endpoint from FMP

* black

* extra period in docstring

* ruff

* forgot one file

* Feature/ipo-calendar: V4 IPO Calendar (#5605)

* coerce string type, add default=None

* add intrinio ipo calendar

* fix typo

* sort __init__

* fix description

* make requested changes

* update test params

* black

* test updates

* etf integration

* Add disc router and WSJ provider

* OBBject extensions (#5612)

* changes to core

* bug?

* docstring

* doc

* Update credentials.py

* fix model_dump

* create extensions

* fix docstring

* doc

* revert change query_exc

* doc

* fix container test

* redirect obbject test patch

* doc

* rename method

* move decorator to extension file

* rename method extend_obbject

* changes in creds model

* avoid credential racing

* doc

* rename prop

* doc

* doc

* remove comment

* comment some code

* free extension names

* docstring

* doc

* docs

* Hotfix/fix-v4-economic-calendar: Add FMP to Econ Calendar and fix some TE bugs (#5580)

* coerce string type, add default=None

* adds fmp to economic calendar and fixes some things with TE

* ruff

* etf integration test

* missing provider for test

* rename te cassette to economic_calendar

* fix test

* rename tests

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>

* Add stocks disc endpoints (#5630)

* Add stocks disc endpoints

* Explicit

* Lint

* Add tests

* Bump timeout

* Feature/disc router (#5616)

* Add disc router and WSJ provider

* Lint

* Lint

* Move endpoints under ETF router

* Update etf_performance.py

* Add ETF info and sector exposure from FMP (#5620)

* Add ETF info from FMP

* Add integration tests

* Add unit tests

* Add etf sectors endpoint

* Add etf sectors fmp endpoint

* Omit nested sector list from fmp info data

* Update tests and test data

* Update test data

* Specify provider explicitly in integration tests

* Update tests

* Skip yfinance etf historical test

* Fix linter errors after the merge

* Fix more linter errors

* Fix failing and add missing tests

* Fix integration tests

* Fix calendar ipo tests

* Add ETF holdings and holding report dates from FMP (#5629)

* Add ETF info from FMP

* Add integration tests

* Add unit tests

* Add etf sectors endpoint

* Add etf sectors fmp endpoint

* Omit nested sector list from fmp info data

* Update tests and test data

* Update test data

* Specify provider explicitly in integration tests

* Update tests

* Skip yfinance etf historical test

* Add etf holdings and holdings date routes and standard models

* Add fmp holdings and holding dates fetchers

* Update holdings fetcher to properly handle date object inputs

* Add unit tests

* Add integration tests

* Black after merge

* Fix test input data format

* Disc router for equities. (#5640)

* Disc router for equities.

* add tests

* Bug fixes in financial statements (#5537)

* `validator` to `field_validator` in standard models

* Polygon add `ttm` to `period` param

* Revert "Polygon add `ttm` to `period` param"

* Polygon add `ttm` to param `period`

* `EarningsCallTranscript` standard model removed `quarter` param

* use `tag` instead of `name` for correct field name

* fix url for intrinio income statement

* modified intrinio cash flow `transform_data` function

* set optional data fields and validate dates

* fix insider trading not fetching recent data

* linting

* refactoring

* refactoring

* set param alias in fmp stock insider provider model

* get all quarter data for intrinio

* add `limit` param to `stocks.fa.ins`

* refactoring and linting

* removed additional comment

* linting

* re-recorded and fix tests

* cash flow standard model field type to `StrictFloat`

* Intrinio cash flow model fixed to get all quarters' data

* linting

* refactoring

* financial statement standard model field type to `StrictFloat`

* Intrinio financial statement model get all quarter data

* set `period` type to `Optional[str]`

* add `period` param to balance sheet provider models

* add `period` param to cash flow provider models

* use alias in model dump

* remove `ttm` from `period` param

* revamped intrinio financials

* fixed incorrect description for `limit` param

* modified tests for `balance`, `cash` and `income`

* re-recorded tests

* skipped intrinio financial statement fetcher tests

* Add ETF county weighting command with FMP data (#5645)

* Remove code that wrongfully infers the filing date as end of quarter

* Add etf countries command with fmp data

* Add countries command to etf router

* Update sector and country weighting fmp models

* Fix docstring typo

* Add unit tests and update mock data

* Add integration tests for etf.countries

* Bring back missing integration tests

* Fix economic calendar integration tests

* fix missing data in intrinio financials

* removed inutile params from Polygon financial statement models

* Intrinio IPO overhaul

* tests fixed and re-recorded

* Add ETF holdings performance with FMP data (#5659)

* Add etf holdings performance endpoint

* Add tests

* Fix failing polygon fetcher tests

* Split long holdings lists into chunks and update test data

* Change chunk length to 500 tickers

* Add stock search from FMP (#5642)

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Feature/v4 pypi (#5579)

* improvements to the publish script

* move scripts

* initial script that builds the package

* adjustments on the docs

* test pypi workflow

* add hour and minute to version

* black

* ruff

* duplicate file

* moving files instead

* build throught the script instead

* update nox file

* integration tests workflow

* change workflow

* fix dev_install

* fix in sed cmd

* testing the platform only

* commenting out the publishing

* fix run commands

* improvements to the publish script and doc

* fix publish script

* updating the script and the workflow

* feature/v4-pypi nightly (#5589)

* init

* Update nightly.py

* cleanup

* init file

* not referencing dev_utils

* updating obb platform key

* revert

* Increase warmup time

* project name on pyproject and adding the --pre flag on readme

* moving to the build folder

* fix noxfile

* reflecting the moves on the import statements

* fix pyproject path

* ruff

* fix paths

* fix super typo

* update workflows

* moving the pypi terminal files into its folder

---------

Co-authored-by: teh_coderer <me@tehcoderer.com>
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* economy.calendar nasdaq (#5628)

* add quandl econcal

* unittest

* add integration tests

* replace &nbsp

* fix bug TE importance

* small fix

* remove stuff from quandl

* add nasdaq provider

* add nasdaq to install files

* Update test_economy_api.py

* test

* quandl leftovers

* remove quandl econcal test

* lint

* replace &nbsp

* ruff

* lint

* update nasdaq gmt

* validate countries

* here

* no need for this

* fix calendar tests

* static

* static

* remove error

* static

* readme + website

* readme

* remove changes TE

* typo

* fix unittest

* fix tests

* Update __init__.py

* Update economic_calendar.py

* revamped model and added helpers for date range

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* Feature/v4-dividend-calendar: Adds Nasdaq dividend calendar and moves the command out of fundamental_analysis (#5672)

* add nasdaq dividend calendar and move out of fundamental_analysis

* black

* ruff

* add random-user-agent to pyproject.toml

* add nasdaq ipo calendar

* add nasdaq flag for SPO to IPO calendar

* add spo test param

* black

* dividend test

* fix transform_query - nasdaq_calendar_ipo

* bring back different headers for ipo calendar

* remove references to test

* Add disc endpoints (#5660)

* Add Quandl top retail

* Add tests

* Add Seeking Alpha

* Add Filings

* Change Provider

* Fix test

* Add dev_install

* static

* static correct

* y u forget me

* updated deps

* static

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* using dev install (#5673)

* Expanding the `fixedincome` menu (#5655)

* models and fetcher

* making the fetcher visible

* std model

* adding the command

* improvements to the models

* typo

* adding the command

* imrpovements to dwpcr

* standard model for ecb int rates

* adding the fetcher for fred

* standard model of ice bofa

* add ice_bofa command

* ruff

* ice bofa fetcher

* moody std model

* moody

* std model

* add cp command

* unused import

* cp

* spot

* ruff

* hqm

* tmc

* ffrmc

* tbffr

* ruff

* typing fixes

* buggy dates

* fred fetcher tests

* ruff

* adding ecb as a provider

* adding new provider related stuff

* adding the new command eu_ycvr

* models

* test for ecb fetcher

* Feature/v4 fixedincome int tests (#5661)

* integration tests

* happy ruff

* mini adjustment

* ruff

* typo

* verbose type

* fix tests

* docstrings everywhere and an happy pydocstyle

* fix tests

* Feature/user-cache-dir: Add User Preference for the Cache Directory (#5621)

* add user preference for storing cached data

* ruff

* fix tests?

* fix test key error..?

* etf integration test fix

* add cache dir to SEC

* black

* improve docs structure for V4 (#5586)

* improve docs structure for V4

* moving over changes

* Add Docs to the navbar

* add some, re-order sidebar

* fix sentence

* add docs install to source instructions

* insert a couple of links, fix a typo

* big terminal docs usage revamps

* improve toggle component for tutorials

* get rid of TOCs for all terminal docs for consistency and simplicity

* improve platform docs

* improve bot docs content structure

* remove index from bot docs

* improve ON THIS PAGE to only display TOC if at least 1 element

* make edit this page text and icon on same line

* add author and date to docs

* add basic syntax page to usage

* sidebar order for usage section

* move api start command to the top with the import python import

* some install page additions

* small improvement

* refactor routines section

* Update introduction-to-routines.md

* Update pipeline-of-commands.md

* Update introduction-to-routines.md

* Update routines-for-power-users.md

* Update community-routines.md

* update keyboard hotkey macros

* askobb page

* Update askobb-feature.md

* Update index.md

* Update index.md

* Update index.md

* Update index.md

* fix images

* remove HeadTitle from all files where it exist

* get rid of extra spacing in markdown

* small update

* small edit

* need index at the start because of main path when homepage of docs

* use openAI to create SEO for front matter on my behalf

* remove table of content from sdk page

* finish improving metadata SEO for SDK docs

* update duplicate header

* update HeadTitle to be more specific on product

* remove index from bot telegram reference

* delete duplicated title for platform reference

* another update

* Hotfix/econometrics export filename fix (#5508)

* fix: Modify 'load' behavior to append data to existing sheet

Details:
- Added logic to detect existing sheet and append data if present.
- There is no direct way to append on the sheet, so I am reading the sheet to get the last row number and then writing after that row number.

Resolves: #5472

* Added --export option for exporting to a desired file in /econometrics menu

* Added a -f/--file option to provide the filename for export.
Made -t/--type and -f/--file mutually exclusive.
If -f is not given, -t is required and the filename will be generated based on the time and module.

* Revert `helper_funcs.py` to original state

* Made some arguments not required.

* Minor changes asked by @deeleeramone

* No sheet name

* black

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>
Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>

* FIX #5600 Updated the Twitter logo to X logo (#5601)

* Change Ultima paragraph (#5624)

* change ultima paragraph

* remove old paragraph

* Update CONTRIBUTING.md (#5623)

* mobile line-brake fixed (#5604)

In the mobile view (media max 400px ), the text line "Installation >>" appears broken into smaller segments.

Co-authored-by: James Maslek <jmaslek11@gmail.com>

* start PR so screenshots can get attached via GitHub website

* Update qa.md

* Docs/terminalpro and improvement (#5622)

* add first iteration of content for Terminal Pro

* Update index.md

* Update dashboards.md

* Update folders.md

* Update grouping.md

* Update report.md

* Update templates.md

* Update quick-start.md

* Update index.md

* Update index.md

* Update home.md

* Update news.md

* Update index.md

* Update index.md

* Update index.md

* Update data-provider.md

* Update chat-with-widget.md

* Update data-manipulation.md

* Update forecasting.md

* fix: images self closing tags

* fix: only pro visitors can see pro

* inter font

* improve SEO content and restructuring

* add new package for toggle on tutorials

* move folder around

* improve main page of each prod

* small updates and improvements

* fix logo above

* small improvement in headtitle for portfolio funcs

* fix typos from OpenAI's GPT-4

* fix _category_ json

* remove generic SEO words

* add script that generates SEO for documentation

* add missing file

* remove sdk warning message

* fix links for andrew

---------

Co-authored-by: jose-donato <zmcdonato@gmail.com>
Co-authored-by: jose-donato <43375532+jose-donato@users.noreply.github.com>
Co-authored-by: andrewkenreich <andrew.kenreich@gmail.com>

* Add light blue banner, blue when clicking sidebar + fix link in docusaurus.config.js (#5627)

* typo

* pro docs tab coloring

* sidebar selection blue

* redirecting missing

* Update crypto introduction.md

* titles

* another title

* Update crypto dd.md

* Update defi.md

* Update disc.md

* Update nft.md

* Update onchain.md

* Update ov.md

* Update introduction.md

updates URL

* Update introduction.md

add HeadTitle

* Update ba.md

* Update comparison.md

* Update dark-pool-shorts.md

* Update disc.md

* fix docs (#5646)

* Update fa.md

* Update gov.md

* Update ins.md

* Update tradinghours.md

* Delete website/content/terminal/menus/stocks/options/screener.md

* Update Options introduction.md

* Update screener head title

* Update screener TOC

* Update econometrics.md

* Update economy head title

* Update etf.md

* codespell

* Fix the redirect for intro to the correct url for docs (#5658)

* fix the redirect for intro to the correct url for docs

* fix code linting

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>

* Contributing guidelines (#5615)

* intro section

* what's expected sections

* improve sections

* improve sections and move how to add a data point

* moving qa under contributor

* better intro

* moving the sharing section

* better expectations

* small intro to the contributor guidelines

* moving things under the contributor section

* there is no pre commits on the platform yet

* dependency management

* best practices for extensions

* typo

* collapse options subfolder

* codespell

* Improve Pro documentation further (#5641)

* small improvement

* Fix TutorialVideo for mobile view

* main page terminal pro docs

* jose whelp

* placeholder index file that needs to be removed

* fix: fixes sidebar, removes unused index.md

* update link to api keys

* update api keys link

* fix hyperlinks in terminal helper

* fix a few more links

* fix some more links

* linter

* black linter

* improve main docs page for each product

* small refactor

* header to take to main page of product

* run OpenAI script to generate SEO for pro content

---------

Co-authored-by: jose-donato <43375532+jose-donato@users.noreply.github.com>

* Update folders.md

* Update integrate-your-own-backend.md

* Update index.md

* Update fixedincome.md

* url

* Update forecast.md

* Update settings.md (#5665)

* Update forex.md

* fix front matter (#5667)

* Fix conflict markers that got into markdown during previous merges

* Fix front matter list formatting

* Fix typo

* small thing

* fix hyperlinks

* Update .codespell.ignore

* output_type options

* Architectural considerations

* Update sidebar to hide pro section by default

* Minor copy changes

* TET pattern

* import statements

* clarification on preferences

* Minor copy changes

* Lint markdown and python

* fix: open index on collapsible root elements

* remove old content

* options title

* Fix linting errors

* Refactor sidebar label to url mapping condition

---------

Co-authored-by: andrewkenreich <andrew.kenreich@gmail.com>
Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Sandip Saha <sandipsmoto@gmail.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
Co-authored-by: professssor <62937125+professssor@users.noreply.github.com>
Co-authored-by: Farookh Zaheer Siddiqui <129654632+FarukhS52@users.noreply.github.com>
Co-authored-by: MAX SHESTOV <57421753+maxxsh@users.noreply.github.com>
Co-authored-by: jose-donato <zmcdonato@gmail.com>
Co-authored-by: jose-donato <43375532+jose-donato@users.noreply.github.com>
Co-authored-by: Mohamad Fadil <fadil.parves@gmail.com>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Add multi-ticker support to etf_countries (#5670)

* add multi-ticker support to etf_countries

* remove commented-out lines

* normalized percent points

* percent -> percentage

* Fix Nasdaq Econ Calendar Fetcher (#5668)

* fix nasdaq econ cal request

* more html tags

* add random-user-agent to pyproject

* another edge case - 24H -> 00:00

* Feature/v4-unusual-options: Adds unusual options endpoint. (#5677)

* add unusual options fro Intrinio

* test params

* move sorting

* Change type

* update FMP fetcher to use the V4 endpoint (#5681)

* Fix price performance not parsed correctly when there is only 1 ticker (#5678)

* Add crypto search standard model and FMP data for it (#5679)

* Apply renaming to news data (#5684)

* inegration complete

* reformatted for black

* fix ruff issues

* revised integration

* changes to versions in pyproject.toml and updated lock files

* Rename models

* Update tests

* Fix typos here and there

* Update routers

* Apply rename to ultima

* Properly mock api key

* Mock ultima api key

---------

Co-authored-by: Adi Sai <adithya.sairam1@gmail.com>

* Feature/v4-market-snapshots: Adds full market snapshots (#5473)

* add stocks market snapshots from fmp

* add fmp fetcher test

* unused imports

* add polygon to market_snapshots

* update fmp market_snapshots

* black

* pydantic2 things

* updates

* change validate_obj to model_validate

* polygon cleanup

* tests

* descriptions

* black

* data description

* minor adjustments, using less local variables, improving readability and removing pandas

* better structure

* minor adjustments

* lint

* fix int tests

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>

* Add NBBO Quotes, with historical, to `obb.stocks.quote()` (#5617)

* coerce string type, add default=None

* add polygon stock quote

* limit cleanup

* add greater/less than to params

* limit param

* black

* tests

* fix tests

* test_etf

* rename model and function to NBBO

* add standard model for nbbo

* improve standardization

* not redifining builtin max

* fix input params

* integration test params

* removing unused import

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* revamped code

* re-recorded nbbo test

* added symbol validator

* added alias in field

* Stock news -> Company news

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* hotfix/update-quandl-to-nasdaq: Merge Quandl + Nasdaq (#5682)

* merge quandl nd nasdaq

* dev_install.py

* test param name

* economy python integration test

* Add more input types to data processing commands (#5453)

* adding more supported types to data processing commands

* ruff

* removing unused import

* docstrings

* data processing types covertion

* data description

* test filters

* ruff

* fix package builder tests

* ruff

* removing print

* Add stocks/dps module (#5638)

* short volume

* pyproject missed

* add sec short interest with days to cover

* Add FINRA OTC endopoint

* some tests

* rest of unit tests

* Integratin test scripts

* dev install

* linters caught by pre commit

* lines too long

* Last test files

* maybe it needs a lock file?

* thanks pratty

* Maybe ill actually listen to pratty next time

* sec test date freeze

* I cant hit buttons

* re record tests

* tests 3

* Stockgrid test

* regen int test

* more testing

* lint

* moved these fns

* dis killing me

* one more test

* last one

* Update integration tests

* Fix tests picking up wrong dps route

---------

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Rename Forex to Currency (#5687)

* restore fields as optional (#5686)

* Rename Crypto (#5689)

* added `crypto.price.historical`

* static

* fix tests

* fix incorrect label

* Move model from sec to finra (#5692)

* Update toml

* fixed deps

* add openbb-etf lock file

* cleanup

* cleanup (#5694)

* Rename Fixed Income (#5695)

* Rename Fixed Income

* Add routers

* Rename stocks to equity (#5688)

* Rename stocks folder

* Rename openbb_stocks package

* Rename routers

* Reroute equity menu

* Update routes based on feedback

* StockNews -> EquityNews

* StockInfo -> EquityProfile

* Rename Equity calendars

* Equity peers rename

* Rename discovery

* Rename fundamental

* Rename price command models

* Rename shorts

* Update charting router

* Fix seeking alpha upcoming release date

* Fix linting errors

* Update tests

* Fix tests

* Skip econometrics api tests because it's python only

* Update integration tests and test data

* Historical analyst estimates

* Skip cboe options chain test. Unable to record vcr cassette

* re-record cboe cassettes

* Document CBOE unit test creation caveats

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>

* Feature/files-for-igor: Adding a few standard models for Igor (#5699)

* add some standard models for Igor

* codespell

* update descriptions

* Add devtools metapackage (#5703)

* Add openbb-devtools metapackage

* adding devtools to the dev_install script

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>

* Rename econometrics commands to be more verbose (#5702)

* Expose command to model map (#5706)

* Rename ta qa (#5708)

* Rename ta folder

* Rename ta to technical

* Rename qa folder

* Rename qa to quantitative

* Rename the actual routes in quantitative

* Equity cleanup (#5707)

* cleanup

* static

* renaming to equity cc @the-praxs

* Proof reading

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Don't enforce imperative for first word of the docstring

* Remove ultima from default extensions

* Rename `economy` and create `index` extension (#5705)

* renamed economy and re-recorded tests

* static

* market indices

* european indices

* fred indices

* index constituents

* tests

* correct package name

* oops

* static

* linting

* Move COT commands to regulators

* Rename economy.list to available_indices

* Rename GDP models and clean up

* Fix charting tests

* Drop _indices from available_indices command

* Fix obbject to_dict test

---------

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Rename etf/disc (#5715)

* Docs/v4-working-docs: PR to Work on V4 Docs (#5693)

* Revert changes related to pydantic2 made into v3 code

* Revert fred model changes in v3

* Revert pydantic2 databento model changes in v2

* Update some names and v3 lock file

* Clean up openbb/package before the beta release

* Platform publishing procedure (#5701)

* update instructions

* introducing the version script

* publish message

* fix platform path

* reflecting version changes

* getting version from installed packages instead

* removing toml from dependencies

* Changes to get `a7` out (#5697)

* changes to get a7 out

* etf menu on the pyproject.toml

* poetry lock

* bump `openbb-etf` to `a5` to match version

* updated deps

* removing ultima

* removing references to a4 cc @the-praxs

* Bump index menu version

* poetry lock

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Update publishing instructions

* Bump core dependencies in extensions and providers

* Add changes to the publish script following the beta release

* Update the versions in static extension map and openbb metapackage

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>
Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Diogo Sousa <montezdesousa@gmail.com>
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
Co-authored-by: tehcoderer <me@tehcoderer.com>
Co-authored-by: DidierRLopes <dro.lopes@campus.fct.unl.pt>
Co-authored-by: andrewkenreich <andrew.kenreich@gmail.com>
Co-authored-by: Sandip Saha <sandipsmoto@gmail.com>
Co-authored-by: professssor <62937125+professssor@users.noreply.github.com>
Co-authored-by: Farookh Zaheer Siddiqui <129654632+FarukhS52@users.noreply.github.com>
Co-authored-by: MAX SHESTOV <57421753+maxxsh@users.noreply.github.com>
Co-authored-by: jose-donato <zmcdonato@gmail.com>
Co-authored-by: jose-donato <43375532+jose-donato@users.noreply.github.com>
Co-authored-by: Mohamad Fadil <fadil.parves@gmail.com>
Co-authored-by: Adi Sai <adithya.sairam1@gmail.com>
2023-11-13 15:03:31 +00:00

1802 lines
63 KiB
Python

# pylint: disable=C0302, R0911, W0612, R0912
""" Options Chains Module """
__docformat__ = "numpy"
# IMPORTATION STANDARD
import logging
from copy import deepcopy
# IMPORTATION THIRDPARTY
from typing import Any, Optional
import numpy as np
import pandas as pd
# IMPORTATION INTERNAL
from openbb_terminal.decorators import log_start_end
from openbb_terminal.rich_config import console
from openbb_terminal.stocks.options.cboe_model import load_options as load_cboe
from openbb_terminal.stocks.options.intrinio_model import load_options as load_intrinio
from openbb_terminal.stocks.options.nasdaq_model import load_options as load_nasdaq
from openbb_terminal.stocks.options.op_helpers import Options
from openbb_terminal.stocks.options.tmx_model import load_options as load_tmx
from openbb_terminal.stocks.options.tradier_model import load_options as load_tradier
from openbb_terminal.stocks.options.yfinance_model import load_options as load_yfinance
logger = logging.getLogger(__name__)
SOURCES = ["CBOE", "YahooFinance", "Tradier", "Intrinio", "Nasdaq", "TMX"]
# mypy: disable-error-code="attr-defined, index"
@log_start_end(log=logger)
def load_options_chains(
symbol: str,
source: str = "CBOE",
date: str = "",
pydantic: bool = False,
) -> Options:
"""Loads all options chains from a specific source, fields returned to each attribute will vary.
Parameters
----------
symbol : str
The underlying asset's symbol.
source: str
The source of the data. Choices are "CBOE", "YahooFinance", "Tradier", "Intrinio", "Nasdaq", or "TMX".
date: Optional[str]
The date for the EOD option chain. Format: YYYY-MM-DD.
This parameter is only available for "TMX" or "Intrinio".
pydantic: bool
Whether to return the object as a Pydantic Model or a subscriptable Pandas object. Default is False.
Returns
-------
Options: Options data object
chains: dict
All options chains data from a specific source. Returns as a Pandas DataFrame if pydantic is False.
expirations: list[str]
List of all unique expiration dates.
hasGreeks: bool
True if the source returns greeks with the chains data.
hasIV: bool
True if the source returns implied volatility with the chains data.
last_price: float
The last price (or the price at the EOD for the date.of the EOD option chain).
source: str
The source that was entered in the input.
strikes: list[float]
List of all unique strike prices.
symbol: str
The symbol that was entered in the input.
SYMBOLS: pd.DataFrame
The symbol directory to the selected source, when available. Only returned when pydantic is False.
underlying_name: str
The name of the underlying asset.
underlying_price: dict
The underlying asset's price and performance. Returns as a Pandas Series if pydantic is False.
Examples
--------
Loads SPY data from CBOE, returns as a Pydantic Model, and displays the longest-dated expiration chain.
>>> from openbb_terminal.sdk import openbb
>>> import pandas as pd
>>> data = openbb.stocks.options.load_options_chains("SPY", pydantic = True)
>>> chains = pd.DataFrame(data.chains)
>>> chains[chains["expiration"] == data.expirations[-1]]
Loads QQQ data from Tradier as a Pydantic Model.
>>> from openbb_terminal.sdk import openbb
>>> data = openbb.stocks.options.load_options_chains("QQQ", source = "Tradier", pydantic = True)
Loads VIX data from YahooFinance as a Pandas object.
>>> from openbb_terminal.sdk import openbb
>>> data = openbb.stocks.options.load_options_chains("^VIX", source = "YahooFinance")
Loads XIU data from TMX and displays the 25 highest open interest options.
>>> from openbb_terminal.sdk import openbb
>>> data = openbb.stocks.options.load_options_chains("XIU", "TMX")
>>> data.chains.sort_values("openInterest", ascending=False).head(25)
Loads the EOD chains data for XIU.TO from March 15, 2020, sorted by number of transactions.
>>> from openbb_terminal.sdk import openbb
>>> data = openbb.stocks.options.load_options_chains("XIU.TO", "TMX", "2020-03-15")
>>> data.chains.sort_values("transactions", ascending=False).head(25)
"""
if source not in SOURCES:
console.print("Invalid choice. Choose from: ", list(SOURCES), sep=None)
return Options()
if source == "Nasdaq":
return load_nasdaq(symbol, pydantic)
if source == "YahooFinance":
return load_yfinance(symbol, pydantic)
if source == "Tradier":
return load_tradier(symbol, pydantic)
if source == "TMX":
if date != "":
return load_tmx(symbol, date, pydantic)
return load_tmx(symbol, pydantic=pydantic)
if source == "Intrinio":
if date != "":
return load_intrinio(symbol, date, pydantic)
return load_intrinio(symbol, pydantic=pydantic)
return load_cboe(symbol, pydantic)
def validate_object( # noqa: PLR0911
options: Options, scope: Optional[str] = "object", days: Optional[int] = None
) -> Any:
"""This is an internal helper function for validating the Options data object passed
through the input of functions defined in the OptionsChains class. The purpose is to handle
multi-type inputs with backwards compatibility and provide robust error handling. The return
is the portion of the object, or a true/false validation, required to perform the operation.
Parameters
----------
options : object
The Options data object.
Accepts both Pydantic and Pandas object types, as defined by `load_options_chains()`.
A Pandas DataFrame, or dictionary, with the options chains data is also accepted.
scope: str
The scope of the data needing to be validated. Choices are: ["chains", "object", "strategies", "nonZeroPrices"]
days: int
The number of target number of days until the expiration.
Returns
-------
Any:
if scope == "chains":
pd.DataFrame
Pandas DataFrame with the validated data.
if scope == "object" or scope == "strategies":
bool
True if the object is a valid Options data object.
Examples
--------
Load some data first:
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains("SPY")
To extract just the chains data, use:
>>> chains = options_chains_model.validate_object(data, scope="chains")
To pass as a true/false validation, use:
>>> if options_chains_model.validate_object(data, scope="object") is False:
>>> return
To pass and return the entire object for non-zero prices:
>>> if options_chains_model.validate_object(data, scope="object") is True:
>>> data = options_chains_model.validate_object(data, scope="nonZeroPrices")
"""
scopes = ["chains", "object", "strategies", "nonZeroPrices"]
scope = scope or "chains"
if scope not in scopes:
console.print("Invalid choice. The supported methods are:", scopes)
return pd.DataFrame()
if scope == "object":
try:
if (
isinstance(options.strikes, list)
and isinstance(options.expirations, list)
and hasattr(options, "last_price")
):
return True
except AttributeError:
console.print(
"Error: Invalid data type supplied. The Options data object is required. "
"Use load_options_chains() first."
)
return False
if scope == "strategies":
try:
if isinstance(options.last_price, float):
return True
except AttributeError:
console.print(
"`last_price` was not found in the OptionsChainsData object and is required for this operation."
)
return False
if scope == "chains":
try:
if isinstance(options, pd.DataFrame):
chains = options.copy()
if isinstance(options, dict):
chains = pd.DataFrame(options)
elif isinstance(options, object) and not isinstance(options, pd.DataFrame):
chains = (
pd.DataFrame(options.chains) # type: ignore[attr-defined]
if isinstance(options.chains, dict) # type: ignore[attr-defined]
else options.chains.copy() # type: ignore[attr-defined]
)
if options is None or chains.empty:
console.print(
"No options chains data found in the supplied object. Use load_options_chains()."
)
return pd.DataFrame()
if "openInterest" not in chains.columns:
console.print("Expected column, openInterest, not found.")
return pd.DataFrame()
if "volume" not in chains.columns:
console.print("Expected column, volume, not found.")
return pd.DataFrame()
except AttributeError:
console.print("Error: Invalid data type supplied.")
return pd.DataFrame()
return chains
if scope == "nonZeroPrices":
dte_estimate = get_nearest_dte( # noqa:F841 pylint: disable=unused-variable
options, days
)
# When Intrinio data is not EOD, there is no "ask" column, renaming "close".
if (
options.source == "Intrinio"
and options.date is None
or options.source == "Intrinio"
and options.date == ""
):
options.chains["ask"] = options.chains["close"]
options.chains["bid"] = options.chains["close"]
# Error handling for TMX EOD data when the date is an expiration date. EOD, 0-day options are excluded.
if options.source == "TMX" and options.date != "":
options.chains = options.chains[options.chains["dte"] > 0]
if (
options.source == "TMX"
and options.date is None
or options.source == "TMX"
and options.date == ""
):
options.chains["ask"] = options.chains["lastPrice"]
options.chains["bid"] = options.chains["lastPrice"]
if (
options.source == "TMX"
or options.source == "YahooFinance"
or options.source == "Nasdaq"
and options.chains.query("`dte` == @dte_estimate")["ask"].sum() == 0
):
options.chains["ask"] = options.chains["lastPrice"]
if (
options.source == "TMX"
or options.source == "YahooFinance"
or options.source == "Nasdaq"
and options.chains.query("`dte` == @dte_estimate")["bid"].sum() == 0
):
options.chains["bid"] = options.chains["lastPrice"]
return options
console.print(
"Error: No valid data supplied. Check the input to ensure it is not empty or None."
)
return False
def get_nearest_expiration(options: Options, expiration: Optional[str] = "") -> str:
"""Gets the closest expiration date to the target date."""
if validate_object(options, scope="object") is False:
return pd.DataFrame()
_expirations = pd.Series(pd.to_datetime(options.expirations))
_expiration = pd.to_datetime(expiration)
_nearest = pd.DataFrame(_expirations - _expiration)
_nearest_exp = abs(_nearest[0].astype("int64")).idxmin()
return options.expirations[_nearest_exp]
def get_nearest_dte(options: Options, days: Optional[int] = 30) -> int:
"""Gets the closest expiration date to the target number of days until expiry.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
Returns
-------
int
The closest expiration date to the target number of days until expiry, expressed as DTE.
Example
-------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains("QQQ")
>>> options_chains_model.get_nearest_dte(data, 42)
>>> options_chains_model.get_nearest_dte(data, 90)
"""
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
days = -1 if days == 0 else days
nearest = (options.chains["dte"] - days).abs().idxmin()
return options.chains.loc[nearest]["dte"]
def get_nearest_call_strike(
options: Options, days: Optional[int] = 30, strike_price: Optional[float] = 0
) -> float:
"""Gets the closest call strike to the target price and number of days until expiry.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
strike_price: float
The target strike price. Default is the last price of the underlying stock.
Returns
-------
float
The closest strike price to the target price and number of days until expiry.
Example
-------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains("SPY")
>>> options_chains_model.get_nearest_call_strike(data)
>>> options_chains_model.get_nearest_call_strike(data, 180, 427)
>>> days = data.chains.dte.unique().tolist()
>>> for day in days:
>>> print(get_nearest_call_strike(data, day))
"""
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
if strike_price == 0:
strike_price = options.last_price
dte_estimate = get_nearest_dte(options, days)
if (
len(
options.chains[options.chains["dte"] == dte_estimate].query(
"`optionType` == 'call'"
)
)
== 0
):
return pd.DataFrame()
nearest = (
(
options.chains[options.chains["dte"] == dte_estimate]
.query("`optionType` == 'call'")
.convert_dtypes()["strike"]
- strike_price
)
.abs()
.idxmin()
)
return options.chains.loc[nearest]["strike"]
def get_nearest_put_strike(
options: Options, days: Optional[int] = 30, strike_price: Optional[float] = 0
) -> float:
"""Gets the closest put strike to the target price and number of days until expiry.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
strike_price: float
The target strike price. Default is the last price of the underlying stock.
Returns
-------
float
The closest strike price to the target price and number of days until expiry.
Example
-------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains('SPY')
>>> options_chains_model.get_nearest_put_strike(data, 90, 402)
>>> options_chains_model.get_nearest_put_strike(data, 90)
>>> days = data.chains.dte.unique().tolist()
>>> for day in days:
>>> print(get_nearest_put_strike(data, day))
"""
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
if strike_price == 0:
strike_price = options.last_price
dte_estimate = get_nearest_dte(options, days)
if (
len(
options.chains[options.chains["dte"] == dte_estimate].query(
"`optionType` == 'put'"
)
)
== 0
):
return pd.DataFrame()
nearest = (
(
options.chains[options.chains["dte"] == dte_estimate]
.query("`optionType` == 'put'")
.convert_dtypes()["strike"]
- strike_price
)
.abs()
.idxmin()
)
return options.chains.loc[nearest]["strike"]
def get_nearest_otm_strike(
options: Options, moneyness: Optional[float] = 5
) -> dict[str, float]:
"""Gets the nearest put and call strikes at a given percent OTM from the underlying price.
Parameters
----------
options : OptionsChains
The Options data object. Use load_options_chains() to load the data.
moneyness: float
The target percent OTM, expressed as a percent between 0 and 100. Default is 5.
Returns
-------
dict[str, float]
Dictionary of the upper (call) and lower (put) strike prices.
Example
-------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains('SPY')
>>> strikes = options_chains_model.get_nearest_otm_strike(data, 5)
"""
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
if not moneyness:
moneyness = 5
if 0 < moneyness < 100:
moneyness = moneyness / 100
if moneyness > 100 or moneyness < 0:
console.print(
"Error: Moneyness must be expressed as a percentage between 0 and 100"
)
return {}
upper = options.last_price * (1 + moneyness)
lower = options.last_price * (1 - moneyness)
strikes = pd.Series(options.strikes)
nearest_call = (upper - strikes).abs().idxmin()
call = strikes[nearest_call]
nearest_putt = (lower - strikes).abs().idxmin()
put = strikes[nearest_putt]
otmStrikes = {"call": call, "put": put}
return otmStrikes
def calculate_straddle(
options: Options,
days: Optional[int] = 30,
strike: float = 0,
) -> pd.DataFrame:
"""Calculates the cost of a straddle and its payoff profile. Use a negative strike price for short options.
Requires the Options data object.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
strike: float
The target strike price. Enter a negative value for short options.
Default is the last price of the underlying stock.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike1 is the nearest call strike, strike2 is the nearest put strike.
Examples
--------
>>> from openbb_terminal.sdk import openbb
>>> data = openbb.stocks.options.load_options_chains('SPY')
>>> openbb.stocks.options.calculate_straddle(data)
"""
options = deepcopy(options)
if not days:
days = 30
if days and days == 0:
days = -1
short: bool = False
if strike is not None and strike < 0:
short = True
strike_price = abs(strike)
bidAsk = "bid" if short else "ask" # noqa:F841
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
dte_estimate = get_nearest_dte(options, days) # noqa:F841
options = validate_object(options, "nonZeroPrices", dte_estimate)
if not strike_price:
strike_price = options.last_price
call_strike_estimate = get_nearest_call_strike(
options, dte_estimate, strike_price
) # noqa:F841
put_strike_estimate = get_nearest_put_strike(
options, dte_estimate, strike_price
) # noqa:F841
call_premium = options.chains.query(
"`strike` == @call_strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
)[bidAsk].values
put_premium = options.chains.query(
"`strike` == @put_strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
)[bidAsk].values
straddle_cost = call_premium + put_premium
straddle: dict[str, Any] = {}
# Include the as-of date if the data is historical EOD.
if (
options.source == "Intrinio"
and options.date != ""
or options.source == "TMX"
and options.date != ""
):
straddle.update({"Date": options.date})
straddle.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": call_strike_estimate,
"Strike 2": put_strike_estimate,
"Strike 1 Premium": call_premium[0],
"Strike 2 Premium": put_premium[0],
"Cost": straddle_cost[0] * -1 if short else straddle_cost[0],
"Cost Percent": round(
straddle_cost[0] / options.last_price * 100, ndigits=4
),
"Breakeven Upper": call_strike_estimate + straddle_cost[0],
"Breakeven Upper Percent": round(
(call_strike_estimate + straddle_cost[0]) / options.last_price * 100,
ndigits=4,
)
- 100,
"Breakeven Lower": put_strike_estimate - straddle_cost[0],
"Breakeven Lower Percent": -100
+ round(
(put_strike_estimate - straddle_cost[0]) / options.last_price * 100,
ndigits=4,
),
"Max Profit": abs(straddle_cost[0]) if short else np.inf,
"Max Loss": np.inf if short else straddle_cost[0] * -1,
}
)
straddle = pd.DataFrame(data=straddle.values(), index=straddle.keys()).rename(
columns={0: "Short Straddle" if short else "Long Straddle"}
)
straddle.loc["Payoff Ratio"] = round(
abs(straddle.loc["Max Profit"][0] / straddle.loc["Max Loss"][0]), ndigits=4
)
return straddle
def calculate_strangle(
options: Options,
days: Optional[int] = 30,
moneyness: Optional[float] = 5,
) -> pd.DataFrame:
"""Calculates the cost of a strangle and its payoff profile. Use a negative value for moneyness for short options.
Requires the Options data object.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
moneyness: float
The percentage of OTM moneyness, expressed as a percent between -100 < 0 < 100.
Enter a negative number for short options.
Default is 5.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike 1 is the nearest call strike, and strike 2 is the nearest put strike.
Examples
--------
>>> data = openbb.stocks.options.load_options_chains("SPY")
>>> data.calculate_strangle()
"""
options = deepcopy(options)
if not days:
days = 30
if not moneyness:
moneyness = 5
short: bool = False
if moneyness < 0:
short = True
moneyness = abs(moneyness)
bidAsk = "bid" if short else "ask" # noqa:F841
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
dte_estimate = get_nearest_dte(options, days) # noqa:F841
options = validate_object(options, "nonZeroPrices", dte_estimate)
if moneyness > 100 or moneyness < 0:
console.print("Error: Moneyness must be between 0 and 100.")
return pd.DataFrame()
strikes = get_nearest_otm_strike(options, moneyness)
call_strike_estimate = get_nearest_call_strike(
options, dte_estimate, strikes["call"]
) # noqa:F841
put_strike_estimate = get_nearest_put_strike(
options, dte_estimate, strikes["put"]
) # noqa:F841
call_premium = options.chains.query(
"`strike` == @call_strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
)[bidAsk].values
put_premium = options.chains.query(
"`strike` == @put_strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
)[bidAsk].values
strangle_cost = call_premium + put_premium
strangle: dict[str, Any] = {}
# Includes the as-of date if it is historical EOD data.
if (
options.source == "Intrinio"
and options.date != ""
or options.source == "TMX"
and options.date != ""
):
strangle.update({"Date": options.date})
strangle.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": call_strike_estimate,
"Strike 2": put_strike_estimate,
"Strike 1 Premium": call_premium[0],
"Strike 2 Premium": put_premium[0],
"Cost": strangle_cost[0] * -1 if short else strangle_cost[0],
"Cost Percent": round(
strangle_cost[0] / options.last_price * 100, ndigits=4
),
"Breakeven Upper": call_strike_estimate + strangle_cost[0],
"Breakeven Upper Percent": round(
(call_strike_estimate + strangle_cost[0]) / options.last_price * 100,
ndigits=4,
)
- 100,
"Breakeven Lower": put_strike_estimate - strangle_cost[0],
"Breakeven Lower Percent": -100
+ round(
(put_strike_estimate - strangle_cost[0]) / options.last_price * 100,
ndigits=4,
),
"Max Profit": abs(strangle_cost[0]) if short else np.inf,
"Max Loss": np.inf if short else strangle_cost[0] * -1,
}
)
strangle = pd.DataFrame(data=strangle.values(), index=strangle.keys()).rename(
columns={0: "Short Strangle" if short else "Long Strangle"}
)
strangle.loc["Payoff Ratio"] = round(
abs(strangle.loc["Max Profit"][0] / strangle.loc["Max Loss"][0]), ndigits=4
)
return strangle
def calculate_vertical_call_spread(
options: Options,
days: Optional[int] = 30,
sold_strike: Optional[float] = 0,
bought_strike: Optional[float] = 0,
) -> pd.DataFrame:
"""Calculates the vertical call spread for the target DTE.
A bull call spread is when the sold strike is above the bought strike.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. This value will be used to get the nearest valid DTE.
Default is 30 days.
sold_strike: float
The target strike price for the short leg of the vertical call spread.
Default is the 5% OTM above the last price of the underlying.
bought_strike: float
The target strike price for the long leg of the vertical call spread. Default is the last price of the underlying.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike 1 is the sold strike, and strike 2 is the bought strike.
Examples
--------
Load the data:
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains()
>>> data = op.load_options_chains("QQQ")
For a bull call spread:
>>> data.calculate_vertical_call_spread(days=10, sold_strike=355, bought_strike=350)
For a bear call spread:
>>> data.calculate_vertical_call_spread(days=10, sold_strike=350, bought_strike=355)
"""
options = deepcopy(options)
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
if not days:
days = 30
if not bought_strike:
bought_strike = options.last_price * 1.0250
if not sold_strike:
sold_strike = options.last_price * 1.0750
dte_estimate = get_nearest_dte(options, days) # noqa:F841
options = validate_object(options, "nonZeroPrices", dte_estimate)
sold = get_nearest_call_strike(options, days, sold_strike)
bought = get_nearest_call_strike(options, days, bought_strike)
sold_premium = options.chains.query(
"`strike` == @sold and `dte` == @dte_estimate and `optionType` == 'call'"
)["bid"].values * (-1)
bought_premium = options.chains.query(
"`strike` == @bought and `dte` == @dte_estimate and `optionType` == 'call'"
)["ask"].values
spread_cost = bought_premium + sold_premium
breakeven_price = bought + spread_cost[0]
max_profit = sold - bought - spread_cost[0]
call_spread_: dict[str, Any] = {}
if sold != bought and spread_cost[0] != 0:
# Includes the as-of date if it is historical EOD data.
if (
options.source == "Intrinio"
and options.date != ""
or options.source == "TMX"
and options.date != ""
):
call_spread_.update({"Date": options.date})
call_spread_.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": sold,
"Strike 2": bought,
"Strike 1 Premium": sold_premium[0],
"Strike 2 Premium": bought_premium[0],
"Cost": spread_cost[0],
"Cost Percent": round(
spread_cost[0] / options.last_price * 100, ndigits=4
),
"Breakeven Lower": breakeven_price,
"Breakeven Lower Percent": round(
(breakeven_price / options.last_price * 100) - 100, ndigits=4
),
"Breakeven Upper": np.nan,
"Breakeven Upper Percent": np.nan,
"Max Profit": max_profit,
"Max Loss": spread_cost[0] * -1,
}
)
call_spread = pd.DataFrame(
data=call_spread_.values(), index=call_spread_.keys()
).rename(columns={0: "Bull Call Spread"})
if call_spread.loc["Cost"][0] < 0:
call_spread.loc["Max Profit"][0] = call_spread.loc["Cost"][0] * -1
call_spread.loc["Max Loss"][0] = -1 * (
bought - sold + call_spread.loc["Cost"][0]
)
lower = bought if sold > bought else sold
call_spread.loc["Breakeven Upper"][0] = (
lower + call_spread.loc["Max Profit"][0]
)
call_spread.loc["Breakeven Upper Percent"][0] = round(
(breakeven_price / options.last_price * 100) - 100, ndigits=4
)
call_spread.loc["Breakeven Lower"][0] = np.nan
call_spread.loc["Breakeven Lower Percent"][0] = np.nan
call_spread.rename(
columns={"Bull Call Spread": "Bear Call Spread"}, inplace=True
)
call_spread.loc["Payoff Ratio"] = round(
abs(call_spread.loc["Max Profit"][0] / call_spread.loc["Max Loss"][0]),
ndigits=4,
)
return call_spread
return pd.DataFrame()
def calculate_vertical_put_spread(
options: Options,
days: Optional[int] = 30,
sold_strike: Optional[float] = 0,
bought_strike: Optional[float] = 0,
) -> pd.DataFrame:
"""Calculates the vertical put spread for the target DTE.
A bear put spread is when the bought strike is above the sold strike.
Parameters
----------
options : object
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. This value will be used to get the nearest valid DTE.
Default is 30 days.
sold_strike: float
The target strike price for the short leg of the vertical put spread. Default is the last price of the underlying.
bought_strike: float
The target strike price for the long leg of the vertical put spread.
Default is the 5% OTM above the last price of the underlying.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike 1 is the sold strike, strike 2 is the bought strike.
Examples
--------
Load the data:
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains()
>>> data = OptionsChains("QQQ")
For a bull put spread:
>>> data.get_vertical_put_spread(data, days=10, sold_strike=355, bought_strike=350)
For a bear put spread:
>>> data.get_vertical_put_spread(data, days=10, sold_strike=355, bought_strike=350)
"""
options = deepcopy(options)
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
if not days:
days = 30
if not bought_strike:
bought_strike = options.last_price * 0.9750
if not sold_strike:
sold_strike = options.last_price * 0.9250
dte_estimate = get_nearest_dte(options, days) # noqa:F841
options = validate_object(options, "nonZeroPrices", dte_estimate)
sold = get_nearest_put_strike(options, days, sold_strike)
bought = get_nearest_put_strike(options, days, bought_strike)
sold_premium = options.chains.query(
"`strike` == @sold and `dte` == @dte_estimate and `optionType` == 'put'"
)["bid"].values * (-1)
bought_premium = options.chains.query(
"`strike` == @bought and `dte` == @dte_estimate and `optionType` == 'put'"
)["ask"].values
spread_cost = bought_premium + sold_premium
max_profit = abs(spread_cost[0])
breakeven_price = sold - max_profit
max_loss = (sold - bought - max_profit) * -1
put_spread_: dict[str, Any] = {}
if sold != bought and max_loss != 0:
# Includes the as-of date if it is historical EOD data.
if (
options.source == "Intrinio"
and options.date != ""
or options.source == "TMX"
and options.date != ""
):
put_spread_.update({"Date": options.date})
put_spread_.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": sold,
"Strike 2": bought,
"Strike 1 Premium": sold_premium[0],
"Strike 2 Premium": bought_premium[0],
"Cost": spread_cost[0],
"Cost Percent": round(max_profit / options.last_price * 100, ndigits=4),
"Breakeven Lower": np.nan,
"Breakeven Lower Percent": np.nan,
"Breakeven Upper": breakeven_price,
"Breakeven Upper Percent": (
100 - round((breakeven_price / options.last_price) * 100, ndigits=4)
),
"Max Profit": max_profit,
"Max Loss": max_loss,
}
)
put_spread = pd.DataFrame(
data=put_spread_.values(), index=put_spread_.keys()
).rename(columns={0: "Bull Put Spread"})
if put_spread.loc["Cost"][0] > 0:
put_spread.loc["Max Profit"][0] = bought - sold - spread_cost[0]
put_spread.loc["Max Loss"][0] = spread_cost[0] * (-1)
put_spread.loc["Breakeven Lower"][0] = bought - spread_cost[0]
put_spread.loc["Breakeven Lower Percent"][0] = 100 - round(
(breakeven_price / options.last_price) * 100, ndigits=4
)
put_spread.loc["Breakeven Upper"][0] = np.nan
put_spread.loc["Breakeven Upper Percent"][0] = np.nan
put_spread.rename(
columns={"Bull Put Spread": "Bear Put Spread"}, inplace=True
)
put_spread.loc["Payoff Ratio"] = round(
abs(put_spread.loc["Max Profit"][0] / put_spread.loc["Max Loss"][0]),
ndigits=4,
)
return put_spread
return pd.DataFrame()
def calculate_synthetic_long(
options: Options,
days: Optional[int] = 30,
strike: float = 0,
) -> pd.DataFrame:
"""Calculates the cost of a synthetic long position at a given strike.
It is expressed as the difference between a bought call and a sold put.
Requires the Options data object.
Parameters
-----------
options : Options
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
strike: float
The target strike price. Default is the last price of the underlying stock.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike1 is the purchased call strike, strike2 is the sold put strike.
Example
--------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains('SPY')
>>> options_chains_model.calculate_synthetic_long(data)
"""
options = deepcopy(options)
if not days:
days = 30
if days and days == 0:
days = -1
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
dte_estimate = get_nearest_dte(options, days)
options = validate_object(options, "nonZeroPrices", dte_estimate)
strike_price = options.last_price if strike == 0 else strike
strike_estimate = get_nearest_call_strike(options, dte_estimate, strike_price)
call_premium = options.chains.query(
"`strike` == @strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
)["ask"].values
put_premium = options.chains.query(
"`strike` == @strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
)["bid"].values * (-1)
position_cost = call_premium[0] + put_premium[0]
breakeven = strike_estimate + position_cost
synthetic_long: dict[str, Any] = {}
synthetic_long.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": strike_estimate,
"Strike 2": strike_estimate,
"Strike 1 Premium": call_premium[0],
"Strike 2 Premium": put_premium[0],
"Cost": position_cost,
"Cost Percent": round(position_cost / options.last_price * 100, ndigits=4),
"Breakeven Lower": np.nan,
"Breakeven Lower Percent": np.nan,
"Breakeven Upper": breakeven,
"Breakeven Upper Percent": round(
((breakeven - options.last_price) / options.last_price) * 100, ndigits=4
),
"Max Profit": np.inf,
"Max Loss": breakeven * (-1),
}
)
strategy = pd.DataFrame(
data=synthetic_long.values(), index=synthetic_long.keys()
).rename(columns={0: "Synthetic Long"})
return strategy
def calculate_synthetic_short(
options: Options,
days: Optional[int] = 30,
strike: float = 0,
) -> pd.DataFrame:
"""Calculates the cost of a synthetic short position at a given strike.
It is expressed as the difference between a sold call and a purchased put.
Requires the Options data object.
Parameters
-----------
options : Options
The Options data object. Use load_options_chains() to load the data.
days: int
The target number of days until expiry. Default is 30 days.
strike: float
The target strike price. Default is the last price of the underlying stock.
Returns
-------
pd.DataFrame
Pandas DataFrame with the results. Strike1 is the sold call strike, strike2 is the purchased put strike.
Example
--------
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains('SPY')
>>> options_chains_model.calculate_synthetic_short(data)
"""
options = deepcopy(options)
if not days:
days = 30
if days and days == 0:
days = -1
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the OptionsChain data object.")
return pd.DataFrame()
if isinstance(options.chains, dict):
options.chains = pd.DataFrame(options.chains)
dte_estimate = get_nearest_dte(options, days)
options = validate_object(options, "nonZeroPrices", dte_estimate)
strike_price = options.last_price if strike == 0 else strike
strike_estimate = get_nearest_call_strike(options, dte_estimate, strike_price)
call_premium = options.chains.query(
"`strike` == @strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
)["bid"].values * (-1)
put_premium = options.chains.query(
"`strike` == @strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
)["ask"].values
position_cost = put_premium[0] + call_premium[0]
breakeven = strike_estimate - position_cost
synthetic_short: dict[str, Any] = {}
synthetic_short.update(
{
"Symbol": options.symbol,
"Underlying Price": options.last_price,
"Expiration": options.chains.query("`dte` == @dte_estimate")[
"expiration"
].unique()[0],
"DTE": dte_estimate,
"Strike 1": strike_estimate,
"Strike 2": strike_estimate,
"Strike 1 Premium": call_premium[0],
"Strike 2 Premium": put_premium[0],
"Cost": position_cost,
"Cost Percent": round(position_cost / options.last_price * 100, ndigits=4),
"Breakeven Lower": breakeven,
"Breakeven Lower Percent": round(
((breakeven - options.last_price) / options.last_price) * 100, ndigits=4
),
"Breakeven Upper": np.nan,
"Breakeven Upper Percent": np.nan,
"Max Profit": breakeven,
"Max Loss": np.inf,
}
)
strategy = pd.DataFrame(
data=synthetic_short.values(), index=synthetic_short.keys()
).rename(columns={0: "Synthetic Short"})
return strategy
@log_start_end(log=logger)
def calculate_stats(options: Options, by: Optional[str] = "expiration") -> pd.DataFrame:
"""Calculates basic statistics for the options chains, like OI and Vol/OI ratios.
Parameters
----------
options : object
The Options data object.
Accepts both Pydantic and Pandas object types, as defined by `load_options_chains()`.
A Pandas DataFrame, or dictionary, with the options chains data is also accepted.
by: str
Whether to calculate by strike or expiration. Default is expiration.
Returns
-------
pd.DataFrame
Pandas DataFrame with the calculated statistics.
Examples
--------
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
>>> data = OptionsChains("SPY")
By expiration date:
>>> data.calculate_stats()
By strike:
>>> data.calculate_stats(data, "strike")
"""
if by not in ["expiration", "strike"]:
console.print(
"Invalid choice. The supported methods are: [expiration, strike]"
)
return pd.DataFrame()
chains = deepcopy(options)
last_price = chains.last_price if hasattr(chains, "last_price") else None
chains = validate_object(chains, scope="chains")
if chains.empty or chains is None:
return chains == pd.DataFrame()
stats = pd.DataFrame()
stats["Puts OI"] = (
chains[chains["optionType"] == "put"]
.groupby(f"{by}")[["openInterest"]]
.sum(numeric_only=True)
.astype("int64")
)
stats["Calls OI"] = (
chains[chains["optionType"] == "call"]
.groupby(f"{by}")[["openInterest"]]
.sum(numeric_only=True)
.astype("int64")
)
stats["Total OI"] = stats["Calls OI"] + stats["Puts OI"]
stats["OI Ratio"] = round(stats["Puts OI"] / stats["Calls OI"], 2)
if by == "expiration" and last_price:
stats["Puts OTM"] = (
chains.query("`optionType` == 'put' & `strike` < @last_price")
.groupby("expiration")[["openInterest"]]
.sum(numeric_only=True)
)
stats["Calls OTM"] = (
chains.query("`optionType` == 'call' & `strike` > @last_price")
.groupby("expiration")[["openInterest"]]
.sum(numeric_only=True)
)
stats["Puts ITM"] = (
chains.query("`optionType` == 'put' & `strike` > @last_price")
.groupby("expiration")[["openInterest"]]
.sum(numeric_only=True)
)
stats["Calls ITM"] = (
chains.query("`optionType` == 'call' & `strike` < @last_price")
.groupby("expiration")[["openInterest"]]
.sum(numeric_only=True)
)
stats["OTM Ratio"] = round(stats["Puts OTM"] / stats["Calls OTM"], 2)
stats["ITM Percent"] = round(
(stats["Puts ITM"] + stats["Calls ITM"]) / stats["Total OI"] * 100, 2
)
stats["Puts Volume"] = (
chains[chains["optionType"] == "put"]
.groupby(f"{by}")[["volume"]]
.sum(numeric_only=True)
.astype("int64")
)
stats["Calls Volume"] = (
chains[chains["optionType"] == "call"]
.groupby(f"{by}")
.sum(numeric_only=True)[["volume"]]
.astype("int64")
)
stats["Total Volume"] = stats["Calls Volume"] + stats["Puts Volume"]
stats["Volume Ratio"] = round(stats["Puts Volume"] / stats["Calls Volume"], 2)
stats["Vol-OI Ratio"] = round(stats["Total Volume"] / stats["Total OI"], 2)
stats.rename_axis("Expiration", inplace=True)
if by == "strike":
stats.rename_axis(index={"Expiration": "Strike"}, inplace=True)
return stats.replace([np.nan, np.inf], None)
@log_start_end(log=logger)
def get_strategies(
options: Options,
days: Optional[list[int]] = None,
straddle_strike: Optional[float] = 0,
strangle_moneyness: Optional[list[float]] = None,
synthetic_longs: Optional[list[float]] = None,
synthetic_shorts: Optional[list[float]] = None,
vertical_calls: Optional[list[float]] = None,
vertical_puts: Optional[list[float]] = None,
) -> pd.DataFrame:
"""Gets options strategies for all, or a list of, DTE(s).
Currently supports straddles, strangles, and vertical spreads.
Multiple strategies, expirations, and % moneyness can be returned.
A negative value for `straddle_strike` or `strangle_moneyness` returns short options.
A synthetic long/short position is a bought/sold call and sold/bought put at the same strike.
A sold call strike that is lower than the bought strike, or a sold put strike that is higher than the bought strike,
is a bearish vertical spread.
Parameters
----------
options: object
The Options data object. Use `load_options_chains()` to load the data.
days: list[int]
List of DTE(s) to get strategies for. Enter a single value, or multiple as a list. Defaults to all.
strike_price: float
The target strike price. Defaults to the last price of the underlying stock.
strangle_moneyness: list[float]
List of OTM moneyness to target, expressed as a percent value between 0 and 100.
Enter a single value, or multiple as a list. Defaults to 5.
synthetic_long: list[float]
List of strikes for a synthetic long position.
synthetic_short: list[float]
List of strikes for a synthetic short position.
vertical_calls: list[float]
Call strikes for vertical spreads, listed as [sold strike, bought strike].
vertical_puts: list[float]
Put strikes for vertical spreads, listed as [sold strike, bought strike].
Returns
-------
pd.DataFrame
Pandas DataFrame with the results.
Examples
--------
Load data
>>> from openbb_terminal.stocks.options import options_chains_model
>>> data = options_chains_model.load_options_chains("SPY")
Return just straddles
>>> options_chains_model.calculate_strategies(data)
Return strangles
>>> options_chains_model.calculate_strategies(data, strangle_moneyness = 10)
Return multiple values for both moneness and days:
>>> options_chains_model.calculate_strategies(data, days = [10,30,60,90], moneyness = [2.5,-5,10,-20])
Return vertical spreads for all expirations.
>>> options_chains_model.calculate_strategies(data, vertical_calls=[430,427], vertical_puts=[420,426])
Return synthetic short and long positions:
>>> options_chains_model.calculate_strategies(
data, days = [30,60], synthetic_short = [450,445], synthetic_long = [450,455]
)
"""
def to_clean_list(x):
if x is None:
return None
return [x] if not isinstance(x, list) else x
options = deepcopy(options)
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if validate_object(options, scope="strategies") is False:
console.print("`last_price` was not found in the Options data object.")
return pd.DataFrame()
options.chains = (
pd.DataFrame(options.chains)
if isinstance(options.chains, dict)
else options.chains
)
strangle_moneyness = strangle_moneyness or [0.0]
days = days or options.chains["dte"].unique().tolist()
# Allows a single input to be passed instead of a list.
days = [days] if isinstance(days, int) else days # type: ignore[list-item]
if not isinstance(vertical_calls, (list, float)) and vertical_calls is not None:
console.print(
"Two strike prices are required. Enter the sold price first, then the bought price."
)
return pd.DataFrame()
if not isinstance(vertical_puts, (list, float)) and vertical_puts is not None:
console.print(
"Two strike prices are required. Enter the sold price first, then the bought price."
)
return pd.DataFrame()
strangle_moneyness = to_clean_list(strangle_moneyness)
synthetic_longs = to_clean_list(synthetic_longs)
synthetic_shorts = to_clean_list(synthetic_shorts)
days_list = []
strategies = pd.DataFrame()
straddles = pd.DataFrame()
strangles = pd.DataFrame()
strangles_ = pd.DataFrame()
synthetic_longs_df = pd.DataFrame()
_synthetic_longs = pd.DataFrame()
synthetic_shorts_df = pd.DataFrame()
_synthetic_shorts = pd.DataFrame()
call_spreads = pd.DataFrame()
put_spreads = pd.DataFrame()
for day in days:
day = day or -1 # noqa: PLW2901
days_list.append(get_nearest_dte(options, day))
days = list(set(days_list))
days.sort()
if vertical_calls is not None:
cStrike1 = vertical_calls[0]
cStrike2 = vertical_calls[1]
for day in days:
call_spread = calculate_vertical_call_spread(
options, day, cStrike1, cStrike2
).transpose()
call_spreads = pd.concat([call_spreads, call_spread])
if vertical_puts:
pStrike1 = vertical_puts[0]
pStrike2 = vertical_puts[1]
for day in days:
put_spread = calculate_vertical_put_spread(
options, day, pStrike1, pStrike2
).transpose()
put_spreads = pd.concat([put_spreads, put_spread])
if straddle_strike:
straddle_strike = (
options.last_price if straddle_strike == 0 else straddle_strike
)
for day in days:
straddle = calculate_straddle(options, day, straddle_strike).transpose()
if straddle.iloc[0]["Cost"] != 0:
straddles = pd.concat([straddles, straddle])
if strangle_moneyness and strangle_moneyness[0] != 0:
for day in days:
for moneyness in strangle_moneyness:
strangle = calculate_strangle(options, day, moneyness).transpose()
if strangle.iloc[0]["Cost"] != 0:
strangles_ = pd.concat([strangles_, strangle])
strangles = pd.concat([strangles, strangles_])
strangles = strangles.query("`Strike 1` != `Strike 2`").drop_duplicates()
if synthetic_longs:
strikes = synthetic_longs
for day in days:
for strike in strikes:
_synthetic_long = calculate_synthetic_long(
options, day, strike
).transpose()
if _synthetic_long.iloc[0]["Strike 1 Premium"] != 0:
_synthetic_longs = pd.concat([_synthetic_longs, _synthetic_long])
synthetic_longs_df = pd.concat([synthetic_longs_df, _synthetic_longs])
if synthetic_shorts:
strikes = synthetic_shorts
for day in days:
for strike in strikes:
_synthetic_short = calculate_synthetic_short(
options, day, strike
).transpose()
if _synthetic_short.iloc[0]["Strike 1 Premium"] != 0:
_synthetic_shorts = pd.concat([_synthetic_shorts, _synthetic_short])
synthetic_shorts_df = pd.concat([synthetic_shorts_df, _synthetic_shorts])
strategies = pd.concat(
[
straddles,
strangles,
synthetic_longs_df,
synthetic_shorts_df,
call_spreads,
put_spreads,
]
)
if strategies.empty:
return get_strategies(options, straddle_strike=options.last_price)
strategies = strategies.reset_index().rename(columns={"index": "Strategy"})
strategies = (
strategies.set_index(["Expiration", "DTE"])
.sort_index()
.drop(columns=["Symbol"])
)
return strategies.reset_index()
@log_start_end(log=logger)
def calculate_skew(
options, expiration: Optional[str] = "", moneyness: Optional[float] = None
) -> pd.DataFrame:
"""
Returns the skewness of the options, either vertical or horizontal.
The vertical skew for each expiry and option is calculated by subtracting the IV of the ATM call or put.
Returns only where the IV is greater than 0.
Horizontal skew is returned if a value for moneyness is supplied.
It is expressed as the difference between skews of two equidistant OTM strikes (the closest call and put).
Parameters
-----------
options: object
The Options data object. Use `load_options_chains()` to load the data.
expiration: str
The expiration date to target. Defaults to all.
moneyness: float
The moneyness to target for calculating horizontal skew. This parameter overrides a defined expiration date.
Returns
--------
pd.DataFrame
Pandas DataFrame with the results.
Examples
----------
>>> from openbb_terminal.stocks.options import options_chains_model as op
>>> data = op.load_options_chains("SPY")
Vertical skew at a given expiry:
>>> skew = op.calculate_skew(data, "2025-12-19")
Vertical skew at all expirations:
>>> skew = op.calculate_skew(data)
Horizontal skew at a given % OTM:
>>> skew = op.calculate_skew(data, moneyness = 10)
"""
options = deepcopy(options)
if validate_object(options, scope="object") is False:
return pd.DataFrame()
if options.hasIV is False:
console.print(
"Options data object does not have Implied Volatility and is required for this function."
)
return pd.DataFrame()
options.chains = validate_object(options.chains, scope="chains")
options.chains = options.chains[options.chains["impliedVolatility"] > 0]
days = options.chains["dte"].unique().tolist()
# expiration = get_nearest_expiration(options, expiration)
if moneyness is not None:
calls = pd.DataFrame()
atm_call_iv = pd.DataFrame()
puts = pd.DataFrame()
atm_put_iv = pd.DataFrame()
skew_df = pd.DataFrame()
strikes = get_nearest_otm_strike(options, moneyness)
for day in days:
atm_call_strike = get_nearest_call_strike( # noqa:F841
options, day, options.last_price
)
call_strike = get_nearest_call_strike( # noqa:F841
options, day, strikes["call"]
)
if (
len(
options.chains[options.chains["dte"] == day].query(
"`optionType` == 'call'"
)
)
> 0
):
call_iv = options.chains[options.chains["dte"] == day].query(
"`optionType` == 'call' & `strike` == @call_strike"
)[["expiration", "strike", "impliedVolatility"]]
calls = pd.concat([calls, call_iv])
atm_call = options.chains[options.chains["dte"] == day].query(
"`optionType` == 'call' & `strike` == @atm_call_strike"
)[["expiration", "strike", "impliedVolatility"]]
atm_call_iv = pd.concat([atm_call_iv, atm_call])
atm_put_strike = get_nearest_put_strike( # noqa:F841
options, day, options.last_price
)
put_strike = get_nearest_put_strike( # noqa:F841
options, day, strikes["put"]
)
if (
len(
options.chains[options.chains["dte"] == day].query(
"`optionType` == 'put'"
)
)
> 0
):
put_iv = options.chains[options.chains["dte"] == day].query(
"`optionType` == 'put' & `strike` == @put_strike"
)[["expiration", "strike", "impliedVolatility"]]
puts = pd.concat([puts, put_iv])
atm_put = options.chains[options.chains["dte"] == day].query(
"`optionType` == 'put' & `strike` == @atm_put_strike"
)[["expiration", "strike", "impliedVolatility"]]
atm_put_iv = pd.concat([atm_put_iv, atm_put])
calls = calls.drop_duplicates(subset=["expiration"]).set_index("expiration")
atm_call_iv = atm_call_iv.drop_duplicates(subset=["expiration"]).set_index(
"expiration"
)
puts = puts.drop_duplicates(subset=["expiration"]).set_index("expiration")
atm_put_iv = atm_put_iv.drop_duplicates(subset=["expiration"]).set_index(
"expiration"
)
skew_df["Call Strike"] = calls["strike"]
skew_df["Call IV"] = calls["impliedVolatility"]
skew_df["Call ATM IV"] = atm_call_iv["impliedVolatility"]
skew_df["Call Skew"] = skew_df["Call IV"] - skew_df["Call ATM IV"]
skew_df["Put Strike"] = puts["strike"]
skew_df["Put IV"] = puts["impliedVolatility"]
skew_df["Put ATM IV"] = atm_put_iv["impliedVolatility"]
skew_df["Put Skew"] = skew_df["Put IV"] - skew_df["Put ATM IV"]
skew_df["ATM Skew"] = skew_df["Call ATM IV"] - skew_df["Put ATM IV"]
skew_df["IV Skew"] = skew_df["Call Skew"] - skew_df["Put Skew"]
return skew_df
calls = options.chains[options.chains["optionType"] == "call"]
puts = options.chains[options.chains["optionType"] == "put"]
call_skew = pd.DataFrame()
put_skew = pd.DataFrame()
skew_df = pd.DataFrame()
for day in days:
atm_call_strike = get_nearest_call_strike(options, day) # noqa:F841
if (
len(
options.chains[options.chains["dte"] == day].query(
"`optionType` == 'call'"
)
)
> 0
):
call = calls[calls["dte"] == day][
["expiration", "optionType", "strike", "impliedVolatility"]
]
call = call.set_index("expiration")
call_atm_iv = call.query("`strike` == @atm_call_strike")[
"impliedVolatility"
].iloc[0]
call["ATM IV"] = call_atm_iv
call["Skew"] = call["impliedVolatility"] - call["ATM IV"]
call_skew = pd.concat([call_skew, call])
atm_put_strike = get_nearest_put_strike(options, day) # noqa:F841
if (
len(
options.chains[options.chains["dte"] == day].query(
"`optionType` == 'put'"
)
)
> 0
):
put = puts[puts["dte"] == day][
["expiration", "optionType", "strike", "impliedVolatility"]
]
put = put.set_index("expiration")
put_atm_iv = put.query("`strike` == @atm_put_strike")[
"impliedVolatility"
].iloc[0]
put["ATM IV"] = put_atm_iv
put["Skew"] = put["impliedVolatility"] - put["ATM IV"]
put_skew = pd.concat([put_skew, put])
call_skew = call_skew.set_index(["strike", "optionType"], append=True)
put_skew = put_skew.set_index(["strike", "optionType"], append=True)
skew_df = pd.concat([call_skew, put_skew]).sort_index().reset_index()
cols = ["Expiration", "Strike", "Option Type", "IV", "ATM IV", "Skew"]
skew_df.columns = cols
if expiration != "":
if expiration not in options.expirations:
expiration = get_nearest_expiration(options, expiration)
return skew_df.query("`Expiration` == @expiration")
return skew_df