mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-01 23:12:21 +00:00
* 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 commit3736c311cb. * 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 commit8a30a44555. * 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 commitbd2170e2ad. * 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 commitfb82ff7bd9. * 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   * 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   * 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>
1802 lines
63 KiB
Python
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
|