├── .gitignore ├── LICENSE.md ├── README.md ├── backTester.py ├── base ├── __init__.py ├── call.py ├── callTest.py ├── option.py ├── optionTest.py ├── put.py ├── putTest.py ├── stock.py └── stockTest.py ├── dataHandler ├── __init__.py ├── csvData.py ├── csvDataTest.py ├── dataHandler.py ├── dataProviders.json ├── pricingConfig.json └── unitTestData │ ├── dataProvidersFakeColumnNotInCSV.json │ ├── dataProvidersFakeNoColsSpecified.json │ ├── dataProvidersFakeUnsupportedDataSource.json │ ├── dataProvidersFakeWithSettlementPrice.json │ └── dataProvidersFakeWrongNumColumns.json ├── events ├── __init__.py ├── event.py ├── signalEvent.py ├── signalEventTest.py ├── tickEvent.py └── tickEventTest.py ├── marketData └── iVolatility │ └── SPX │ └── SPX_2011_2017 │ └── RawIV_5day_sample.zip ├── optionPrimitives ├── __init__.py ├── nakedPut.py ├── nakedPutTest.py ├── optionPrimitive.py ├── putVertical.py ├── putVerticalTest.py ├── strangle.py └── strangleTest.py ├── portfolioManager ├── __init__.py ├── portfolio.py └── portfolioTest.py ├── riskManager ├── putVerticalRiskManagement.py ├── putVerticalRiskManagementTest.py ├── riskManagement.py ├── strangleRiskManagement.py └── strangleRiskManagementTest.py ├── sampleData ├── aapl_sample_ivolatility.csv ├── bad_column_name.csv └── spx_sample_ivolatility.csv ├── strategyManager ├── StrangleStrat.py ├── __init__.py ├── putVerticalStrat.py ├── putVerticalStratTest.py ├── strangleStratTest.py ├── strategy.py └── strategyTest.py └── utils ├── __init__.py └── combineCSVs.py /.gitignore: -------------------------------------------------------------------------------- 1 | # OS-dependent files 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # CSV files and doc files used for backtesting 10 | *.doc 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | .idea/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # Ignore specific files 108 | /marketData 109 | /utils 110 | positions_put_vertical_01_11_21.csv 111 | positions_put_vertical_01_11_21.log 112 | positions_put_vertical_01_12_21.csv 113 | positions_put_vertical_01_12_21.log 114 | positions_put_vertical_strat_close_at_50_percent_01_08_21_fixed_pl_percentage.csv 115 | positions_put_vertical_strat_close_at_50_percent_01_10_21_try_comms_fees.csv 116 | positions_strangle_01_15_21.csv 117 | positions_strangle_01_15_21.log 118 | session.log -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Michael Santoro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OptionSuite 2 | Option / stock strategy backtester and live trader* framework. 3 | 4 | To get started quickly, click the image below to bring up the tutorial video on YouTube. 5 | 6 | [![Video Tutorial](https://img.youtube.com/vi/gvzlKoPj57A/0.jpg)](https://www.youtube.com/watch?v=gvzlKoPj57A) 7 | 8 | # Looking for a winning strategy developed by this backtester? Check out [IncreaseYourStockReturns](https://www.increaseyourstockreturns.com/)! 9 | 10 | # Press / tutorials by others: 11 | 12 | [Pipkekit](https://pipekit.io/blog/options-backtesting-in-python-an-introductory-walkthrough) 13 | 14 | [Medium](https://medium.com/coinmonks/options-backtesting-in-python-an-introductory-walkthrough-fa14b32642ef) 15 | 16 | 17 | [Getting started](#getting-started) decribes what you need in order to get started backtesting. 18 | 19 | Please note that you need to purchase a [data package](#getting-the-data) in order to use this library since the sample data is quite limited. 20 | 21 | # Objective 22 | The objective of the OptionSuite library is to create a general framework to backtest options strategies and to be extensible enough to handle live trading. 23 | 24 | *Live trader is currently not supported, but the general framework is in place to enable support for live trading. 25 | 26 | # Overview of Library 27 | The library is designed in a modular way, and several abstractions are provided which allow the user to add additional features. The directory structure of the library is as follows: 28 | 29 | **base** - contains the abstract options class (option.py) and the two derived classes (call.py and put.py), which serve as the general options types for all other classes. 30 | 31 | **dataHandler** - contains the abstract class (dataHandler.py), which is set up to handle loading option data from different sources. The framework has been tested with CSV data, and a CSV data handler class (csvData.py) is provided to load tick data provided through the CSV format. An example CSV format is provided in the **sampleData** directory. More information on the CSV data needed for back testing is covered in the [getting started](#getting-started) section. 32 | 33 | **events** - the entire library / framework is event driven, and the abstract event class (event.py) handles the creation and deletion of events. The framework currently supports two different types of events: tick events (tickEvent.py) and signal events (signalEvent.py). The tick events are used to load data from the **dataHandler** and create the base option types (puts and calls). Signal events are generated to indicate that the criteria for the strategy in **strategyManager** has been successfully met. 34 | 35 | **optionPrimitives** - option primitives allow for naked puts and calls as well as combinations of puts and calls. For example, an option primtive could describe a naked put, or it could describe a strangle, which is a combination of puts and calls. Since certain trades like strangles are common, the option primitives abstract class (optionPrimitive.py) wraps the base types (calls and puts), and describes the functionality needed to create and update the primitive. The strangle primitive (strangle.py) and put vertical (putVertical.py) are fully functional. 36 | 37 | **portfolioManager** - the portfolio manager (portfolio.py) holds and manages all of the open positions. Potential positions are first generated by the **strategyManager**, and the portfolio manager opens a new positions if all risk paramters have been met. The portfolio is current held in memory, but a future implementation would ideally store the positions into a database or other non-volatile source. 38 | 39 | **sampleData** - a single file spx_sample_ivolatility.csv is provided. It serves as an example of the CSV data format provided by iVolatility. 40 | 41 | **strategyManager** - the strategy manager module provides an abstract class (strategy.py) which defines the basic parameters needed for an options strategy. The purpose of the strategy manager module is to filter the incoming tick data (by means of a tick event) to determine if a position should be opened (which would in turn fire a signal event). A full strangle stategy (StrangleStrat.py) and put vertical strategy (putVerticalStrat.py) are provided, which includes options for trade management as well as several different criteria for determining if a trade should be opened (signal event generated). 42 | 43 | **utils** - the utilities directory provides a utility for combining CSVs (e.g., if there are multiple CSVs for different years of historical data). 44 | 45 | **backTester.py** - this is the "main" method for the library. It sets up all parameters for a backtesting session, and initializes the **dataHandler** class, the **portfolioManager** class, and the **strategyManager** class. It is helpful to start with this file to see how all of the modules work together. 46 | 47 | # Getting Started 48 | *The library has been tested with Python 2.7+* 49 | 50 | To get started, you first need some historical data for the backtests. 51 | 52 | ## Getting the Data 53 | 54 | The *combinedCSV.csv* file used during development and testing contains SPX data from 1990 to 2017 provided by [iVolatility](http://www.ivolatility.com/fast_data_sales_form1.j). If you'd like to use the same dataset I did, then you want to request the EOD Raw IV dataset for SPX. 55 | 56 | *There is a 10% discount on all orders greater than $100 if you use code SupraCV10PCTOFF in the "Please tell us what data you want to receive:" field.* 57 | 58 | You can request different time periods. A large time period such as 1990 to 2017 is broken up into multiple CSVs. The *combineCSVs.py* in **utils** can be used to combine multiple CSVs into a single CSV. 59 | 60 | ## Loading the Data 61 | 62 | Once you have downloaded the data, simply update the three lines below, and you are ready to run *backTester.py*. 63 | 64 | ``` 65 | dataProviderPath = '/Users/msantoro/PycharmProjects/Backtester/dataHandler/dataProviders.json' 66 | dataProvider = 'iVolatility' 67 | filename = '/Users/msantoro/PycharmProjects/Backtester/sampleData/spx_sample_ivolatility.csv' 68 | ``` 69 | 70 | ## Visualizing the Data 71 | 72 | The output data is written to CSV in the *monitoring.csv* file. 73 | 74 | # Troubleshooting 75 | Please send bugs or any other issues you encounter to [msantoro@gmail.com](mailto:msantoro@gmail.com). I will do my best to help you get up and running. You can also report an issue using GitHub's issue tracker. 76 | 77 | -------------------------------------------------------------------------------- /backTester.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import datetime 3 | import decimal 4 | import logging 5 | import queue 6 | from dataHandler import csvData 7 | from events import event as event_class 8 | from riskManager import putVerticalRiskManagement 9 | from strategyManager import putVerticalStrat 10 | from portfolioManager import portfolio 11 | from collections import defaultdict 12 | 13 | """ 14 | This file runs the end-to-end backtesting session. 15 | """ 16 | 17 | 18 | class BackTestSession(object): 19 | """Class for holding all parameters of backtesting session.""" 20 | 21 | def __init__(self): 22 | 23 | # Create queue to hold events (ticks, signals, etc.). 24 | self.eventQueue = queue.Queue() 25 | 26 | # Create CsvData class object. 27 | dataProviderPath = './dataHandler/dataProviders.json' 28 | dataProvider = 'iVolatility' 29 | filename ='./sampleData/spx_sample_ivolatility.csv' 30 | self.dataHandler = csvData.CsvData(csvPath=filename, dataProviderPath=dataProviderPath, 31 | dataProvider=dataProvider, eventQueue=self.eventQueue) 32 | 33 | # Parameters for strategy. 34 | startDateTime = '01/01/1990' 35 | startDateTimeFormatted = datetime.datetime.strptime(startDateTime, '%m/%d/%Y') 36 | # Save maxCapitalToUse in the session since the run function requires it. 37 | self.maxCapitalToUse = decimal.Decimal(0.75) # Up to 75% of net liq can be used in trades. 38 | maxCapitalToUsePerTrade = decimal.Decimal(0.40) # 40% max capital to use per trade / strategy. 39 | startingCapital = 1000000 40 | strategyName = 'PUT_VERTICAL_STRAT' 41 | riskManagement = 'HOLD_TO_EXPIRATION' 42 | closeDuration = 0 # Number of days from expiration to close the trade. 43 | optPutToBuyDelta = -0.01 44 | maxPutToBuyDelta = -0.1 45 | minPutToBuyDelta = -0.005 46 | optPutToSellDelta = -0.25 47 | maxPutToSellDelta = -0.30 48 | minPutToSellDelta = -0.11 49 | underlyingTicker = 'SPX' 50 | orderQuantity = 1 51 | contractMultiplier = 100 52 | optimalDTE = 25 53 | minimumDTE = 20 54 | maximumDTE = 55 55 | maxBidAsk = decimal.Decimal(15) # Set to a large value to effectively disable. 56 | minCreditDebit = decimal.Decimal(1.00) 57 | 58 | # Set up portfolio and position monitoring. 59 | self.positionMonitoring = defaultdict(list) 60 | pricingSource = 'tastyworks' 61 | pricingSourceConfigFile = './dataHandler/pricingConfig.json' 62 | self.portfolioManager = portfolio.Portfolio(decimal.Decimal(startingCapital), self.maxCapitalToUse, 63 | maxCapitalToUsePerTrade, positionMonitoring=self.positionMonitoring) 64 | 65 | if strategyName != 'PUT_VERTICAL_STRAT': 66 | raise ValueError('Strategy not supported.') 67 | else: 68 | # TODO(msantoro): If statements below should use polymorphism where the riskManagement.py has all of the 69 | # base risk management types. 70 | if riskManagement == 'HOLD_TO_EXPIRATION': 71 | riskManagement = putVerticalRiskManagement.PutVerticalManagementStrategyTypes.HOLD_TO_EXPIRATION 72 | elif riskManagement == 'CLOSE_AT_50_PERCENT': 73 | riskManagement = putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT 74 | elif riskManagement == 'CLOSE_AT_50_PERCENT_OR_21_DAYS': 75 | riskManagement = putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS 76 | elif riskManagement == 'CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS': 77 | riskManagement = putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS 78 | elif riskManagement == 'CLOSE_AT_21_DAYS': 79 | riskManagement = putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_21_DAYS 80 | else: 81 | raise ValueError('Risk management type not supported.') 82 | if closeDuration <= 0: 83 | closeDuration = None 84 | riskManagementStrategy = putVerticalRiskManagement.PutVerticalRiskManagement(riskManagement, closeDuration) 85 | self.strategyManager = putVerticalStrat.PutVerticalStrat( 86 | self.eventQueue, optPutToBuyDelta, maxPutToBuyDelta, minPutToBuyDelta, optPutToSellDelta, 87 | maxPutToSellDelta, minPutToSellDelta, underlyingTicker, orderQuantity, contractMultiplier, 88 | riskManagementStrategy, pricingSource, pricingSourceConfigFile, startDateTimeFormatted, optimalDTE, 89 | minimumDTE, maximumDTE, maxBidAsk=maxBidAsk, maxCapitalToUsePerTrade=maxCapitalToUsePerTrade, 90 | minCreditDebit=minCreditDebit) 91 | 92 | # Write params to log file to be able to track experiments. 93 | # Set up logging for the session. 94 | logging.basicConfig(filename='log.log', level=logging.DEBUG) 95 | logging.info( 96 | 'optPutToSellDelta: {} maxPutToSellDelta: {} minPutToSellDelta: {} optPutToBuyDelta: {}' 97 | ' maxPutToBuyDelta: {} minPutToBuyDelta: {} underlyingTicker: {} orderQuantity: {}' 98 | ' optimalDTE: {} minimumDTE: {} maximumDTE: {} maxBidAsk: {} minCredit: {} riskManagement: {}' 99 | ' startingCapital: {} self.maxCapitalToUse: {} maxCapitalToUsePerTrade: {}' 100 | ' pricingSource: {}'.format( 101 | optPutToSellDelta, maxPutToSellDelta, minPutToSellDelta, optPutToBuyDelta, maxPutToBuyDelta, 102 | minPutToBuyDelta, underlyingTicker, orderQuantity, optimalDTE, minimumDTE, maximumDTE, maxBidAsk, 103 | minCreditDebit, riskManagementStrategy.getRiskManagementType(), startingCapital, 104 | self.maxCapitalToUse, maxCapitalToUsePerTrade, pricingSource)) 105 | 106 | 107 | def run(currentSession): 108 | while 1: # Infinite loop to keep processing items in queue. 109 | try: 110 | event = currentSession.eventQueue.get(False) 111 | except queue.Empty: 112 | # Get data for tick event. 113 | if not currentSession.dataHandler.getNextTick(): 114 | # Get out of infinite while loop; no more data available. 115 | break 116 | else: 117 | if event is not None: 118 | if event.type == event_class.EventTypes.TICK: 119 | currentSession.portfolioManager.updatePortfolio(event) 120 | # We pass the net liquidity and available buying power to the strategy. 121 | availableBuyingPower = decimal.Decimal(currentSession.maxCapitalToUse) * ( 122 | currentSession.portfolioManager.netLiquidity) - currentSession.portfolioManager.totalBuyingPower 123 | currentSession.strategyManager.checkForSignal(event, currentSession.portfolioManager.netLiquidity, 124 | availableBuyingPower) 125 | elif event.type == event_class.EventTypes.SIGNAL: 126 | currentSession.portfolioManager.onSignal(event) 127 | else: 128 | raise NotImplemented("Unsupported event.type '%s'." % event.type) 129 | 130 | 131 | if __name__ == "__main__": 132 | # Create a session and configure the session. 133 | session = BackTestSession() 134 | 135 | # Run the session. 136 | run(session) 137 | 138 | # Write position monitoring to CSV file. 139 | with open('monitoring.csv', 'w') as outfile: 140 | writer = csv.writer(outfile) 141 | writer.writerow(session.positionMonitoring.keys()) 142 | writer.writerows(zip(*session.positionMonitoring.values())) 143 | -------------------------------------------------------------------------------- /base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/base/__init__.py -------------------------------------------------------------------------------- /base/call.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from base import option 3 | 4 | 5 | @dataclasses.dataclass 6 | class Call(option.Option): 7 | """This class defines a CALL option, which inherits from the Option class.""" 8 | optionType: option.OptionTypes = option.OptionTypes.CALL 9 | -------------------------------------------------------------------------------- /base/callTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import unittest 4 | from base import call 5 | from base import option 6 | 7 | 8 | class TestCallOption(unittest.TestCase): 9 | def testCallOptionCreation(self): 10 | """Tests that a CALL option is created successfully.""" 11 | callOption = call.Call(underlyingTicker='SPY', strikePrice=250, expirationDateTime=datetime.datetime.strptime( 12 | '01/01/2050',"%m/%d/%Y")) 13 | # Trivial checks on the callOption object. 14 | self.assertEqual(callOption.strikePrice, 250) 15 | self.assertEqual(callOption.optionType, option.OptionTypes.CALL) 16 | 17 | 18 | if __name__ == '__main__': 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /base/option.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import dataclasses 3 | import datetime 4 | import decimal 5 | import enum 6 | import logging 7 | from typing import Optional, Text 8 | 9 | 10 | class OptionTypes(enum.Enum): 11 | PUT = 0 12 | CALL = 1 13 | 14 | 15 | @dataclasses.dataclass 16 | class Option(abc.ABC): 17 | """This class defines the basic type for the backtester -- an option. 18 | Put and call are derived from this class. 19 | 20 | Attributes: 21 | underlyingTicker: ticker symbol (e.g., SPY) of underlying. 22 | strikePrice: strike price of option. 23 | expirationDateTime: date/time at which option expires. 24 | underlyingPrice: price of the underlying / stock which has option derivatives in dollars. 25 | optionSymbol: special market symbol used to denote option. 26 | bidPrice: current bid price of option. 27 | askPrice: current asking price of option. 28 | tradePrice: price of the option when trade was executed / put on. 29 | settlementPrice: settlement price of the option; could be the mean of bid / ask but not always. 30 | openInterest: number of open option contracts. 31 | volume: number of contracts traded. 32 | dateTime: date / time of last updated option chain (this comes from the CSV). 33 | tradeDateTime: date / time that option was put on (this is populated once the option is added to the portfolio). 34 | delta: greek for quantifying percent of stock we're long or short (-1 to 1). 35 | theta: daily return in dollars if no movement in underlying price. 36 | gamma: describes rate of change of delta (float). 37 | rho: how much option price changes with change in interest rate (dollars). 38 | vega: change in price of option for every 1% change in volatility. 39 | impliedVol: implied volatility percentage. 40 | exchangeCode: symbol used to denote which exchanged used or where quote came from. 41 | """ 42 | underlyingTicker: Text 43 | strikePrice: decimal.Decimal 44 | expirationDateTime: datetime.datetime 45 | underlyingPrice: Optional[decimal.Decimal] = None 46 | optionSymbol: Optional[Text] = None 47 | bidPrice: Optional[decimal.Decimal] = None 48 | askPrice: Optional[decimal.Decimal] = None 49 | tradePrice: decimal.Decimal = None 50 | settlementPrice: decimal.Decimal = None 51 | openInterest: Optional[int] = None 52 | volume: Optional[int] = None 53 | dateTime: Optional[datetime.datetime] = None 54 | tradeDateTime: Optional[datetime.datetime] = None 55 | delta: Optional[float] = None 56 | theta: Optional[float] = None 57 | gamma: Optional[float] = None 58 | rho: Optional[float] = None 59 | vega: Optional[float] = None 60 | impliedVol: Optional[float] = None 61 | exchangeCode: Optional[Text] = None 62 | 63 | def __post_init__(self): 64 | if self.__class__ == Option: 65 | raise TypeError('Cannot instantiate abstract class.') 66 | 67 | def calcOptionPriceDiff(self) -> decimal.Decimal: 68 | """Calculate the difference in price of the put/call when the trade was put on versus its current value. 69 | 70 | :return: price difference (original price - current price). 71 | """ 72 | return self.tradePrice - self.settlementPrice 73 | 74 | def getNumDaysLeft(self) -> int: 75 | """Determine the number of days between the current date/time and expiration date / time. 76 | 77 | :return: number of days between curDateTime and expDateTime. 78 | """ 79 | return (self.expirationDateTime - self.dateTime).days 80 | 81 | def updateOption(self, updatedOption: 'Option') -> None: 82 | """Update the relevant values of the original option with those of the new option; e.g., update price, delta. 83 | 84 | :param updatedOption: new option from the latest tick. 85 | :raises ValueError: option cannot be updated. 86 | """ 87 | # Check that we are dealing with the same option. 88 | if self.underlyingTicker in updatedOption.underlyingTicker and ( 89 | self.strikePrice == updatedOption.strikePrice) and ( 90 | self.expirationDateTime == updatedOption.expirationDateTime): 91 | self.underlyingPrice = updatedOption.underlyingPrice 92 | self.bidPrice = updatedOption.bidPrice 93 | self.askPrice = updatedOption.askPrice 94 | self.settlementPrice = updatedOption.settlementPrice 95 | self.openInterest = updatedOption.openInterest 96 | self.volume = updatedOption.volume 97 | self.dateTime = updatedOption.dateTime 98 | self.delta = updatedOption.delta 99 | self.theta = updatedOption.theta 100 | self.gamma = updatedOption.gamma 101 | self.rho = updatedOption.rho 102 | self.vega = updatedOption.vega 103 | self.impliedVol = updatedOption.impliedVol 104 | else: 105 | logging.info( 106 | 'originalTicker: {} tickerToUpdate: {} originalStrike: {} strikeToUpdate: {} originalExp: {}' 107 | ' expToUpdate: {} '.format(self.underlyingTicker, updatedOption.underlyingTicker, 108 | self.strikePrice, updatedOption.strikePrice, self.expirationDateTime, 109 | updatedOption.expirationDateTime)) 110 | raise ValueError('Cannot update option; this option appears to be from a different option chain.') 111 | -------------------------------------------------------------------------------- /base/optionTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | from base import option 4 | 5 | 6 | class TestOptionsClass(unittest.TestCase): 7 | 8 | def testOptionClassCreation(self): 9 | """Tests than an exception is raised when class is instantiated.""" 10 | with self.assertRaisesRegex(TypeError, "Cannot instantiate abstract class."): 11 | option.Option(underlyingTicker='SPY', strikePrice=250, expirationDateTime=datetime.datetime.now()) 12 | 13 | 14 | if __name__ == '__main__': 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /base/put.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from base import option 3 | 4 | 5 | @dataclasses.dataclass 6 | class Put(option.Option): 7 | """This class defines a PUT option, which inherits from the Option class.""" 8 | optionType: option.OptionTypes = option.OptionTypes.PUT 9 | -------------------------------------------------------------------------------- /base/putTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import unittest 4 | from base import put 5 | 6 | 7 | class TestPutOption(unittest.TestCase): 8 | def setUp(self): 9 | self._putOptionToTest = put.Put(underlyingTicker='SPY', strikePrice=decimal.Decimal(250), 10 | dateTime=datetime.datetime.strptime('01/01/2021', 11 | "%m/%d/%Y"), 12 | expirationDateTime=datetime.datetime.strptime('01/01/2050', 13 | "%m/%d/%Y"), 14 | tradePrice=decimal.Decimal(3.00), settlementPrice=decimal.Decimal(1.25)) 15 | 16 | def testCalcOptionPriceDiff(self): 17 | """Tests that the difference between current price and trade price is calculated correctly.""" 18 | expectedPriceDiff = self._putOptionToTest.tradePrice - self._putOptionToTest.settlementPrice 19 | self.assertEqual(self._putOptionToTest.calcOptionPriceDiff(), expectedPriceDiff) 20 | 21 | def testNumberDaysUntilExpiration(self): 22 | """Tests that the number of days to expiration is computed correctly.""" 23 | expectedDays = self._putOptionToTest.expirationDateTime - self._putOptionToTest.dateTime 24 | self.assertEqual(self._putOptionToTest.getNumDaysLeft(), expectedDays.days) 25 | 26 | def testUpdateOptionSuccess(self): 27 | """Tests that option values are successfully updated with latest data.""" 28 | updatedPut = put.Put(underlyingTicker='SPY', strikePrice=250, delta=0.3, 29 | expirationDateTime=datetime.datetime.strptime('01/01/2050', 30 | "%m/%d/%Y"), 31 | bidPrice=decimal.Decimal(0.50), askPrice=decimal.Decimal(0.75)) 32 | self._putOptionToTest.updateOption(updatedPut) 33 | self.assertEqual(self._putOptionToTest.bidPrice, decimal.Decimal(0.50)) 34 | self.assertEqual(self._putOptionToTest.askPrice, decimal.Decimal(0.75)) 35 | 36 | def testUpdateOptionInvalidOptionStrikePrice(self): 37 | """Tests that error is raised if we update an option with different parameters (wrong strike price).""" 38 | updatedPut = put.Put(underlyingTicker='SPY', strikePrice=255, 39 | expirationDateTime=datetime.datetime.strptime('01/01/2050', 40 | "%m/%d/%Y")) 41 | with self.assertRaisesRegex(ValueError, 42 | ('Cannot update option; this option appears to be from a different option chain.')): 43 | self._putOptionToTest.updateOption(updatedPut) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /base/stock.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import datetime 3 | import decimal 4 | from typing import Optional, Text 5 | 6 | 7 | @dataclasses.dataclass 8 | class Stock: 9 | """This class defines one the basic types for the backtester -- a stock. 10 | Attributes: 11 | underlyingPrice: price of the underlying / stock which has option derivatives in dollars. 12 | underlyingTicker: ticker symbol (e.g., SPY) of underlying. 13 | bidPrice: current bid price of option. 14 | askPrice: current asking price of option. 15 | tradePrice: price of stock when order was executed. 16 | settlementPrice: current settlement price of stock 17 | openInterest: number of open option contracts. 18 | volume: number of contracts traded. 19 | dateTime: data / time of quote received; would also be data / time bought / sold. 20 | exchangeCode: symbol used to denote which exchanged used or where quote came from. 21 | openCost: cost to open the option trade. 22 | closeCost: cost to close out the option trade. 23 | """ 24 | underlyingPrice: decimal.Decimal 25 | underlyingTicker: Text 26 | bidPrice: Optional[decimal.Decimal] = None 27 | askPrice: Optional[decimal.Decimal] = None 28 | tradePrice: decimal.Decimal = None 29 | settlementPrice: decimal.Decimal = None 30 | openInterest: Optional[int] = 0 31 | volume: Optional[int] = 0 32 | dateTime: Optional[datetime.datetime] = None 33 | exchangeCode: Optional[Text] = None 34 | openCost: Optional[decimal.Decimal] = None 35 | closeCost: Optional[decimal.Decimal] = None 36 | -------------------------------------------------------------------------------- /base/stockTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import decimal 3 | from base import stock 4 | 5 | 6 | class TestStockClass(unittest.TestCase): 7 | def testStockClassCreation(self): 8 | # Test Stock class creation and getter methods 9 | stockObj = stock.Stock(underlyingTicker='SPY', underlyingPrice=decimal.Decimal(250)) 10 | # Test that the underlying ticker, direction, and underlying price are populated correctly. 11 | self.assertEqual(stockObj.underlyingTicker, 'SPY') 12 | self.assertEqual(stockObj.underlyingPrice, decimal.Decimal(250)) 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /dataHandler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/dataHandler/__init__.py -------------------------------------------------------------------------------- /dataHandler/csvData.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import csv 3 | import datetime 4 | import decimal 5 | import json 6 | import logging 7 | import pandas as pd 8 | import queue 9 | from dataHandler import dataHandler 10 | from base import call 11 | from base import put 12 | from base import option 13 | from events import tickEvent 14 | from typing import Iterable, Mapping, Text 15 | 16 | 17 | class CsvData(dataHandler.DataHandler): 18 | """This class handles data from CSV files which will be used for backtesting sessions.""" 19 | 20 | def __init__(self, csvPath: Text, dataProviderPath: Text, dataProvider: Text, eventQueue: queue.Queue) -> None: 21 | """Initializes CSV data parameters for file reading. 22 | 23 | Attributes: 24 | csvPath: path to CSV file used in backtesting. 25 | dataProviderPath: path to data provider JSON file. 26 | dataProvider: historical data provider (e.g, provider of CSV). 27 | eventQueue: location to place new data tick event. 28 | """ 29 | self.__csvPath = csvPath 30 | self.__dataProviderPath = dataProviderPath 31 | self.__curTimeDate = None 32 | self.__dataConfig = None 33 | self.__csvReader = None 34 | self.__csvColumnNames = None 35 | self.__dateColumnName = None 36 | self.__nextTimeDateRow = None 37 | self.__dataProvider = dataProvider 38 | self.__eventQueue = eventQueue 39 | 40 | # Open data source. Raises exception if failure. 41 | self.__dataConfig = self.__openDataSource() 42 | 43 | def __openDataSource(self) -> Mapping[Text, Mapping[Text, Mapping[Text, Text]]]: 44 | """Used to connect to the data source for the first time. In the case of a CSV, this means opening the file. 45 | The directory used is determined during initialization. 46 | 47 | :return dictionary from dataProviders.json file. 48 | :raises OSError: Cannot find a CSV at specified location. 49 | :raises ValueError: Cannot load data as a JSON file. 50 | :raises ValueError: Requested data provider not found in JSON file. 51 | :raises ValueError: Number of CSV columns not provided in JSON file. 52 | :raises ValueError: Number of columns read from CSV does not match number of columns in JSON file. 53 | """ 54 | try: 55 | fileHandle = open(self.__csvPath, 'r') 56 | except OSError as e: 57 | raise OSError('Unable to open CSV at location: %s.' % self.__csvPath) from e 58 | 59 | # Load data provider information from dataProviders.json file. 60 | try: 61 | with open(self.__dataProviderPath) as dataProvider: 62 | dataConfig = json.load(dataProvider) 63 | except (FileNotFoundError, json.decoder.JSONDecodeError) as e: 64 | raise ValueError('Failure when trying to open / load data from JSON file: %s.' % ( 65 | self.__dataProviderPath)) from e 66 | 67 | # Check that data provider in JSON file matches the provided string in self._dataProvider 68 | if self.__dataProvider not in dataConfig: 69 | raise ValueError( 70 | 'The requested data provider: %s was not found in dataProviders.json' % self.__dataProvider) 71 | 72 | # Check that the number of columns in the CSV matches the number specified by the config file. 73 | self.__csvReader = csv.DictReader(fileHandle) 74 | self.__csvColumnNames = self.__csvReader.fieldnames 75 | numberCsvColumns = len(self.__csvColumnNames) 76 | if 'number_columns' not in dataConfig[self.__dataProvider]: 77 | raise ValueError('number_columns was not provided in dataProviders.json file.') 78 | if not numberCsvColumns == dataConfig[self.__dataProvider]['number_columns']: 79 | raise ValueError( 80 | 'Number of columns in CSV and dataProviders.json do not match.') 81 | return dataConfig 82 | 83 | def __getMatchingRows(self) -> Iterable[Iterable[Text]]: 84 | """Gets all rows in CSV that match the current time / date. 85 | 86 | :return List of lists of matching rows. 87 | """ 88 | rowList = [] 89 | for row in self.__csvReader: 90 | if datetime.datetime.strptime(row[self.__dateColumnName], 91 | self.__dataConfig[self.__dataProvider][ 92 | 'date_time_format']) == self.__curTimeDate: 93 | rowList.append(row) 94 | else: 95 | # Save the last row that doesn't match the curTimeDate, so we can use it for the next option chain. 96 | self.__nextTimeDateRow = row 97 | break 98 | return rowList 99 | 100 | def __getOptionChain(self) -> pd.DataFrame: 101 | """Used to get the option chain data for the underlying. The option chain consists of all the puts and calls 102 | at all strikes currently listed for the underlying. 103 | 104 | :return Pandas dataframe with option chain data. 105 | """ 106 | self.__dateColumnName = self.__dataConfig[self.__dataProvider]['column_names']['dateTime'] 107 | if not self.__dateColumnName in self.__csvColumnNames: 108 | raise TypeError('The dateColumnName was not found in the CSV.') 109 | 110 | # Get the first date from the CSV if self.__curTimeDate is None. 111 | if self.__curTimeDate is None: 112 | # Find the index of the date column in the header row of the CSV. 113 | rowList = [] 114 | # Get the next row of the CSV and convert the date column to a datetime object. 115 | row = next(self.__csvReader) 116 | rowList.append(row) 117 | self.__curTimeDate = datetime.datetime.strptime(row[self.__dateColumnName], 118 | self.__dataConfig[self.__dataProvider]['date_time_format']) 119 | # Get the rest of the rows that match the curTimeDate. 120 | rowList.extend(self.__getMatchingRows()) 121 | 122 | # Create a Pandas dataframe from the rows with matching date. 123 | return pd.DataFrame(rowList, columns=self.__csvColumnNames) 124 | 125 | else: 126 | if self.__nextTimeDateRow is None: 127 | logging.warning('None was returned for the nextTimeDateRow in the CSV reader.') 128 | return pd.DataFrame() 129 | # Get the date / time from the previously stored row. 130 | self.__curTimeDate = datetime.datetime.strptime(self.__nextTimeDateRow[self.__dateColumnName], 131 | self.__dataConfig[self.__dataProvider]['date_time_format']) 132 | 133 | # Get all the CSV rows for the curTimeDate. 134 | rowList = [] 135 | rowList.append(self.__nextTimeDateRow) 136 | # Get the rest of the rows that match the curTimeDate. 137 | rowList.extend(self.__getMatchingRows()) 138 | 139 | # If no rows were added above, it means that there's no more data to read from the CSV. 140 | if len(rowList) == 1: 141 | self.__nextTimeDateRow = None 142 | return pd.DataFrame() 143 | # Create a Pandas dataframe from the list of lists. 144 | return pd.DataFrame(rowList, columns=self.__csvColumnNames) 145 | 146 | def __createBaseType(self, optionChain: pd.DataFrame) -> Iterable[option.Option]: 147 | """Convert an option chain held in a dataframe to base option types (calls or puts). 148 | 149 | Attributes: 150 | optionChain: Pandas dataframe with optionChain data as rows. 151 | 152 | :raises ValueError: Symbol for put/call in JSON not found in dataframe column 153 | :raises ValueError: Dictionary sizes don't match. 154 | :raises ValueError: optionType not found in the dataProviders.json file. 155 | :raises ValueError: dataProvider.json column name not found in CSV. 156 | :return: List of Option base type objects (puts or calls). 157 | """ 158 | optionObjects = [] 159 | optionTypeField = 'optionType' 160 | dataProviderConfig = self.__dataConfig[self.__dataProvider] 161 | # Create a dictionary for the fields that we will read from each row of the dataframe. The fields should 162 | # also be specified in the dataProviders.json file. 163 | optionFieldDict = self.__dataConfig[self.__dataProvider]['column_names'] 164 | optionDict = copy.deepcopy(optionFieldDict) 165 | # We don't need the optionTypeField in optionDict, so let's delete it. 166 | del optionDict[optionTypeField] 167 | for _, row in optionChain.iterrows(): 168 | # Defaults to PUT (True). 169 | putOrCall = True 170 | optionTypeFound = False 171 | for option_column_name, dataframe_column_name in optionFieldDict.items(): 172 | # Check that we need to look up the field (don't need field if it's blank in dataProviders.json). 173 | if not dataframe_column_name: 174 | continue 175 | if dataframe_column_name not in row: 176 | raise ValueError( 177 | 'Column name %s in dataProvider.json not found in CSV.' % dataframe_column_name) 178 | if option_column_name == optionTypeField: 179 | optionType = row[dataframe_column_name] 180 | # Convert any lowercase symbols to uppercase. 181 | optionType = str(optionType).upper() 182 | if optionType == dataProviderConfig['call_symbol_abbreviation']: 183 | putOrCall = False 184 | elif optionType == dataProviderConfig['put_symbol_abbreviation']: 185 | putOrCall = True 186 | else: 187 | raise ValueError( 188 | 'Symbol for put / call in dataProviders.json not found in optionChain dataframe.') 189 | # Will be used to remove optionTypeField from the optionFieldDict to make the processing below more 190 | # straightforward. 191 | optionTypeFound = True 192 | else: 193 | optionDict[option_column_name] = row[dataframe_column_name] 194 | 195 | if not optionTypeFound: 196 | raise ValueError('dataProviders.json must have an entry for optionType') 197 | 198 | # For futures options, we rely on settlementPrice, and for index options, we rely on tradePrice. 199 | # We will use one variable settlementPrice for both, so we set the settlementPrice = tradePrice for 200 | # index options. Let's check if settlementPrice is None or empty. 201 | if optionDict['settlementPrice'] is None or not optionDict[ 202 | 'settlementPrice']: # For index options 203 | if optionDict['bidPrice'] is not None and optionDict['askPrice'] is not None: 204 | optionDict['tradePrice'] = (decimal.Decimal(optionDict['bidPrice']) + decimal.Decimal( 205 | optionDict['askPrice'])) / decimal.Decimal(2.0) 206 | optionDict['settlementPrice'] = optionDict['tradePrice'] 207 | else: # For future options. 208 | optionDict['tradePrice'] = optionDict['settlementPrice'] 209 | 210 | # Do some formatting of the entries. 211 | argsDict = {'underlyingTicker': optionDict['underlyingTicker'] if optionDict[ 212 | 'underlyingTicker'] else None, 213 | 'strikePrice': decimal.Decimal(optionDict['strikePrice']) if optionDict[ 214 | 'strikePrice'] else None, 215 | 'delta': float(optionDict['delta']) if optionDict['delta'] else None, 216 | 'expirationDateTime': datetime.datetime.strptime( 217 | optionDict['expirationDateTime'], dataProviderConfig['date_time_format']) if optionDict[ 218 | 'expirationDateTime'] else None, 219 | 'underlyingPrice': decimal.Decimal(optionDict['underlyingPrice'] if optionDict[ 220 | 'underlyingPrice'] else None), 221 | 'optionSymbol': optionDict['optionSymbol'] if optionDict['optionSymbol'] else None, 222 | 'bidPrice': decimal.Decimal(optionDict['bidPrice']) if optionDict[ 223 | 'bidPrice'] else None, 224 | 'askPrice': decimal.Decimal(optionDict['askPrice']) if optionDict[ 225 | 'askPrice'] else None, 226 | 'settlementPrice': decimal.Decimal(optionDict['settlementPrice']) if optionDict[ 227 | 'settlementPrice'] else None, 228 | 'tradePrice': decimal.Decimal(optionDict['tradePrice']) if optionDict[ 229 | 'tradePrice'] else None, 230 | 'openInterest': int(optionDict['openInterest']) if optionDict[ 231 | 'openInterest'] else None, 232 | 'volume': int(optionDict['volume']) if optionDict['volume'] else None, 233 | 'dateTime': datetime.datetime.strptime(optionDict['dateTime'], 234 | dataProviderConfig['date_time_format']) if optionDict[ 235 | 'dateTime'] else None, 236 | 'tradeDateTime': datetime.datetime.strptime( 237 | optionDict['dateTime'], dataProviderConfig['date_time_format']) if optionDict[ 238 | 'dateTime'] else None, 239 | 'theta': float(optionDict['theta']) if optionDict['theta'] else None, 240 | 'gamma': float(optionDict['gamma']) if optionDict['gamma'] else None, 241 | 'rho': float(optionDict['rho']) if optionDict['rho'] else None, 242 | 'vega': float(optionDict['vega']) if optionDict['vega'] else None, 243 | 'impliedVol': float(optionDict['impliedVol']) if optionDict['impliedVol'] else None, 244 | 'exchangeCode': optionDict['exchangeCode'] if optionDict['exchangeCode'] else None, 245 | } 246 | if not putOrCall: 247 | optionObjects.append(call.Call(**argsDict)) 248 | else: 249 | optionObjects.append(put.Put(**argsDict)) 250 | return optionObjects 251 | 252 | def getNextTick(self) -> bool: 253 | """Used to get the next available piece of data from the data source. For the CSV example, this would likely be 254 | the next row for a stock or group of rows for an option chain. 255 | 256 | :return True / False indicating if there is data available. 257 | """ 258 | if self.__dataConfig[self.__dataProvider]['data_source_type'] == 'options': 259 | # Get optionChain as a dataframe. 260 | optionChain = self.__getOptionChain() 261 | if len(optionChain.index) == 0: 262 | # No more data available. 263 | return False 264 | # Convert optionChain from a dataframe to Option class objects. 265 | optionChainObjs = self.__createBaseType(optionChain) 266 | # Create tick event with option chain objects. 267 | event = tickEvent.TickEvent() 268 | event.createEvent(optionChainObjs) 269 | self.__eventQueue.put(event) 270 | return True 271 | elif self.__dataConfig[self.__dataProvider]['data_source_type'] == 'stocks': 272 | pass 273 | else: 274 | raise TypeError('data_source_type not supported.') 275 | -------------------------------------------------------------------------------- /dataHandler/csvDataTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import decimal 3 | from dataHandler import csvData 4 | import queue 5 | 6 | 7 | class TestCSVHandler(unittest.TestCase): 8 | 9 | def setUp(self): 10 | # Create CsvData class object. 11 | self._dataProvider = 'iVolatility' 12 | self._dataProviderPath = 'dataHandler/dataProviders.json' 13 | self._csvPath = 'sampleData/aapl_sample_ivolatility.csv' 14 | self._eventQueue = queue.Queue() 15 | self._csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=self._dataProviderPath, 16 | dataProvider=self._dataProvider, eventQueue=self._eventQueue) 17 | 18 | def testOpenDataSourceNoCSVFound(self): 19 | """Tests that an exception is raised when no CSV is found.""" 20 | with self.assertRaisesRegex(OSError, 'Unable to open CSV at location: bad_path_name.'): 21 | csvData.CsvData(csvPath='bad_path_name', dataProviderPath=self._dataProviderPath, 22 | dataProvider=self._dataProvider, eventQueue=self._eventQueue) 23 | 24 | def testOpenDataSourceNoDataProviderJSON(self): 25 | with self.assertRaisesRegex( 26 | ValueError, ( 27 | 'Failure when trying to open / load data from JSON file: %s.' % 'bad_path_name')): 28 | csvData.CsvData(csvPath=self._csvPath, dataProviderPath='bad_path_name', 29 | dataProvider=self._dataProvider, eventQueue=self._eventQueue) 30 | 31 | def testOpenDataSourceInvalidDataProvider(self): 32 | """Tests that an exception is raised if the requested data provider isn't in the config file.""" 33 | with self.assertRaisesRegex(ValueError, ('The requested data provider: unknown_data_provider was not found in ' 34 | 'dataProviders.json')): 35 | csvData.CsvData(csvPath=self._csvPath, dataProviderPath=self._dataProviderPath, 36 | dataProvider='unknown_data_provider', eventQueue=self._eventQueue) 37 | 38 | def testOpenDataSourceNumberColumnsNotSpecified(self): 39 | """Tests than an exception is raised if the number of columns field is not provided in dataProvider.json""" 40 | dataProviderPath = 'dataHandler/unitTestData/dataProvidersFakeNoColsSpecified.json' 41 | dataProvider = 'test_provider' 42 | with self.assertRaisesRegex(ValueError, 43 | 'number_columns was not provided in dataProviders.json file.'): 44 | csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, dataProvider=dataProvider, 45 | eventQueue=self._eventQueue) 46 | 47 | def testOpenDataSourceWrongNumberColumns(self): 48 | """Tests than an exception is raised if the wrong number of columns is specified in dataProvider.json""" 49 | dataProviderPath = 'dataHandler/unitTestData/dataProvidersFakeWrongNumColumns.json' 50 | dataProvider = 'test_provider' 51 | with self.assertRaisesRegex( 52 | ValueError, 'Number of columns in CSV and dataProviders.json do not match.'): 53 | csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, dataProvider=dataProvider, 54 | eventQueue=self._eventQueue) 55 | 56 | def testGetOptionChain(self): 57 | """Tests that an option chain is successfully read from CSV file.""" 58 | # The first and second calls to getNextTick should load one option chain into the queue and return True, 59 | # and the third call should return False 60 | self.assertTrue(self._csvObj.getNextTick()) 61 | self.assertTrue(self._eventQueue.qsize(), 1) 62 | self.assertTrue(self._csvObj.getNextTick()) 63 | self.assertTrue(self._eventQueue.qsize(), 2) 64 | self.assertFalse(self._csvObj.getNextTick()) 65 | self.assertTrue(self._eventQueue.qsize(), 2) 66 | 67 | # Check number of option objects in the first and second queue positions. 68 | desiredNumObjects = 1822 69 | self.assertEqual(len(self._eventQueue.get().getData()), desiredNumObjects) 70 | self.assertEqual(len(self._eventQueue.get().getData()), desiredNumObjects) 71 | self.assertEqual(self._eventQueue.qsize(), 0) 72 | 73 | def testGetOptionChainBadColumnName(self): 74 | """Tests that an exception is raised if column name in the CSV doesn't match the one in dataProviders.json.""" 75 | # Create CsvData class object. 76 | filename = 'sampleData/bad_column_name.csv' 77 | csvObj = csvData.CsvData(csvPath=filename, dataProviderPath=self._dataProviderPath, 78 | dataProvider=self._dataProvider, eventQueue=self._eventQueue) 79 | 80 | with self.assertRaisesRegex(TypeError, 'The dateColumnName was not found in the CSV'): 81 | csvObj.getNextTick() 82 | 83 | def testGetNextTick(self): 84 | """Tests that Put and Call objects are created successfully from a tick event.""" 85 | # First row in the sample data is a call, and second row is a put. 86 | eventQueue = queue.Queue() 87 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=self._dataProviderPath, 88 | dataProvider=self._dataProvider, eventQueue=eventQueue) 89 | csvObj.getNextTick() 90 | optionChainObjs = eventQueue.get().getData() 91 | desiredCallAskPrice = decimal.Decimal(40.45) 92 | desiredPutAskPrice = decimal.Decimal(0.01) 93 | desiredStrikePrice = 55 94 | desiredUnderlyingTicker = 'AAPL' 95 | self.assertEqual(optionChainObjs[0].underlyingTicker, desiredUnderlyingTicker) 96 | self.assertEqual(optionChainObjs[0].strikePrice, desiredStrikePrice) 97 | self.assertEqual(optionChainObjs[1].underlyingTicker, desiredUnderlyingTicker) 98 | self.assertEqual(optionChainObjs[1].strikePrice, desiredStrikePrice) 99 | self.assertAlmostEqual(optionChainObjs[0].askPrice, desiredCallAskPrice) 100 | self.assertAlmostEqual(optionChainObjs[1].askPrice, desiredPutAskPrice) 101 | 102 | def testGetTwoOptionChains(self): 103 | """Tests that we are able to read data from two different option chains (different dates).""" 104 | eventQueue = queue.Queue() 105 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=self._dataProviderPath, 106 | dataProvider=self._dataProvider, eventQueue=eventQueue) 107 | lastElementIndex = 1821 108 | # Gets the first option chain from 8/7/2014. 1822 rows in CSV. 109 | csvObj.getNextTick() 110 | # Gets the second option chain 8/8/2014. 1822 rows in CSV. 111 | csvObj.getNextTick() 112 | optionChain1 = eventQueue.get().getData() 113 | optionChain2 = eventQueue.get().getData() 114 | self.assertEqual(optionChain1[0].dateTime, optionChain1[lastElementIndex].dateTime) 115 | self.assertEqual(optionChain2[0].dateTime, optionChain2[lastElementIndex].dateTime) 116 | 117 | def testColumnNotInCSVNotSupportedColumn(self): 118 | """Tests that an exception is raised if a column from dataProvider.json is not in the CSV.""" 119 | dataProviderPath = 'dataHandler/unitTestData/dataProvidersFakeColumnNotInCSV.json' 120 | dataProvider = 'test_provider' 121 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, dataProvider=dataProvider, 122 | eventQueue=self._eventQueue) 123 | with self.assertRaisesRegex(ValueError, 124 | 'Column name dummy_value in dataProvider.json not found in CSV.'): 125 | csvObj.getNextTick() 126 | 127 | def testColumnNotInCSVNotSupportedDataSource(self): 128 | """Tests that an exception is raised if a column from dataProvider.json is not in the CSV.""" 129 | dataProviderPath = 'dataHandler/unitTestData/dataProvidersFakeUnsupportedDataSource.json' 130 | dataProvider = 'test_provider' 131 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, 132 | dataProvider=dataProvider, 133 | eventQueue=self._eventQueue) 134 | with self.assertRaisesRegex(TypeError, 135 | 'data_source_type not supported.'): 136 | csvObj.getNextTick() 137 | 138 | def testCheckTradePriceIsCorrectForIndexOptions(self): 139 | """Tests that the trade and settlement price is correct for index options.""" 140 | dataProvider = 'iVolatility' 141 | dataProviderPath = 'dataHandler/dataProviders.json' 142 | eventQueue = queue.Queue() 143 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, dataProvider=dataProvider, 144 | eventQueue=eventQueue) 145 | csvObj.getNextTick() 146 | option = eventQueue.get().getData()[0] 147 | self.assertEqual(option.tradePrice, option.settlementPrice) 148 | self.assertEqual(option.settlementPrice, option.tradePrice) 149 | 150 | def testCheckTradePriceIsCorrectForFuturesOptions(self): 151 | """Tests that the trade and settlement price is correct for futures options.""" 152 | dataProvider = 'iVolatility' 153 | dataProviderPath = 'dataHandler/unitTestData/dataProvidersFakeWithSettlementPrice.json' 154 | eventQueue = queue.Queue() 155 | csvObj = csvData.CsvData(csvPath=self._csvPath, dataProviderPath=dataProviderPath, dataProvider=dataProvider, 156 | eventQueue=eventQueue) 157 | csvObj.getNextTick() 158 | option = eventQueue.get().getData()[0] 159 | # Note that the settlementPrice for the first row of the CSV is not the same as the (bidPrice + askPrice) / 2, 160 | # which is what allows us to carry out the test below. 161 | self.assertEqual(option.tradePrice, option.settlementPrice) 162 | 163 | 164 | if __name__ == '__main__': 165 | unittest.main() 166 | -------------------------------------------------------------------------------- /dataHandler/dataHandler.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class DataHandler(abc.ABC): 5 | """This class is a generic type for handling incoming data. Incoming data sources could be historical data in the 6 | form of a CSV or a database, or it could be live tick data coming from an exchange (not currently supported).""" 7 | 8 | @abc.abstractmethod 9 | def getNextTick(self) -> bool: 10 | """Used to get the next available piece of data from the data source. For a CSV, this would be the next row of 11 | the CSV. 12 | 13 | :return True / False indicating if data is available. 14 | """ 15 | pass 16 | -------------------------------------------------------------------------------- /dataHandler/dataProviders.json: -------------------------------------------------------------------------------- 1 | {"iVolatility": { 2 | "number_columns": 25, 3 | "column_names": { 4 | "dateTime": "date", 5 | "underlyingTicker": "symbol", 6 | "exchangeCode": "exchange", 7 | "optionSymbol": "option_symbol", 8 | "optionType": "call/put", 9 | "strikePrice": "strike", 10 | "underlyingPrice": "stock_price_close", 11 | "askPrice": "ask", 12 | "bidPrice": "bid", 13 | "settlementPrice": "mean_price", 14 | "tradePrice": "", 15 | "tradeDataTime": "", 16 | "impliedVol": "iv", 17 | "volume": "volume", 18 | "openInterest": "open_interest", 19 | "delta": "delta", 20 | "theta": "theta", 21 | "vega": "vega", 22 | "gamma": "gamma", 23 | "rho": "rho", 24 | "expirationDateTime": "option_expiration" 25 | }, 26 | "call_symbol_abbreviation": "C", 27 | "put_symbol_abbreviation": "P", 28 | "date_time_format": "%m/%d/%Y", 29 | "data_source_type": "options" 30 | }, 31 | 32 | "iVolatility_futures": { 33 | "number_columns": 29, 34 | "column_names": { 35 | "dateTime": "date", 36 | "underlyingTicker": "underlying", 37 | "exchangeCode": "exchange", 38 | "optionSymbol": "futures_symbol", 39 | "optionType": "call_put", 40 | "strikePrice": "strike", 41 | "underlyingPrice": "futures_close", 42 | "askPrice": "ask", 43 | "bidPrice": "bid", 44 | "settlementPrice": "settlement", 45 | "tradePrice": "", 46 | "impliedVol": "iv", 47 | "volume": "volume", 48 | "openInterest": "open_interest", 49 | "delta": "delta", 50 | "theta": "theta", 51 | "vega": "vega", 52 | "gamma": "gamma", 53 | "rho": "", 54 | "expirationDateTime": "options_expiration_date" 55 | }, 56 | "call_symbol_abbreviation": "C", 57 | "put_symbol_abbreviation": "P", 58 | "date_time_format": "%m/%d/%Y", 59 | "data_source_type": "options" 60 | }} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /dataHandler/pricingConfig.json: -------------------------------------------------------------------------------- 1 | {"tastyworks": { 2 | "stock_options": { 3 | "index_option": { 4 | "open": { 5 | "commission_per_contract": 1.00, 6 | "proprietary_index_fee_per_contract": 0.65, 7 | "max_commission_per_leg": 0.00, 8 | "clearing_fee_per_contract": 0.10, 9 | "orf_fee_per_contract": 0.02915, 10 | "sec_fee_per_contract_wo_trade_price": 0.00051 11 | }, 12 | "close": { 13 | "commission_per_contract": 0.00, 14 | "proprietary_index_fee_per_contract": 0.65, 15 | "clearing_fee_per_contract": 0.10, 16 | "orf_fee_per_contract": 0.02915, 17 | "finra_taf_per_contract": 0.002, 18 | "sec_fee_per_contract_wo_trade_price": 0.00051 19 | } 20 | }, 21 | "equity_or_etf_option": { 22 | "open": { 23 | "commission_per_contract": 1.00, 24 | "max_commission_per_leg": 10.00, 25 | "clearing_fee_per_contract": 0.10, 26 | "orf_fee_per_contract": 0.02915, 27 | "sec_fee_per_contract_wo_trade_price": 0.00051 28 | }, 29 | "close": { 30 | "commission_per_contract": 0.00, 31 | "clearing_fee_per_contract": 0.10, 32 | "orf_fee_per_contract": 0.02915, 33 | "finra_taf_per_contract": 0.002, 34 | "sec_fee_per_contract_wo_trade_price": 0.00051 35 | } 36 | } 37 | } 38 | }, 39 | "tastyworks_futures": { 40 | "futures_options": { 41 | "es_option": { 42 | "open": { 43 | "commission_per_contract": 2.50, 44 | "clearing_fee_per_contract": 0.30, 45 | "nfa_fee_per_contract": 0.02, 46 | "exchange_fee_per_contract": 0.55 47 | }, 48 | "close": { 49 | "commission_per_contract": 0.00, 50 | "clearing_fee_per_contract": 0.30, 51 | "nfa_fee_per_contract": 0.02, 52 | "exchange_fee_per_contract": 0.55 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /dataHandler/unitTestData/dataProvidersFakeColumnNotInCSV.json: -------------------------------------------------------------------------------- 1 | {"test_provider": { 2 | "number_columns": 25, 3 | "column_names": { 4 | "dateTime": "date", 5 | "underlyingTicker": "symbol", 6 | "exchangeCode": "exchange", 7 | "optionSymbol": "option_symbol", 8 | "optionType": "call/put", 9 | "strikePrice": "strike", 10 | "underlyingPrice": "stock_price_close", 11 | "askPrice": "ask", 12 | "bidPrice": "bid", 13 | "settlementPrice": "", 14 | "tradePrice": "", 15 | "tradeDataTime": "", 16 | "impliedVol": "iv", 17 | "volume": "volume", 18 | "openInterest": "open_interest", 19 | "delta": "delta", 20 | "theta": "theta", 21 | "vega": "vega", 22 | "gamma": "gamma", 23 | "rho": "rho", 24 | "expirationDateTime": "option_expiration", 25 | "dummyColumn": "dummy_value" 26 | }, 27 | "call_symbol_abbreviation": "C", 28 | "put_symbol_abbreviation": "P", 29 | "date_time_format": "%m/%d/%Y", 30 | "data_source_type": "options" 31 | }} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /dataHandler/unitTestData/dataProvidersFakeNoColsSpecified.json: -------------------------------------------------------------------------------- 1 | {"test_provider": { 2 | "column_names": { 3 | "dateTime": "date", 4 | "underlyingTicker": "symbol", 5 | "exchangeCode": "exchange", 6 | "optionSymbol": "option_symbol", 7 | "optionType": "call/put", 8 | "strikePrice": "strike", 9 | "underlyingPrice": "stock_price_close", 10 | "askPrice": "ask", 11 | "bidPrice": "bid", 12 | "settlementPrice": "", 13 | "tradePrice": "", 14 | "tradeDataTime": "", 15 | "impliedVol": "iv", 16 | "volume": "volume", 17 | "openInterest": "open_interest", 18 | "delta": "delta", 19 | "theta": "theta", 20 | "vega": "vega", 21 | "gamma": "gamma", 22 | "rho": "rho", 23 | "expirationDateTime": "option_expiration" 24 | }, 25 | "call_symbol_abbreviation": "C", 26 | "put_symbol_abbreviation": "P", 27 | "date_time_format": "%m/%d/%Y", 28 | "data_source_type": "options" 29 | }} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dataHandler/unitTestData/dataProvidersFakeUnsupportedDataSource.json: -------------------------------------------------------------------------------- 1 | {"test_provider": { 2 | "number_columns": 25, 3 | "column_names": { 4 | "dateTime": "date", 5 | "underlyingTicker": "symbol", 6 | "exchangeCode": "exchange", 7 | "optionSymbol": "option_symbol", 8 | "optionType": "call/put", 9 | "strikePrice": "strike", 10 | "underlyingPrice": "stock_price_close", 11 | "askPrice": "ask", 12 | "bidPrice": "bid", 13 | "settlementPrice": "", 14 | "tradePrice": "", 15 | "tradeDataTime": "", 16 | "impliedVol": "iv", 17 | "volume": "volume", 18 | "openInterest": "open_interest", 19 | "delta": "delta", 20 | "theta": "theta", 21 | "vega": "vega", 22 | "gamma": "gamma", 23 | "rho": "rho", 24 | "expirationDateTime": "option_expiration" 25 | }, 26 | "call_symbol_abbreviation": "C", 27 | "put_symbol_abbreviation": "P", 28 | "date_time_format": "%m/%d/%Y", 29 | "data_source_type": "unsupported_type" 30 | }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /dataHandler/unitTestData/dataProvidersFakeWithSettlementPrice.json: -------------------------------------------------------------------------------- 1 | {"iVolatility": { 2 | "number_columns": 25, 3 | "column_names": { 4 | "dateTime": "date", 5 | "underlyingTicker": "symbol", 6 | "exchangeCode": "exchange", 7 | "optionSymbol": "option_symbol", 8 | "optionType": "call/put", 9 | "strikePrice": "strike", 10 | "underlyingPrice": "stock_price_close", 11 | "askPrice": "ask", 12 | "bidPrice": "bid", 13 | "settlementPrice": "settlement", 14 | "tradePrice": "", 15 | "tradeDataTime": "", 16 | "impliedVol": "iv", 17 | "volume": "volume", 18 | "openInterest": "open_interest", 19 | "delta": "delta", 20 | "theta": "theta", 21 | "vega": "vega", 22 | "gamma": "gamma", 23 | "rho": "rho", 24 | "expirationDateTime": "option_expiration" 25 | }, 26 | "call_symbol_abbreviation": "C", 27 | "put_symbol_abbreviation": "P", 28 | "date_time_format": "%m/%d/%Y", 29 | "data_source_type": "options" 30 | }, 31 | 32 | "iVolatility_futures": { 33 | "number_columns": 29, 34 | "column_names": { 35 | "dateTime": "date", 36 | "underlyingTicker": "underlying", 37 | "exchangeCode": "exchange", 38 | "optionSymbol": "futures_symbol", 39 | "optionType": "call/put", 40 | "strikePrice": "strike", 41 | "underlyingPrice": "futures_close", 42 | "askPrice": "ask", 43 | "bidPrice": "bid", 44 | "settlementPrice": "settlement", 45 | "tradePrice": "", 46 | "impliedVol": "iv", 47 | "volume": "volume", 48 | "openInterest": "open_interest", 49 | "delta": "delta", 50 | "theta": "theta", 51 | "vega": "vega", 52 | "gamma": "gamma", 53 | "rho": "", 54 | "expirationDateTime": "options_expiration_date" 55 | }, 56 | "call_symbol_abbreviation": "C", 57 | "put_symbol_abbreviation": "P", 58 | "date_time_format": "%m/%d/%Y", 59 | "data_source_type": "options" 60 | }} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /dataHandler/unitTestData/dataProvidersFakeWrongNumColumns.json: -------------------------------------------------------------------------------- 1 | {"test_provider": { 2 | "number_columns": 23, 3 | "column_names": { 4 | "dateTime": "date", 5 | "underlyingTicker": "symbol", 6 | "exchangeCode": "exchange", 7 | "optionSymbol": "option_symbol", 8 | "optionType": "call/put", 9 | "strikePrice": "strike", 10 | "underlyingPrice": "stock_price_close", 11 | "askPrice": "ask", 12 | "bidPrice": "bid", 13 | "settlementPrice": "", 14 | "tradePrice": "", 15 | "tradeDataTime": "", 16 | "impliedVol": "iv", 17 | "volume": "volume", 18 | "openInterest": "open_interest", 19 | "delta": "delta", 20 | "theta": "theta", 21 | "vega": "vega", 22 | "gamma": "gamma", 23 | "rho": "rho", 24 | "expirationDateTime": "option_expiration" 25 | }, 26 | "call_symbol_abbreviation": "C", 27 | "put_symbol_abbreviation": "P", 28 | "date_time_format": "%m/%d/%Y", 29 | "data_source_type": "options" 30 | }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/events/__init__.py -------------------------------------------------------------------------------- /events/event.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import enum 3 | 4 | 5 | class EventTypes(enum.Enum): 6 | TICK = 0 7 | SIGNAL = 1 8 | 9 | 10 | class EventHandler(abc.ABC): 11 | """This class is a generic type for handling all events for the backtester.""" 12 | 13 | @abc.abstractmethod 14 | def createEvent(self, data) -> None: 15 | """Create an event which will be used for later processing e.g., create a data tick event for an option chain 16 | returned by the CSV data handler. 17 | 18 | Attributes: 19 | data: input data for the event. e.g., option chain. 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /events/signalEvent.py: -------------------------------------------------------------------------------- 1 | from events import event 2 | from typing import Any, Iterable 3 | 4 | 5 | class SignalEvent(event.EventHandler): 6 | """This class handles the events for signals to (e.g., executing a Strangle strategy).""" 7 | 8 | def __init__(self) -> None: 9 | self.__data = None 10 | self.type = event.EventTypes.SIGNAL 11 | 12 | def getData(self) -> Iterable[Any]: 13 | return self.__data 14 | 15 | def createEvent(self, data: Iterable[Any]) -> None: 16 | """Create a signal event. 17 | 18 | Attributes: 19 | data: input data for the event. 20 | """ 21 | self.__data = data 22 | -------------------------------------------------------------------------------- /events/signalEventTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from events import event 3 | from events import signalEvent 4 | 5 | 6 | class TestSignalEvent(unittest.TestCase): 7 | 8 | def testCreateSignalEvent(self): 9 | """Tests that a signal event is successfully created.""" 10 | signalObj = signalEvent.SignalEvent() 11 | # Check that the data reference attribute is set to None since there has been no data passed. 12 | self.assertEqual(signalObj.getData(), None) 13 | self.assertEqual(signalObj.type, event.EventTypes.SIGNAL) 14 | 15 | 16 | if __name__ == '__main__': 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /events/tickEvent.py: -------------------------------------------------------------------------------- 1 | from events import event 2 | from typing import Any, Iterable 3 | 4 | 5 | class TickEvent(event.EventHandler): 6 | """This class handles the events for new incoming data.""" 7 | 8 | def __init__(self) -> None: 9 | self.__data = None 10 | self.type = event.EventTypes.TICK 11 | 12 | def getData(self) -> Iterable[Any]: 13 | return self.__data 14 | 15 | def createEvent(self, data: Iterable[Any]) -> None: 16 | """Creates a tick event. 17 | 18 | Attributes: 19 | data: input data for the event. e.g., a row of CSV data. 20 | """ 21 | self.__data = data 22 | -------------------------------------------------------------------------------- /events/tickEventTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from events import event 3 | from events import tickEvent 4 | 5 | 6 | class TestTickEvent(unittest.TestCase): 7 | 8 | def testCreateTickEvent(self): 9 | """Tests that a signal event is successfully created.""" 10 | tickObj = tickEvent.TickEvent() 11 | # Check that the data reference attribute is set to None since there has been no data passed. 12 | self.assertEqual(tickObj.getData(), None) 13 | self.assertEqual(tickObj.type, event.EventTypes.TICK) 14 | 15 | 16 | if __name__ == '__main__': 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /marketData/iVolatility/SPX/SPX_2011_2017/RawIV_5day_sample.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/marketData/iVolatility/SPX/SPX_2011_2017/RawIV_5day_sample.zip -------------------------------------------------------------------------------- /optionPrimitives/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/optionPrimitives/__init__.py -------------------------------------------------------------------------------- /optionPrimitives/nakedPut.py: -------------------------------------------------------------------------------- 1 | from base import option 2 | from base import put 3 | from optionPrimitives import optionPrimitive 4 | from typing import Any, Dict, Iterable, Optional, Text 5 | import datetime 6 | import decimal 7 | import logging 8 | 9 | 10 | class NakedPut(optionPrimitive.OptionPrimitive): 11 | """This class sets up the naked put option primitive. 12 | 13 | Attributes: 14 | orderQuantity: number of naked puts. 15 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. (E.g. 100 for 16 | options and 50 for ES futures options). 17 | putToBuyOrSell: put option 18 | buyOrSell: Indicates if we want to the put to be long (buy) or short (sell) the put. 19 | """ 20 | 21 | def __init__(self, orderQuantity: int, contractMultiplier: int, putToBuyOrSell: put.Put, 22 | buyOrSell: optionPrimitive.TransactionType) -> None: 23 | if orderQuantity < 1: 24 | raise ValueError('Order quantity must be a positive (> 0) number.') 25 | self.__numContracts = orderQuantity 26 | self.__contractMultiplier = contractMultiplier 27 | self.__putToBuyOrSell = putToBuyOrSell 28 | self.__buyOrSell = buyOrSell 29 | # The opening and closing fees for the naked put are populated by the strategyManager. 30 | self.__openingFees = None 31 | self.__closingFees = None 32 | 33 | def getDateTime(self) -> Optional[datetime.datetime]: 34 | """Get the current date/time for the naked put.""" 35 | if self.__putToBuyOrSell.dateTime is not None: 36 | return self.__putToBuyOrSell.dateTime 37 | return None 38 | 39 | def getTradeDateTime(self) -> Optional[datetime.datetime]: 40 | """Gets the date/time for when the naked put was created.""" 41 | if self.__putToBuyOrSell.tradeDateTime is not None: 42 | return self.__putToBuyOrSell.tradeDateTime 43 | return None 44 | 45 | def getExpirationDateTime(self) -> Optional[datetime.datetime]: 46 | """Gets the expiration date/time for the naked put.""" 47 | if self.__putToBuyOrSell.expirationDateTime is not None: 48 | return self.__putToBuyOrSell.expirationDateTime 49 | return None 50 | 51 | def getUnderlyingTicker(self) -> Optional[Text]: 52 | """Get the name of the underlying being used for the naked put.""" 53 | if self.__putToBuyOrSell.underlyingTicker is not None: 54 | return self.__putToBuyOrSell.underlyingTicker 55 | return None 56 | 57 | def getUnderlyingPrice(self) -> Optional[decimal.Decimal]: 58 | """Get the price of the underlying being used for the naked put.""" 59 | if self.__putToBuyOrSell.underlyingPrice is not None: 60 | return self.__putToBuyOrSell.underlyingPrice 61 | return None 62 | 63 | def getDelta(self) -> Optional[float]: 64 | """Get total delta (all contracts) for the naked put. 65 | 66 | :return Delta of naked put. 67 | """ 68 | return self.__numContracts * self.__putToBuyOrSell.delta 69 | 70 | def getVega(self) -> Optional[float]: 71 | """Get total vega for the naked put. 72 | 73 | :return Vega of naked put. 74 | """ 75 | return self.__numContracts * self.__putToBuyOrSell.vega 76 | 77 | def getTheta(self) -> Optional[float]: 78 | """Get total theta for the naked put. 79 | 80 | :return Theta of naked put. 81 | """ 82 | return self.__numContracts * self.__putToBuyOrSell.theta 83 | 84 | def getGamma(self) -> Optional[float]: 85 | """Get total gamma for the naked put. 86 | 87 | :return Gamma of naked put. 88 | """ 89 | return self.__numContracts * self.__putToBuyOrSell.gamma 90 | 91 | def getNumContracts(self) -> int: 92 | """Returns the total number of naked puts.""" 93 | return self.__numContracts 94 | 95 | def getContractMultiplier(self) -> int: 96 | """Returns the contract multiplier.""" 97 | return self.__contractMultiplier 98 | 99 | def setNumContracts(self, numContracts: int) -> None: 100 | """Sets the number of contracts for the naked put. 101 | 102 | :param numContracts: Number of naked put contracts we want to put on. 103 | """ 104 | if numContracts < 1: 105 | raise ValueError('Number of contracts must be a positive (> 0) number.') 106 | self.__numContracts = numContracts 107 | 108 | def calcProfitLoss(self) -> decimal.Decimal: 109 | """Calculate the profit and loss for the naked put using option values when the trade was placed and new option 110 | values from tick data. 111 | 112 | :return: Profit / loss (positive decimal for profit, negative decimal for loss). 113 | """ 114 | putProfitLoss = self.__putToBuyOrSell.calcOptionPriceDiff() 115 | if self.__buyOrSell == optionPrimitive.TransactionType.BUY: 116 | putProfitLoss = -putProfitLoss 117 | 118 | # Multiple profit / loss of naked put by the number of contracts and contract multiplier. 119 | totProfitLoss = putProfitLoss * self.__numContracts * self.__contractMultiplier 120 | return totProfitLoss 121 | 122 | def calcProfitLossPercentage(self) -> float: 123 | """Calculate the profit and loss for the naked put as a percentage of the initial trade price. 124 | 125 | :return: Profit / loss as a percentage of the initial trade price. Returns a negative percentage for a loss. 126 | """ 127 | totProfitLoss = self.calcProfitLoss() 128 | 129 | # Calculate the initial credit or debit paid for selling or buying the naked put. 130 | totCreditDebit = self.__putToBuyOrSell.tradePrice * self.__contractMultiplier * self.__numContracts 131 | 132 | # Express totProfitLoss as a percentage. 133 | percentProfitLoss = (totProfitLoss / totCreditDebit) * 100 134 | return percentProfitLoss 135 | 136 | def getBuyingPower(self) -> decimal.Decimal: 137 | """The formula for calculating buying power is based off of Tastyworks. Note that this only applies to equity 138 | options (not futures options). 139 | buying power short put -- greatest of (1, 2) * number of contracts * 100: 140 | (1) 20% of the underlying price minus the out of money amount plus the option premium 141 | (2) 10% of the strike price plus the option premium 142 | buying power long put = premium of the put * number of contracts 143 | 144 | :return: Amount of buying power required to put on the trade. 145 | """ 146 | currentPutPrice = self.__putToBuyOrSell.settlementPrice 147 | if self.__buyOrSell == optionPrimitive.TransactionType.BUY: 148 | buyingPower = currentPutPrice * self.__numContracts * self.__contractMultiplier 149 | if buyingPower <= 0: 150 | logging.warning('Buying power cannot be less <= 0; check option data.') 151 | else: 152 | buyingPower1 = decimal.Decimal(-0.8) * self.__putToBuyOrSell.underlyingPrice + ( 153 | self.__putToBuyOrSell.strikePrice + currentPutPrice) 154 | buyingPower2 = decimal.Decimal(0.1) * self.__putToBuyOrSell.strikePrice + currentPutPrice 155 | maxBuyingPower = max(buyingPower1, buyingPower2) 156 | 157 | buyingPower = maxBuyingPower * self.__numContracts * self.__contractMultiplier 158 | if buyingPower <= 0: 159 | logging.warning('Buying power cannot be <= 0; check option data.') 160 | return buyingPower 161 | 162 | def getCommissionsAndFees(self, openOrClose: Text, pricingSource: Text, pricingSourceConfig: Dict[Any, Any]) -> \ 163 | decimal.Decimal: 164 | """Compute / apply the commissions and fees necessary to put on the trade. 165 | 166 | :param openOrClose: indicates whether we are opening or closing a trade; commissions may be different. 167 | :param pricingSource: indicates which source to read commissions and fee information from. 168 | :param pricingSourceConfig: JSON object for the pricing source. See pricingConfig.json for the file structure. 169 | :return total commission and fees for opening or closing the trade. 170 | """ 171 | if pricingSource == 'tastyworks': 172 | putToBuyOrSellSettlementPrice = self.__putToBuyOrSell.settlementPrice 173 | if openOrClose == 'open': 174 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['open'] 175 | elif openOrClose == 'close': 176 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['close'] 177 | else: 178 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 179 | 180 | secFees = indexOptionConfig['sec_fee_per_contract_wo_trade_price'] * float(putToBuyOrSellSettlementPrice) 181 | return decimal.Decimal(indexOptionConfig['commission_per_contract'] + 182 | indexOptionConfig['clearing_fee_per_contract'] + 183 | indexOptionConfig['orf_fee_per_contract'] + 184 | indexOptionConfig['proprietary_index_fee_per_contract'] + secFees) 185 | elif pricingSource == 'tastyworks_futures': 186 | if openOrClose == 'open': 187 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['open'] 188 | elif openOrClose == 'close': 189 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['close'] 190 | else: 191 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 192 | return decimal.Decimal( 193 | esOptionConfig['commission_per_contract'] + esOptionConfig['clearing_fee_per_contract'] + 194 | esOptionConfig['nfa_fee_per_contract'] + esOptionConfig['exchange_fee_per_contract']) 195 | 196 | def updateValues(self, tickData: Iterable[option.Option]) -> bool: 197 | """Based on the latest price data, update the option values for the naked put. 198 | 199 | :param tickData: option chain with price information (puts, calls) 200 | :return: returns True if we were able to update the options; false otherwise. 201 | """ 202 | putToBuyOrSell = self.__putToBuyOrSell 203 | putStrike = putToBuyOrSell.strikePrice 204 | putExpiration = putToBuyOrSell.expirationDateTime 205 | 206 | # Go through the tickData to find the PUT option with a strike price that matches the putStrike above. 207 | # Note that this should not return more than one option since we specify the strike price, expiration, 208 | # and option type (PUT). 209 | # TODO: we can speed this up by indexing / keying the options by option symbol. 210 | matchingPutToBuyOrSellOption = None 211 | for currentOption in tickData: 212 | if (currentOption.strikePrice == putStrike and currentOption.expirationDateTime == putExpiration and ( 213 | currentOption.optionType == option.OptionTypes.PUT)): 214 | matchingPutToBuyOrSellOption = currentOption 215 | break 216 | 217 | if matchingPutToBuyOrSellOption is None: 218 | logging.warning("No matching PUT was found in the option chain; cannot update naked put.") 219 | return False 220 | 221 | if matchingPutToBuyOrSellOption.settlementPrice is None: 222 | logging.warning("Settlement price was zero for the put to update; won't update. See warning below.") 223 | logging.warning('Bad option info: %s', matchingPutToBuyOrSellOption) 224 | return False 225 | 226 | # Update option intrinsics. 227 | putToBuyOrSell.updateOption(matchingPutToBuyOrSellOption) 228 | return True 229 | 230 | def getNumberOfDaysLeft(self) -> int: 231 | """Determine the number of days between the dateTime and the expirationDateTime. 232 | 233 | :return: number of days between curDateTime and expDateTime. 234 | """ 235 | putToBuyOrSell = self.__putToBuyOrSell 236 | currentDateTime = putToBuyOrSell.dateTime 237 | expirationDateTime = putToBuyOrSell.expirationDateTime 238 | return (expirationDateTime - currentDateTime).days 239 | 240 | def getOpeningFees(self) -> decimal.Decimal: 241 | """Get the saved opening fees for the naked put. 242 | 243 | :return: opening fees. 244 | """ 245 | return self.__openingFees 246 | 247 | def setOpeningFees(self, openingFees: decimal.Decimal) -> None: 248 | """Set the opening fees for the naked put. 249 | 250 | :param openingFees: cost to open naked put. 251 | """ 252 | self.__openingFees = openingFees 253 | 254 | def getClosingFees(self) -> decimal.Decimal: 255 | """Get the saved closing fees for the naked put. 256 | 257 | :return: closing fees. 258 | """ 259 | return self.__closingFees 260 | 261 | def setClosingFees(self, closingFees: decimal.Decimal) -> None: 262 | """Set the closing fees for the naked put. 263 | 264 | :param closingFees: cost to close naked put. 265 | """ 266 | self.__closingFees = closingFees 267 | -------------------------------------------------------------------------------- /optionPrimitives/optionPrimitive.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import decimal 3 | import enum 4 | from base import option 5 | from typing import Iterable 6 | 7 | 8 | class TransactionType(enum.Enum): 9 | BUY = 0 10 | SELL = 1 11 | 12 | 13 | class TradeDirection(enum.Enum): 14 | LONG = 0 15 | SHORT = 1 16 | 17 | 18 | class OptionPrimitive(abc.ABC): 19 | """This class is a generic type for any primitive that can be made using a PUT or CALL option and/or stock, 20 | e.g., iron condor or strangle. 21 | """ 22 | 23 | @abc.abstractmethod 24 | def getBuyingPower(self) -> decimal.Decimal: 25 | """Used to calculate the buying power needed for the option primitive.""" 26 | pass 27 | 28 | @abc.abstractmethod 29 | def getDelta(self) -> float: 30 | """Used to get the total delta for the option primitive.""" 31 | pass 32 | 33 | @abc.abstractmethod 34 | def getVega(self) -> float: 35 | """Used to get the total vega for the option primitive.""" 36 | pass 37 | 38 | @abc.abstractmethod 39 | def getTheta(self) -> float: 40 | """Used to get the total theta for the option primitive.""" 41 | pass 42 | 43 | @abc.abstractmethod 44 | def getGamma(self) -> float: 45 | """Used to get the total gamma for the option primitive.""" 46 | pass 47 | 48 | @abc.abstractmethod 49 | def calcProfitLoss(self) -> decimal.Decimal: 50 | """Calculate the profit and loss for the option primitive based on option values when the trade was placed and 51 | new option values. 52 | 53 | :return: Profit / loss (positive decimal for profit, negative decimal for loss). 54 | """ 55 | pass 56 | 57 | @abc.abstractmethod 58 | def calcProfitLossPercentage(self) -> float: 59 | """Calculate the profit and loss for the option primitive based on option values when the trade was placed and 60 | new option values. 61 | 62 | :return: Profit / loss as a percentage of the initial option prices. Returns negative percentage for a loss. 63 | """ 64 | pass 65 | 66 | @abc.abstractmethod 67 | def updateValues(self, tickData: Iterable[option.Option]) -> bool: 68 | """Based on the latest pricing data, update the option values. 69 | 70 | :param tickData: option chain with pricing information. 71 | :return True if we were able to update values, false otherwise. 72 | """ 73 | pass 74 | 75 | def calcBidAskDiff(self, bidPrice: decimal.Decimal, askPrice: decimal.Decimal) -> decimal.Decimal: 76 | """Calculate the absolute difference between the bid and ask price. 77 | 78 | :param bidPrice: price at which the option can be sold. 79 | :param askPrice: price at which the option can be bought. 80 | :return: Absolute difference; 81 | """ 82 | return abs(bidPrice - askPrice) 83 | -------------------------------------------------------------------------------- /optionPrimitives/putVertical.py: -------------------------------------------------------------------------------- 1 | from base import option 2 | from base import put 3 | from optionPrimitives import optionPrimitive 4 | from typing import Any, Dict, Iterable, Optional, Text 5 | import datetime 6 | import decimal 7 | import logging 8 | 9 | 10 | class PutVertical(optionPrimitive.OptionPrimitive): 11 | """This class sets up the put vertical option primitive. 12 | 13 | Attributes: 14 | orderQuantity: number of put verticals. 15 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. (E.g. 100 for 16 | options and 50 for ES futures options). 17 | putToBuy: put option 18 | putToSell: put option 19 | buyOrSell: Indicates if we want to the vertical to be long (buy) or short (sell). 20 | """ 21 | 22 | def __init__(self, orderQuantity: int, contractMultiplier: int, putToBuy: put.Put, putToSell: put.Put, 23 | buyOrSell: optionPrimitive.TransactionType) -> None: 24 | 25 | if orderQuantity < 1: 26 | raise ValueError('Order quantity must be a positive (> 0) number.') 27 | if contractMultiplier < 1: 28 | raise ValueError('Contract multiplier must be a positive (> 0) number.') 29 | if putToBuy.expirationDateTime != putToSell.expirationDateTime: 30 | raise ValueError('Both put options must have the same expiration.') 31 | self.__numContracts = orderQuantity 32 | self.__contractMultiplier = contractMultiplier 33 | self.__putToBuy = putToBuy 34 | self.__putToSell = putToSell 35 | self.__buyOrSell = buyOrSell 36 | # The opening and closing fees per vertical (one short and one long put) are populated by the strategyManager. 37 | self.__openingFees = None 38 | self.__closingFees = None 39 | 40 | def getDateTime(self) -> Optional[datetime.datetime]: 41 | """Get the current date/time for the options in the vertical.""" 42 | if self.__putToBuy.dateTime is not None: 43 | return self.__putToBuy.dateTime 44 | return None 45 | 46 | def getExpirationDateTime(self) -> Optional[datetime.datetime]: 47 | """Gets the expiration date/time for the vertical.""" 48 | if self.__putToBuy.expirationDateTime is not None: 49 | return self.__putToBuy.expirationDateTime 50 | return None 51 | 52 | def getUnderlyingTicker(self) -> Optional[Text]: 53 | """Get the name of the underlying being used for the vertical.""" 54 | if self.__putToBuy.underlyingTicker is not None: 55 | return self.__putToBuy.underlyingTicker 56 | return None 57 | 58 | def getUnderlyingPrice(self) -> Optional[decimal.Decimal]: 59 | """Get the price of the underlying being used for the vertical.""" 60 | if self.__putToBuy.underlyingPrice is not None: 61 | return self.__putToBuy.underlyingPrice 62 | return None 63 | 64 | def getDelta(self) -> Optional[float]: 65 | """Get the delta for the vertical. 66 | 67 | :return Delta of vertical or None if deltas don't exist for both options. 68 | """ 69 | if self.__putToBuy.delta is not None and self.__putToSell.delta is not None: 70 | return self.__numContracts * (self.__putToBuy.delta + self.__putToSell.delta) 71 | return None 72 | 73 | def getVega(self) -> Optional[float]: 74 | """Get the vega for the vertical. 75 | 76 | :return Vega of vertical or None if vegas don't exist for both options. 77 | """ 78 | if self.__putToBuy.vega is not None and self.__putToSell.vega is not None: 79 | return self.__numContracts * (self.__putToBuy.vega + self.__putToSell.vega) 80 | return None 81 | 82 | def getTheta(self) -> Optional[float]: 83 | """Get the theta for the vertical. 84 | 85 | :return Theta of vertical or None if thetas don't exist for both options. 86 | """ 87 | if self.__putToBuy.theta is not None and self.__putToSell.theta is not None: 88 | return self.__numContracts * (self.__putToBuy.theta + self.__putToSell.theta) 89 | return None 90 | 91 | def getGamma(self) -> Optional[float]: 92 | """Get the gamma for the vertical. 93 | 94 | :return Gamma of vertical or None if gammas don't exist for both options. 95 | """ 96 | if self.__putToBuy.gamma is not None and self.__putToSell.gamma is not None: 97 | return self.__numContracts * (self.__putToBuy.gamma + self.__putToSell.gamma) 98 | return None 99 | 100 | def getNumContracts(self) -> int: 101 | """Returns the total number of put verticals.""" 102 | return self.__numContracts 103 | 104 | def getContractMultiplier(self) -> int: 105 | """Returns the contract multiplier.""" 106 | return self.__contractMultiplier 107 | 108 | def setNumContracts(self, numContracts: int) -> None: 109 | """Sets the number of contracts for the put vertical. 110 | 111 | :param numContracts: Number of put vertical contracts we want to put on. 112 | """ 113 | if numContracts < 1: 114 | raise ValueError('Number of contracts must be a positive (> 0) number.') 115 | self.__numContracts = numContracts 116 | 117 | def calcProfitLoss(self) -> decimal.Decimal: 118 | """Calculate the profit and loss for the vertical position using option values when the trade 119 | was placed and new option values. 120 | 121 | :return: Profit / loss (positive decimal for profit, negative decimal for loss). 122 | """ 123 | putToBuyProfitLoss = -self.__putToBuy.calcOptionPriceDiff() 124 | putToSellProfitLoss = self.__putToSell.calcOptionPriceDiff() 125 | 126 | # Add the profit / loss of put and call, and multiply by the number of contracts. 127 | totProfitLoss = (putToBuyProfitLoss + putToSellProfitLoss) * self.__numContracts * self.__contractMultiplier 128 | return totProfitLoss 129 | 130 | def calcRealizedProfitLoss(self) -> decimal.Decimal: 131 | """This is the same as calcProfitLoss() except that we include the commissions and fees to close.""" 132 | return self.calcProfitLoss() - self.getClosingFees() * self.__numContracts 133 | 134 | def calcProfitLossPercentage(self) -> float: 135 | """Calculate the profit and loss for the vertical position as a percentage of the initial trade price. 136 | 137 | :return: Profit / loss as a percentage of the initial option prices. Returns negative percentage for a loss. 138 | """ 139 | # Add the profit / loss of put and call. 140 | totProfitLoss = self.calcProfitLoss() 141 | 142 | # Get the initial credit or debit paid for selling or buying the vertical, respectively. 143 | putToBuyCreditDebit = -self.__putToBuy.tradePrice 144 | putToSellCreditDebit = self.__putToSell.tradePrice 145 | totCreditDebit = (putToBuyCreditDebit + putToSellCreditDebit) * self.__contractMultiplier * self.__numContracts 146 | 147 | # Express totProfitLoss as a percentage. 148 | percentProfitLoss = (totProfitLoss / totCreditDebit) * 100 149 | return percentProfitLoss 150 | 151 | def getBuyingPower(self) -> decimal.Decimal: 152 | """The formula for calculating buying power is based off of TastyWorks. 153 | buying power short put vertical = distance between strikes * contract multiplier + 154 | (short put option price - long put option price) 155 | buying power long put vertical = difference between cost of two options * contract multiplier 156 | 157 | :return: Amount of buying power required to put on the trade. 158 | """ 159 | currentPutToBuyPrice = self.__putToBuy.settlementPrice 160 | currentPutToSellPrice = self.__putToSell.settlementPrice 161 | if self.__buyOrSell == optionPrimitive.TransactionType.BUY: 162 | buyingPower = (currentPutToBuyPrice - currentPutToSellPrice) * self.__numContracts * ( 163 | self.__contractMultiplier) 164 | if buyingPower <= 0: 165 | logging.warning('Buying power cannot be less <= 0; check strikes in long putVertical') 166 | else: 167 | buyingPower = ((self.__putToSell.strikePrice - self.__putToBuy.strikePrice) - ( 168 | currentPutToSellPrice - currentPutToBuyPrice)) * self.__numContracts * self.__contractMultiplier 169 | if buyingPower <= 0: 170 | logging.warning('Buying power cannot be <= 0; check strikes for short putVertical') 171 | return buyingPower 172 | 173 | def getCommissionsAndFees(self, openOrClose: Text, pricingSource: Text, pricingSourceConfig: Dict[Any, Any]) -> \ 174 | decimal.Decimal: 175 | """Compute / apply the commissions and fees necessary to put on the trade. These are per put vertical contract, 176 | which consists of one short put and one long put. 177 | 178 | :param openOrClose: indicates whether we are opening or closing a trade, where the commissions can be different. 179 | :param pricingSource: indicates which source to read commissions and fee information from. 180 | :param pricingSourceConfig: JSON object for the pricing source. See pricingConfig.json file for the structure. 181 | :return total commission and fees for opening or closing the trade (per put vertical contract; two puts). 182 | """ 183 | if pricingSource == 'tastyworks': 184 | putToBuySettlementPrice = self.__putToBuy.settlementPrice 185 | putToSellSettlementPrice = self.__putToSell.settlementPrice 186 | if openOrClose == 'open': 187 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['open'] 188 | secFees = indexOptionConfig['sec_fee_per_contract_wo_trade_price'] * float(putToSellSettlementPrice) 189 | elif openOrClose == 'close': 190 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['close'] 191 | secFees = indexOptionConfig['sec_fee_per_contract_wo_trade_price'] * float(putToBuySettlementPrice) 192 | else: 193 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 194 | 195 | # SEC fees only apply to one leg, not both, which is which it's not multiplied by '2'. 196 | return decimal.Decimal(2 * (indexOptionConfig['commission_per_contract'] + 197 | indexOptionConfig['clearing_fee_per_contract'] + 198 | indexOptionConfig['orf_fee_per_contract'] + 199 | indexOptionConfig['proprietary_index_fee_per_contract']) + secFees) 200 | elif pricingSource == 'tastyworks_futures': 201 | if openOrClose == 'open': 202 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['open'] 203 | elif openOrClose == 'close': 204 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['close'] 205 | else: 206 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 207 | return decimal.Decimal(2 * (esOptionConfig['commission_per_contract'] + 208 | esOptionConfig['clearing_fee_per_contract'] + 209 | esOptionConfig['nfa_fee_per_contract'] + 210 | esOptionConfig['exchange_fee_per_contract'])) 211 | 212 | def updateValues(self, tickData: Iterable[option.Option]) -> bool: 213 | """Based on the latest pricing data, update the option values for the vertical. 214 | 215 | :param tickData: option chain with pricing information (puts, calls) 216 | :return: returns True if we were able to update the options; false otherwise. 217 | """ 218 | putToSell = self.__putToSell 219 | putStrike = putToSell.strikePrice 220 | putExpiration = putToSell.expirationDateTime 221 | putTicker = putToSell.underlyingTicker 222 | 223 | # Go through the tickData to find the PUT option with a strike price that matches the putStrike above. 224 | # Note that this should not return more than one option since we specify the strike price, expiration, 225 | # and option type (PUT). 226 | # TODO: we can speed this up by indexing / keying the options by option symbol. 227 | matchingPutToSellOption = None 228 | for currentOption in tickData: 229 | if (putTicker in currentOption.underlyingTicker and currentOption.strikePrice == putStrike and ( 230 | currentOption.expirationDateTime == putExpiration) and ( 231 | currentOption.optionType == option.OptionTypes.PUT)): 232 | matchingPutToSellOption = currentOption 233 | break 234 | 235 | if matchingPutToSellOption is None: 236 | logging.warning( 237 | "No matching short PUT was found in the option chain for the vertical; cannot update vertical.") 238 | return False 239 | 240 | if matchingPutToSellOption.settlementPrice is None: 241 | logging.warning("Settlement price was zero for the put to sell option update; won't update. See below") 242 | logging.warning('Bad option info: %s', matchingPutToSellOption) 243 | return False 244 | 245 | putToBuy = self.__putToBuy 246 | putStrike = putToBuy.strikePrice 247 | putExpiration = putToBuy.expirationDateTime 248 | putTicker = putToBuy.underlyingTicker 249 | 250 | # Go through the tickData to find the PUT option with a strike price that matches the putStrike above 251 | # Note that this should not return more than one option since we specify the strike price, expiration, 252 | # and the option type (PUT). 253 | # TODO: we can speed this up by indexing / keying the options by option symbol. 254 | matchingPutToBuyOption = None 255 | for currentOption in tickData: 256 | if (putTicker in currentOption.underlyingTicker and currentOption.strikePrice == putStrike and ( 257 | currentOption.expirationDateTime == putExpiration) and ( 258 | currentOption.optionType == option.OptionTypes.PUT)): 259 | matchingPutToBuyOption = currentOption 260 | break 261 | 262 | if matchingPutToBuyOption is None: 263 | logging.warning( 264 | "No matching long PUT was found in the option chain for the vertical; cannot update vertical.") 265 | return False 266 | 267 | # This handles data errors in the CSV where there was no bid or ask price. 268 | if matchingPutToBuyOption.settlementPrice is None: 269 | logging.warning("Settlement price was zero for the put to buy option update; won't update. See below") 270 | logging.warning('Bad option info: %s', matchingPutToBuyOption) 271 | return False 272 | 273 | putToBuyProfitLoss = -(putToBuy.tradePrice - matchingPutToBuyOption.settlementPrice) 274 | putToSellProfitLoss = putToSell.tradePrice - matchingPutToSellOption.settlementPrice 275 | totProfitLoss = (putToBuyProfitLoss + putToSellProfitLoss) * self.__contractMultiplier 276 | 277 | # Get the initial credit or debit paid for selling or buying the vertical, respectively. 278 | putToBuyCreditDebit = -putToBuy.tradePrice 279 | putToSellCreditDebit = putToSell.tradePrice 280 | totCreditDebit = (putToBuyCreditDebit + putToSellCreditDebit) * self.__contractMultiplier 281 | 282 | # Express totProfitLoss as a percentage. 283 | percentProfitLoss = (totProfitLoss / totCreditDebit) * 100 284 | 285 | # If we are selling the putVertical (short put vertical), the percent profit/loss cannot be greater 286 | # than 100. This can happen if the input data is bad. For example, we have seen that the settlementPrice 287 | # is 0 with zero greeks and other strange values. For the short put vertical, we saw the option price 288 | # go down when the underlying went up, which should never happen. 289 | if (self.__buyOrSell == optionPrimitive.TransactionType.SELL and percentProfitLoss > 100) or ( 290 | self.__buyOrSell == optionPrimitive.TransactionType.BUY and percentProfitLoss < 100): 291 | logging.warning('Percent profit/loss was greater than 100; cannot update vertical.') 292 | logging.warning('Put to buy: %s', putToBuy) 293 | logging.warning('Chosen put to buy: %s', matchingPutToBuyOption) 294 | logging.warning('Put to sell: %s', putToSell) 295 | logging.warning('Chosen put to sell: %s', matchingPutToSellOption) 296 | return False 297 | 298 | # Update option intrinsics. 299 | putToSell.updateOption(matchingPutToSellOption) 300 | putToBuy.updateOption(matchingPutToBuyOption) 301 | return True 302 | 303 | def getNumberOfDaysLeft(self) -> int: 304 | """Determine the number of days between the dateTime and the expirationDateTime. 305 | 306 | :return: number of days between curDateTime and expDateTime. 307 | """ 308 | # Since we require both put options to have the same dateTime and expirationDateTime, we can use either option 309 | # to get the number of days until expiration. 310 | putOpt = self.__putToBuy 311 | currentDateTime = putOpt.dateTime 312 | expirationDateTime = putOpt.expirationDateTime 313 | return (expirationDateTime - currentDateTime).days 314 | 315 | def getOpeningFees(self) -> decimal.Decimal: 316 | """Get the saved opening fees for the put vertical. 317 | 318 | :return: opening fees. 319 | """ 320 | if self.__openingFees is None: 321 | raise ValueError('Opening fees have not been populated in the respective strategyManager class.') 322 | return self.__openingFees 323 | 324 | def setOpeningFees(self, openingFees: decimal.Decimal) -> None: 325 | """Set the opening fees for the put vertical per contract (one short and one long put) 326 | 327 | :param openingFees: cost to open put vertical. 328 | """ 329 | self.__openingFees = openingFees 330 | 331 | def getClosingFees(self) -> decimal.Decimal: 332 | """Get the saved closing fees for the put vertical. 333 | 334 | :return: closing fees. 335 | """ 336 | if self.__closingFees is None: 337 | raise ValueError('Closing fees have not been populated in the respective strategyManager class.') 338 | return self.__closingFees 339 | 340 | def setClosingFees(self, closingFees: decimal.Decimal) -> None: 341 | """Set the closing fees for the put vertical per contract (one short and one long put). 342 | 343 | :param closingFees: cost to close put vertical. 344 | """ 345 | self.__closingFees = closingFees 346 | -------------------------------------------------------------------------------- /optionPrimitives/strangle.py: -------------------------------------------------------------------------------- 1 | from base import call 2 | from base import option 3 | from base import put 4 | from optionPrimitives import optionPrimitive 5 | from typing import Any, Dict, Iterable, Optional, Text 6 | import datetime 7 | import decimal 8 | import logging 9 | 10 | 11 | class Strangle(optionPrimitive.OptionPrimitive): 12 | """This class sets up the strangle option primitive. 13 | 14 | Attributes: 15 | orderQuantity: number of strangles 16 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. (E.g. 100 for 17 | options and 50 for ES futures options). 18 | callOpt: call option 19 | putOpt: put option 20 | buyOrSell: Indicates if we want to buy or sell the strangle. 21 | """ 22 | 23 | def __init__(self, orderQuantity: int, contractMultiplier: int, callOpt: call.Call, putOpt: put.Put, 24 | buyOrSell: optionPrimitive.TransactionType) -> None: 25 | 26 | if orderQuantity < 1: 27 | raise ValueError('Order quantity must be a positive (> 0) number.') 28 | self.__numContracts = orderQuantity 29 | self.__contractMultiplier = contractMultiplier 30 | self.__putOpt = putOpt 31 | self.__callOpt = callOpt 32 | self.__buyOrSell = buyOrSell 33 | # The opening and closing fees per strangle are populated by the strategyManager. 34 | self.__openingFees = None 35 | self.__closingFees = None 36 | 37 | def getDateTime(self) -> Optional[datetime.datetime]: 38 | """Get the current date/time for the options in the strange.""" 39 | if self.__putOpt.dateTime is not None: 40 | return self.__putOpt.dateTime 41 | return None 42 | 43 | def getExpirationDateTime(self) -> Optional[datetime.datetime]: 44 | """Gets the expiration date/time for the strangle.""" 45 | if self.__putOpt.expirationDateTime is not None: 46 | return self.__putOpt.expirationDateTime 47 | return None 48 | 49 | def getUnderlyingTicker(self) -> Optional[Text]: 50 | """Get the name of the underlying being used for the strangle.""" 51 | if self.__putOpt.underlyingTicker is not None: 52 | return self.__putOpt.underlyingTicker 53 | return None 54 | 55 | def getUnderlyingPrice(self) -> Optional[decimal.Decimal]: 56 | """Get the price of the underlying being used for the strangle.""" 57 | if self.__putOpt.underlyingPrice is not None: 58 | return self.__putOpt.underlyingPrice 59 | return None 60 | 61 | def getDelta(self) -> Optional[float]: 62 | """Get the delta for the strangle. 63 | 64 | :return Delta of strangle or None if deltas don't exist for both options. 65 | """ 66 | if self.__putOpt.delta is not None and self.__callOpt.delta is not None: 67 | return self.__numContracts * (self.__putOpt.delta + self.__callOpt.delta) 68 | return None 69 | 70 | def getVega(self) -> Optional[float]: 71 | """Get the vega for the strangle. 72 | 73 | :return Vega of strangle or None if vegas don't exist for both options. 74 | """ 75 | if self.__putOpt.vega is not None and self.__callOpt.vega is not None: 76 | return self.__numContracts * (self.__putOpt.vega + self.__callOpt.vega) 77 | return None 78 | 79 | def getTheta(self) -> Optional[float]: 80 | """Get the theta for the strangle. 81 | 82 | :return Theta of strange or None if thetas don't exist for both options. 83 | """ 84 | if self.__putOpt.theta is not None and self.__callOpt.theta is not None: 85 | return self.__numContracts * (self.__putOpt.theta + self.__callOpt.theta) 86 | return None 87 | 88 | def getGamma(self) -> Optional[float]: 89 | """Get the gamma for the strangle. 90 | 91 | :return Gamma of strange or None if gammas don't exist for both options. 92 | """ 93 | if self.__putOpt.gamma is not None and self.__callOpt.gamma is not None: 94 | return self.__numContracts * (self.__putOpt.gamma + self.__callOpt.gamma) 95 | return None 96 | 97 | def getNumContracts(self) -> int: 98 | """Returns the total number of strangles.""" 99 | return self.__numContracts 100 | 101 | def getContractMultiplier(self) -> int: 102 | """Returns the contract multiplier.""" 103 | return self.__contractMultiplier 104 | 105 | def setNumContracts(self, numContracts: int) -> None: 106 | """Sets the number of contracts for the strangle primitive. 107 | 108 | :param numContracts: Number of strangle contracts we want to put on. 109 | """ 110 | if numContracts < 1: 111 | raise ValueError('Number of contracts must be a positive (> 0) number.') 112 | self.__numContracts = numContracts 113 | 114 | def calcProfitLoss(self) -> decimal.Decimal: 115 | """Calculate the profit and loss for the strangle position using option values when the trade 116 | was placed and new option values. Note that profit and loss are reversed if we buy or sell a put/call; 117 | if we buy a put/call, we want the option value to increase; if we sell a put/call, we want the option value 118 | to decrease. 119 | 120 | :return: Profit / loss (positive decimal for profit, negative decimal for loss). 121 | """ 122 | # Handle profit / loss for put first. 123 | putProfitLoss = self.__putOpt.calcOptionPriceDiff() 124 | callProfitLoss = self.__callOpt.calcOptionPriceDiff() 125 | 126 | # If we're buying the strangle, we have the opposite of the selling case. 127 | if self.__buyOrSell == optionPrimitive.TransactionType.BUY: 128 | putProfitLoss = -putProfitLoss 129 | callProfitLoss = -callProfitLoss 130 | 131 | # Add the profit / loss of put and call, and multiply by the number of contracts. 132 | totProfitLoss = (putProfitLoss + callProfitLoss) * self.__numContracts * self.__contractMultiplier 133 | return totProfitLoss 134 | 135 | def calcRealizedProfitLoss(self) -> decimal.Decimal: 136 | """This is the same as calcProfitLoss() except that we include the commissions and fees to close.""" 137 | return self.calcProfitLoss() - self.__closingFees * self.__numContracts 138 | 139 | def calcProfitLossPercentage(self) -> float: 140 | """Calculate the profit and loss for the strangle position as a percentage of the initial trade price. 141 | 142 | :return: Profit / loss as a percentage of the initial option prices. Returns negative percentage for a loss. 143 | """ 144 | # Add the profit / loss of put and call. 145 | totalProfitLoss = self.calcProfitLoss() 146 | 147 | # Get the initial credit or debit paid for selling or buying the strangle, respectively. 148 | callCreditDebit = self.__callOpt.tradePrice 149 | putCreditDebit = self.__putOpt.tradePrice 150 | totCreditDebit = (callCreditDebit + putCreditDebit) * self.__contractMultiplier * self.__numContracts 151 | 152 | # Express totProfitLoss as a percentage. 153 | percentProfitLoss = (totalProfitLoss / totCreditDebit) * 100 154 | return percentProfitLoss 155 | 156 | def getBuyingPower(self) -> decimal.Decimal: 157 | """The formula for calculating buying power is based off of TastyWorks. This is for cash settled indices! 158 | There are two possible methods to calculate buying power, and the method which generates the maximum possible 159 | buying power is the one chosen. 160 | 161 | :return: Amount of buying power required to put on the trade. 162 | """ 163 | currentCallPrice = self.__callOpt.settlementPrice 164 | currentPutPrice = self.__putOpt.settlementPrice 165 | # Method 1 - 20% rule -- 20% of the underlying, less the difference between the strike price and the stock 166 | # price, plus the option value, multiplied by number of contracts. 167 | 168 | # Use any one of the options to get underlying price (call option used here). 169 | underlyingPrice = self.__callOpt.underlyingPrice 170 | 171 | # Handle call side of strangle. 172 | callBuyingPower1 = ((decimal.Decimal(0.20) * underlyingPrice) - ( 173 | self.__callOpt.strikePrice - underlyingPrice) + currentCallPrice) * self.__numContracts * \ 174 | self.__contractMultiplier 175 | # Handle put side of strangle. 176 | putBuyingPower1 = ((decimal.Decimal(0.20) * underlyingPrice) - ( 177 | underlyingPrice - self.__putOpt.strikePrice) + currentPutPrice) * self.__numContracts * \ 178 | self.__contractMultiplier 179 | methodOneBuyingPower = max(callBuyingPower1, putBuyingPower1) 180 | 181 | # Method 2 - 10% rule -- 10% of the exercise value plus premium value. 182 | # Handle call side of strangle. 183 | callBuyingPower2 = (decimal.Decimal(0.10) * self.__callOpt.strikePrice + currentCallPrice) * ( 184 | self.__numContracts * self.__contractMultiplier) 185 | # Handle put side of strangle. 186 | putBuyingPower2 = (decimal.Decimal(0.10) * self.__putOpt.strikePrice + currentPutPrice) * ( 187 | self.__numContracts * self.__contractMultiplier) 188 | methodTwoBuyingPower = max(callBuyingPower2, putBuyingPower2) 189 | 190 | return max(methodOneBuyingPower, methodTwoBuyingPower) 191 | 192 | def getCommissionsAndFees(self, openOrClose: Text, pricingSource: Text, pricingSourceConfig: Dict[Any, Any]) -> \ 193 | decimal.Decimal: 194 | """Compute / apply the commissions and fees necessary to put on the trade. 195 | 196 | :param openOrClose: indicates whether we are opening or closing a trade, where the commissions can be different. 197 | :param pricingSource: indicates which source to read commissions and fee information from. 198 | :param pricingSourceConfig: JSON object for the pricing source. See pricingConfig.json file for the structure. 199 | :return total commission and fees for opening or closing the trade. 200 | """ 201 | if pricingSource == 'tastyworks': 202 | if openOrClose == 'open': 203 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['open'] 204 | # Multiply by 2 to handle put and call. 205 | return decimal.Decimal(2 * (indexOptionConfig['commission_per_contract'] + 206 | indexOptionConfig['clearing_fee_per_contract'] + 207 | indexOptionConfig['orf_fee_per_contract'])) 208 | elif openOrClose == 'close': 209 | indexOptionConfig = pricingSourceConfig['stock_options']['index_option']['close'] 210 | putFees = decimal.Decimal(indexOptionConfig['commission_per_contract'] + 211 | indexOptionConfig['clearing_fee_per_contract'] + 212 | indexOptionConfig['orf_fee_per_contract'] + 213 | indexOptionConfig['finra_taf_per_contract']) + \ 214 | decimal.Decimal( 215 | indexOptionConfig['sec_fee_per_contract_wo_trade_price']) * \ 216 | self.__putOpt.settlementPrice 217 | callFees = decimal.Decimal(indexOptionConfig['commission_per_contract'] + 218 | indexOptionConfig['clearing_fee_per_contract'] + 219 | indexOptionConfig['orf_fee_per_contract'] + 220 | indexOptionConfig['finra_taf_per_contract']) + \ 221 | decimal.Decimal( 222 | indexOptionConfig['sec_fee_per_contract_wo_trade_price']) * \ 223 | self.__callOpt.settlementPrice 224 | return decimal.Decimal(putFees + callFees) 225 | else: 226 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 227 | elif pricingSource == 'tastyworks_futures': 228 | if openOrClose == 'open': 229 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['open'] 230 | elif openOrClose == 'close': 231 | esOptionConfig = pricingSourceConfig['futures_options']['es_option']['close'] 232 | else: 233 | raise TypeError('Only open or close types can be provided to getCommissionsAndFees().') 234 | return decimal.Decimal(2 * (esOptionConfig['commission_per_contract'] + 235 | esOptionConfig['clearing_fee_per_contract'] + 236 | esOptionConfig['nfa_fee_per_contract'] + 237 | esOptionConfig['exchange_fee_per_contract'])) 238 | 239 | def updateValues(self, tickData: Iterable[option.Option]) -> bool: 240 | """Based on the latest pricing data, update the option values for the strangle. 241 | 242 | :param tickData: option chain with pricing information (puts, calls) 243 | :return: returns True if we were able to update the options; false otherwise. 244 | """ 245 | # Work with put option first. 246 | putOpt = self.__putOpt 247 | putStrike = putOpt.strikePrice 248 | putExpiration = putOpt.expirationDateTime 249 | 250 | # Go through the tickData to find the PUT option with a strike price that matches the putStrike above. 251 | # Note that this should not return more than one option since we specify the strike price, expiration, 252 | # option type (PUT), and option symbol. 253 | # TODO: we can speed this up by indexing / keying the options by option symbol. 254 | matchingPutOption = None 255 | for currentOption in tickData: 256 | if (currentOption.strikePrice == putStrike and currentOption.expirationDateTime == putExpiration and ( 257 | currentOption.optionType == option.OptionTypes.PUT)): 258 | matchingPutOption = currentOption 259 | break 260 | 261 | if matchingPutOption is None: 262 | logging.warning("No matching PUT was found in the option chain for the strangle; cannot update strangle.") 263 | return False 264 | 265 | if matchingPutOption.settlementPrice is None: 266 | logging.warning("Settlement price was zero for the PUT option; won't update strangle. See below") 267 | logging.warning('Bad option info: %s', matchingPutOption) 268 | return False 269 | 270 | # Work with call option. 271 | callOpt = self.__callOpt 272 | callStrike = callOpt.strikePrice 273 | callExpiration = callOpt.expirationDateTime 274 | 275 | # Go through the tickData to find the CALL option with a strike price that matches the callStrike above 276 | # Note that this should not return more than one option since we specify the strike price, expiration, 277 | # the option type (CALL), and option symbol. 278 | # TODO: we can speed this up by indexing / keying the options by option symbol. 279 | matchingCallOption = None 280 | for currentOption in tickData: 281 | if (currentOption.strikePrice == callStrike and currentOption.expirationDateTime == callExpiration and ( 282 | currentOption.optionType == option.OptionTypes.CALL)): 283 | matchingCallOption = currentOption 284 | break 285 | 286 | if matchingCallOption is None: 287 | logging.warning("No matching CALL was found in the option chain for the strangle; cannot update strangle.") 288 | return False 289 | 290 | if matchingCallOption.settlementPrice is None: 291 | logging.warning("Settlement price was zero for the CALL option; won't update strangle. See below") 292 | logging.warning('Bad option info: %s', matchingCallOption) 293 | return False 294 | 295 | # Update option intrinsics 296 | putOpt.updateOption(matchingPutOption) 297 | callOpt.updateOption(matchingCallOption) 298 | return True 299 | 300 | def getNumberOfDaysLeft(self) -> int: 301 | """Determine the number of days between the dateTime and the expirationDateTime. 302 | 303 | :return: number of days between curDateTime and expDateTime. 304 | """ 305 | # Since we require the put and call options to have the same dateTime and expirationDateTime, we can use either 306 | # option to get the number of days until expiration. 307 | putOpt = self.__putOpt 308 | currentDateTime = putOpt.dateTime 309 | expirationDateTime = putOpt.expirationDateTime 310 | return (expirationDateTime - currentDateTime).days 311 | 312 | def getOpeningFees(self) -> decimal.Decimal: 313 | """Get the saved opening fees for the strangle. 314 | 315 | :return: opening fees. 316 | """ 317 | if self.__openingFees is None: 318 | raise ValueError('Opening fees have not been populated in the respective strategyManager class.') 319 | return self.__openingFees 320 | 321 | def setOpeningFees(self, openingFees: decimal.Decimal) -> None: 322 | """Set the opening fees for the strange. 323 | 324 | :param openingFees: cost to open strangle. 325 | """ 326 | self.__openingFees = openingFees 327 | 328 | def getClosingFees(self) -> decimal.Decimal: 329 | """Get the saved closing fees for the strangle. 330 | 331 | :return: closing fees. 332 | """ 333 | if self.__closingFees is None: 334 | raise ValueError('Closing fees have not been populated in the respective strategyManager class.') 335 | return self.__closingFees 336 | 337 | def setClosingFees(self, closingFees: decimal.Decimal) -> None: 338 | """Set the closing fees for the strangle. 339 | 340 | :param closingFees: cost to close strangle. 341 | """ 342 | self.__closingFees = closingFees 343 | -------------------------------------------------------------------------------- /portfolioManager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/portfolioManager/__init__.py -------------------------------------------------------------------------------- /portfolioManager/portfolio.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import decimal 3 | import logging 4 | import typing 5 | from events import signalEvent, tickEvent 6 | from optionPrimitives import optionPrimitive 7 | 8 | 9 | @dataclasses.dataclass() 10 | class Portfolio(object): 11 | """This class creates a portfolio to hold all open positions. 12 | At the moment, the portfolio runs live, but in the future we should migrate the portfolio to be stored in a 13 | database. 14 | 15 | Attributes: 16 | startingCapital -- How much capital we have when starting. 17 | maxCapitalToUse -- Max percent of portfolio to use (decimal between 0 and 1). 18 | maxCapitalToUsePerTrade -- Max percent of portfolio to use on one trade (same underlying), 0 to 1. 19 | positionMonitoring -- Used to keep track of portfolio values over time. 20 | 21 | 22 | Portfolio intrinsics: 23 | realizedCapital: Updated when positions are actually closed. 24 | netLiquidity: Net liquidity of total portfolio (ideally includes commissions, fees, etc.). 25 | totalBuyingPower: Total buying power being used in portfolio. 26 | totalDelta: Sum of deltas for all positions (positive or negative). 27 | totalVega: Sum of vegas for all positions (positive or negative). 28 | totalTheta: Sum of thetas for all positions (positive or negative). 29 | totalGamma: Sum of gammas for all positions (positive or negative). 30 | """ 31 | 32 | startingCapital: decimal.Decimal 33 | maxCapitalToUse: decimal.Decimal 34 | maxCapitalToUsePerTrade: decimal.Decimal 35 | positionMonitoring: typing.Optional[typing.DefaultDict[typing.Text, list]] = None 36 | realizedCapital: typing.ClassVar[decimal.Decimal] 37 | netLiquidity: typing.ClassVar[decimal.Decimal] 38 | totalBuyingPower: typing.ClassVar[decimal.Decimal] = decimal.Decimal(0.0) 39 | totalDelta: typing.ClassVar[float] = 0.0 40 | totalVega: typing.ClassVar[float] = 0.0 41 | totalTheta: typing.ClassVar[float] = 0.0 42 | totalGamma: typing.ClassVar[float] = 0.0 43 | totalNumberContracts: typing.ClassVar[int] = 0 44 | activePositions: typing.ClassVar[list] = [] 45 | 46 | def __post_init__(self): 47 | self.realizedCapital = self.startingCapital 48 | self.netLiquidity = self.startingCapital 49 | self.activePositions = [] 50 | 51 | def onSignal(self, event: signalEvent) -> None: 52 | """Handle a new signal event; indicates that a new position should be added to the portfolio if portfolio risk 53 | management conditions are satisfied. 54 | 55 | :param event: Event to be handled by portfolio; a signal event in this case. 56 | """ 57 | # Get the data from the tick event 58 | eventData = event.getData() 59 | 60 | # Return if there's no data 61 | if not eventData: 62 | return 63 | 64 | positionData = eventData[0] 65 | 66 | # Determine the commissions that the trade requires. 67 | openCommissionFeeCapitalRequirement = positionData.getOpeningFees() * positionData.getNumContracts() 68 | 69 | self.activePositions.append(eventData) 70 | # Reduce the realized capital by the commissions and fees. 71 | self.realizedCapital -= openCommissionFeeCapitalRequirement 72 | 73 | def updatePortfolio(self, event: tickEvent) -> None: 74 | """ Updates the intrinsics of the portfolio by updating the values of the options used in the different 75 | optionPrimitives. 76 | 77 | :param event: Tick event with the option chain which will be used to update the portfolio. 78 | """ 79 | # Get the data from the tick event. 80 | tickData = event.getData() 81 | 82 | # If we did not get any tick data or there are no positions in the portfolio, return. 83 | if not tickData or not self.activePositions: 84 | return 85 | 86 | # Go through the positions currently in the portfolio and update the prices. 87 | # We first reset the entire portfolio and recalculate the values. 88 | self.totalDelta = 0 89 | self.totalGamma = 0 90 | self.totalVega = 0 91 | self.totalTheta = 0 92 | self.totalBuyingPower = decimal.Decimal(0) 93 | self.netLiquidity = decimal.Decimal(0) 94 | self.totalNumberContracts = 0 95 | 96 | # Array / list used to keep track of which positions we should remove. 97 | idxsToDelete = [] 98 | 99 | # Go through all positions in portfolio and update the values. 100 | currentDateTime = None 101 | underlyingPrice = None 102 | for idx, curPosition in enumerate(self.activePositions): 103 | positionData = curPosition[0] 104 | riskManagementStrategy = curPosition[1] 105 | currentDateTime = positionData.getDateTime() 106 | underlyingPrice = positionData.getUnderlyingPrice() 107 | 108 | if not positionData.updateValues(tickData): 109 | self.realizedCapital += positionData.calcRealizedProfitLoss() 110 | 111 | # Add position to array to be removed. 112 | idxsToDelete.append(idx) 113 | logging.warning('Could not update option values; removing position.') 114 | continue 115 | 116 | if riskManagementStrategy.managePosition(positionData): 117 | self.realizedCapital += positionData.calcRealizedProfitLoss() 118 | 119 | # Add position to array to be removed. 120 | idxsToDelete.append(idx) 121 | else: 122 | self.netLiquidity += positionData.calcProfitLoss() 123 | # Update greeks and total buying power. 124 | self.__calcPortfolioValues(positionData) 125 | self.totalNumberContracts += positionData.getNumContracts() 126 | 127 | # Add the realized capital to the profit / loss of all open positions to get final net liq. 128 | self.netLiquidity += self.realizedCapital 129 | 130 | # Go through and delete any positions which were added to the idxsToDelete array. 131 | for idx in reversed(idxsToDelete): 132 | logging.info('The %s position was closed.', self.activePositions[idx][0].getUnderlyingTicker()) 133 | del (self.activePositions[idx]) 134 | 135 | if self.positionMonitoring is not None: 136 | # Update the position monitoring dictionary. 137 | self.positionMonitoring['Date'].append(currentDateTime) 138 | self.positionMonitoring['UnderlyingPrice'].append(underlyingPrice) 139 | self.positionMonitoring['NetLiq'].append(self.netLiquidity) 140 | self.positionMonitoring['RealizedCapital'].append(self.realizedCapital) 141 | self.positionMonitoring['NumPositions'].append(len(self.activePositions)) 142 | self.positionMonitoring['TotNumContracts'].append(self.totalNumberContracts) 143 | self.positionMonitoring['BuyingPower'].append(self.totalBuyingPower) 144 | self.positionMonitoring['TotalDelta'].append(self.totalDelta) 145 | 146 | logging.info( 147 | 'Date: {} UnderlyingPrice: {} NetLiq: {} RealizedCapital: {} NumPositions: {} TotNumContracts: {}' 148 | ' BuyingPower: {} TotalDelta: {}'.format(currentDateTime, underlyingPrice, self.netLiquidity, 149 | self.realizedCapital, len(self.activePositions), 150 | self.totalNumberContracts, self.totalBuyingPower, self.totalDelta)) 151 | 152 | def __calcPortfolioValues(self, curPosition: optionPrimitive.OptionPrimitive) -> None: 153 | """Updates portfolio values for current position. 154 | 155 | :param curPosition: Current position in portfolio being processed. 156 | """ 157 | self.totalDelta += curPosition.getDelta() 158 | self.totalGamma += curPosition.getGamma() 159 | self.totalTheta += curPosition.getTheta() 160 | self.totalVega += curPosition.getVega() 161 | self.totalBuyingPower += curPosition.getBuyingPower() 162 | -------------------------------------------------------------------------------- /portfolioManager/portfolioTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime 3 | import decimal 4 | import json 5 | from portfolioManager import portfolio 6 | from optionPrimitives import optionPrimitive, strangle 7 | from base import put 8 | from base import call 9 | from events import signalEvent, tickEvent 10 | from riskManager import strangleRiskManagement 11 | 12 | 13 | class TestPortfolio(unittest.TestCase): 14 | 15 | def setUp(self): 16 | """Create portfolio object to be shared among tests.""" 17 | 18 | # Strangle object to be shared among tests. 19 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 20 | strikePrice=decimal.Decimal(2690), delta=-0.16, gamma=0.01, theta=0.02, vega=0.03, 21 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 22 | expirationDateTime=datetime.datetime.strptime('01/20/2021', "%m/%d/%Y"), 23 | bidPrice=decimal.Decimal(7.45), askPrice=decimal.Decimal(7.50), 24 | tradePrice=decimal.Decimal(7.475), 25 | settlementPrice=decimal.Decimal(7.475)) 26 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 27 | strikePrice=decimal.Decimal(2855), delta=0.16, gamma=0.01, theta=0.02, vega=0.03, 28 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 29 | expirationDateTime=datetime.datetime.strptime('01/20/2021', 30 | "%m/%d/%Y"), 31 | bidPrice=decimal.Decimal(5.20), askPrice=decimal.Decimal(5.40), 32 | tradePrice=decimal.Decimal(5.30), settlementPrice=decimal.Decimal(5.30)) 33 | self.__strangleObj = strangle.Strangle(orderQuantity=1, contractMultiplier=100, callOpt=callOpt, putOpt=putOpt, 34 | buyOrSell=optionPrimitive.TransactionType.SELL) 35 | self.pricingSource = 'tastyworks' 36 | with open('./dataHandler/pricingConfig.json') as config: 37 | fullConfig = json.load(config) 38 | self.pricingSourceConfig = fullConfig[self.pricingSource] 39 | self.__strangleObj.setOpeningFees(self.__strangleObj.getCommissionsAndFees('open', self.pricingSource, 40 | self.pricingSourceConfig)) 41 | self.__strangleObj.setClosingFees(self.__strangleObj.getCommissionsAndFees('close', self.pricingSource, 42 | self.pricingSourceConfig)) 43 | self.riskManagement = strangleRiskManagement.StrangleRiskManagement( 44 | strangleRiskManagement.StrangleManagementStrategyTypes.HOLD_TO_EXPIRATION) 45 | 46 | startingCapital = decimal.Decimal(1000000) 47 | maxCapitalToUse = decimal.Decimal(0.5) 48 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 49 | self.__portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 50 | 51 | def testOnSignalSuccess(self): 52 | """Tests that onSignal event successfully updates portfolio and updates realized capital.""" 53 | # Create signal event. 54 | event = signalEvent.SignalEvent() 55 | event.createEvent([self.__strangleObj, self.riskManagement]) 56 | 57 | # Test portfolio onSignal event. 58 | self.__portfolioObj.onSignal(event) 59 | 60 | # Check that positions array in portfolio is not empty. 61 | self.assertEqual(len(self.__portfolioObj.activePositions), 1) 62 | self.assertAlmostEqual(self.__portfolioObj.realizedCapital, self.__portfolioObj.startingCapital - ( 63 | self.__strangleObj.getOpeningFees() * self.__strangleObj.getNumContracts())) 64 | 65 | def testOnSignalReturnsEmpty(self): 66 | """Tests that activePositions has length 0 if eventData is empty.""" 67 | event = signalEvent.SignalEvent() 68 | self.__portfolioObj.onSignal(event) 69 | self.assertEqual(len(self.__portfolioObj.activePositions), 0) 70 | 71 | def testUpdatePortfolioSuccess(self): 72 | """Tests the ability to update option values for a position in the portfolio.""" 73 | # Create strangle event. 74 | event = signalEvent.SignalEvent() 75 | event.createEvent([self.__strangleObj, self.riskManagement]) 76 | 77 | # Create portfolio onSignal event, which adds the position to the portfolio. 78 | startingCapital = decimal.Decimal(1000000) 79 | maxCapitalToUse = decimal.Decimal(0.5) 80 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 81 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 82 | portfolioObj.onSignal(event) 83 | 84 | # Next, create a strangle with the next days prices and update the portfolio values. 85 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 86 | strikePrice=decimal.Decimal(2690), delta=-0.16, gamma=0.01, theta=0.02, vega=0.03, 87 | dateTime=datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 88 | expirationDateTime=datetime.datetime.strptime('01/20/2021', "%m/%d/%Y"), 89 | bidPrice=decimal.Decimal(6.45), askPrice=decimal.Decimal(6.50), 90 | tradePrice=decimal.Decimal(6.475), 91 | settlementPrice=decimal.Decimal(6.475)) 92 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 93 | strikePrice=decimal.Decimal(2855), delta=0.16, gamma=0.01, theta=0.02, vega=0.03, 94 | dateTime=datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 95 | expirationDateTime=datetime.datetime.strptime('01/20/2021', 96 | "%m/%d/%Y"), 97 | bidPrice=decimal.Decimal(4.20), askPrice=decimal.Decimal(4.40), 98 | tradePrice=decimal.Decimal(4.30), settlementPrice=decimal.Decimal(4.30)) 99 | 100 | # Create tick event and update portfolio values. 101 | testOptionChain = [callOpt, putOpt] 102 | event = tickEvent.TickEvent() 103 | event.createEvent(testOptionChain) 104 | portfolioObj.updatePortfolio(event) 105 | 106 | # Check that the new portfolio values are correct (e.g., buying power, total delta, total gamma, etc.). 107 | self.assertAlmostEqual(portfolioObj.totalBuyingPower, decimal.Decimal(49278.8)) 108 | self.assertAlmostEqual(portfolioObj.totalVega, 0.06) 109 | self.assertAlmostEqual(portfolioObj.totalDelta, 0.0) 110 | self.assertAlmostEqual(portfolioObj.totalGamma, 0.02) 111 | self.assertAlmostEqual(portfolioObj.totalTheta, 0.04) 112 | self.assertAlmostEqual(portfolioObj.netLiquidity, decimal.Decimal(1000197.7416999999999)) 113 | 114 | def testUpdatePortfolioNoTickData(self): 115 | """Tests that portfolio is not updated if no tick data is passed.""" 116 | # Create strangle event. 117 | event = signalEvent.SignalEvent() 118 | event.createEvent([self.__strangleObj, self.riskManagement]) 119 | 120 | # Create portfolio onSignal event, which adds the position to the portfolio. 121 | startingCapital = decimal.Decimal(1000000) 122 | maxCapitalToUse = decimal.Decimal(0.5) 123 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 124 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 125 | portfolioObj.onSignal(event) 126 | 127 | # No tick data passed in. 128 | event = tickEvent.TickEvent() 129 | portfolioObj.updatePortfolio(event) 130 | 131 | # The number of positions in the portfolio and the realized capital should not change. 132 | self.assertEqual(len(portfolioObj.activePositions), 1) 133 | self.assertAlmostEqual(portfolioObj.realizedCapital, portfolioObj.startingCapital - ( 134 | self.__strangleObj.getOpeningFees() * self.__strangleObj.getNumContracts())) 135 | 136 | def testUpdatePortfolioNoPositions(self): 137 | """Tests that the portfolio remains empty if there are no active positions.""" 138 | # Create portfolio onSignal event, which adds the position to the portfolio. 139 | startingCapital = decimal.Decimal(1000000) 140 | maxCapitalToUse = decimal.Decimal(0.5) 141 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 142 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 143 | 144 | # No tick data passed in. 145 | event = tickEvent.TickEvent() 146 | portfolioObj.updatePortfolio(event) 147 | 148 | self.assertEqual(len(portfolioObj.activePositions), 0) 149 | 150 | def testUpdatePortfolioNoMatchingOption(self): 151 | """Tests that a position is removed from the portfolio if there are not matching options.""" 152 | # Create strangle event. 153 | event = signalEvent.SignalEvent() 154 | event.createEvent([self.__strangleObj, self.riskManagement]) 155 | 156 | # Create portfolio onSignal event, which adds the position to the portfolio. 157 | startingCapital = decimal.Decimal(1000000) 158 | maxCapitalToUse = decimal.Decimal(0.5) 159 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 160 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 161 | portfolioObj.onSignal(event) 162 | 163 | # Let's change the strike price (2690->2790) of the putOpt below so that there will be no matching option, 164 | # and the portfolio cannot be updated. 165 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 166 | strikePrice=decimal.Decimal(2790), delta=-0.16, gamma=0.01, theta=0.02, vega=0.03, 167 | dateTime=datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 168 | expirationDateTime=datetime.datetime.strptime('01/20/2021', "%m/%d/%Y"), 169 | bidPrice=decimal.Decimal(6.45), askPrice=decimal.Decimal(6.50), 170 | tradePrice=decimal.Decimal(6.475), 171 | settlementPrice=decimal.Decimal(6.475)) 172 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 173 | strikePrice=decimal.Decimal(2855), delta=0.16, gamma=0.01, theta=0.02, vega=0.03, 174 | dateTime=datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 175 | expirationDateTime=datetime.datetime.strptime('01/20/2021', 176 | "%m/%d/%Y"), 177 | bidPrice=decimal.Decimal(4.20), askPrice=decimal.Decimal(4.40), 178 | tradePrice=decimal.Decimal(4.30), settlementPrice=decimal.Decimal(4.30)) 179 | 180 | # Create tick event and update portfolio values. 181 | testOptionChain = [callOpt, putOpt] 182 | event = tickEvent.TickEvent() 183 | event.createEvent(testOptionChain) 184 | portfolioObj.updatePortfolio(event) 185 | 186 | # Only position in portfolio should be removed since we can't update it. 187 | self.assertEqual(len(portfolioObj.activePositions), 0) 188 | self.assertAlmostEqual(portfolioObj.realizedCapital, 189 | startingCapital - ( 190 | self.__strangleObj.getOpeningFees() + self.__strangleObj.getClosingFees() 191 | )*self.__strangleObj.getNumContracts()) 192 | 193 | def testUpdatePortfolioRiskManagementHoldToExpiration(self): 194 | """Tests that the position is removed from the portfolio when expiration occurs.""" 195 | startingCapital = decimal.Decimal(1000000) 196 | maxCapitalToUse = decimal.Decimal(0.5) 197 | maxCapitalToUsePerTrade = decimal.Decimal(0.25) 198 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 199 | 200 | # Add first position to the portfolio 201 | event = signalEvent.SignalEvent() 202 | event.createEvent([self.__strangleObj, self.riskManagement]) 203 | portfolioObj.onSignal(event) 204 | self.assertEqual(len(portfolioObj.activePositions), 1) 205 | 206 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2800.00), 207 | strikePrice=decimal.Decimal(2700), delta=-0.16, gamma=0.01, theta=0.02, vega=0.03, 208 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 209 | expirationDateTime=datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 210 | bidPrice=decimal.Decimal(8.00), askPrice=decimal.Decimal(8.50), 211 | tradePrice=decimal.Decimal(8.25), 212 | settlementPrice=decimal.Decimal(8.25)) 213 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2800.00), 214 | strikePrice=decimal.Decimal(3000), delta=0.16, gamma=0.01, theta=0.02, vega=0.03, 215 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 216 | expirationDateTime=datetime.datetime.strptime('01/02/2021', 217 | "%m/%d/%Y"), 218 | bidPrice=decimal.Decimal(6.00), askPrice=decimal.Decimal(6.50), 219 | tradePrice=decimal.Decimal(6.25), settlementPrice=decimal.Decimal(6.25)) 220 | strangleObj = strangle.Strangle(orderQuantity=1, contractMultiplier=100, callOpt=callOpt, putOpt=putOpt, 221 | buyOrSell=optionPrimitive.TransactionType.SELL) 222 | strangleObj.setOpeningFees( 223 | strangleObj.getCommissionsAndFees('open', self.pricingSource, self.pricingSourceConfig)) 224 | strangleObj.setClosingFees( 225 | strangleObj.getCommissionsAndFees('close', self.pricingSource, self.pricingSourceConfig)) 226 | 227 | # Add second position to the portfolio. 228 | event = signalEvent.SignalEvent() 229 | event.createEvent([strangleObj, self.riskManagement]) 230 | portfolioObj.onSignal(event) 231 | self.assertEqual(len(portfolioObj.activePositions), 2) 232 | 233 | # Update the portfolio, which should remove the second event. We do not change the prices of putOpt or callOpt. 234 | testOptionChain = [callOpt, putOpt] 235 | event = tickEvent.TickEvent() 236 | event.createEvent(testOptionChain) 237 | portfolioObj.updatePortfolio(event) 238 | # There should be no positions in the portfolio since the first position was removed given that there 239 | # was no tick data to update it, and the second position was removed since expiration occurred. 240 | self.assertEqual(len(portfolioObj.activePositions), 0) 241 | 242 | def testOnMultipleSignalSuccess(self): 243 | """Tests that the portfolio values are correct after multiple trades have been put on.""" 244 | event = signalEvent.SignalEvent() 245 | event.createEvent([self.__strangleObj, self.riskManagement]) 246 | 247 | # Create portfolio onSignal event, which adds the position to the portfolio. 248 | startingCapital = decimal.Decimal(1000000) 249 | maxCapitalToUse = decimal.Decimal(0.5) 250 | maxCapitalToUsePerTrade = decimal.Decimal(0.5) 251 | portfolioObj = portfolio.Portfolio(startingCapital, maxCapitalToUse, maxCapitalToUsePerTrade) 252 | portfolioObj.onSignal(event) 253 | 254 | # Add second signal / trade. 255 | event = signalEvent.SignalEvent() 256 | event.createEvent([self.__strangleObj, self.riskManagement]) 257 | portfolioObj.onSignal(event) 258 | 259 | # Updates the portfolio to get the buying power, delta values, etc. 260 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2800.00), 261 | strikePrice=decimal.Decimal(2690), delta=-0.16, gamma=0.01, theta=0.02, vega=0.03, 262 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 263 | expirationDateTime=datetime.datetime.strptime('01/20/2021', "%m/%d/%Y"), 264 | bidPrice=decimal.Decimal(8.00), askPrice=decimal.Decimal(8.50), 265 | tradePrice=decimal.Decimal(8.25), settlementPrice=decimal.Decimal(8.25)) 266 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2800.00), 267 | strikePrice=decimal.Decimal(2855), delta=0.16, gamma=0.01, theta=0.02, vega=0.03, 268 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 269 | expirationDateTime=datetime.datetime.strptime('01/20/2021', 270 | "%m/%d/%Y"), 271 | bidPrice=decimal.Decimal(6.00), askPrice=decimal.Decimal(6.50), 272 | tradePrice=decimal.Decimal(6.25), settlementPrice=decimal.Decimal(6.25)) 273 | testOptionChain = [callOpt, putOpt] 274 | event = tickEvent.TickEvent() 275 | event.createEvent(testOptionChain) 276 | portfolioObj.updatePortfolio(event) 277 | 278 | self.assertAlmostEqual(portfolioObj.totalBuyingPower, decimal.Decimal(102250.00)) 279 | self.assertAlmostEqual(portfolioObj.totalVega, 0.12) 280 | self.assertAlmostEqual(portfolioObj.totalDelta, 0.0) 281 | self.assertAlmostEqual(portfolioObj.totalGamma, 0.04) 282 | self.assertAlmostEqual(portfolioObj.totalTheta, 0.08) 283 | 284 | 285 | if __name__ == '__main__': 286 | unittest.main() 287 | -------------------------------------------------------------------------------- /riskManager/putVerticalRiskManagement.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import enum 3 | import logging 4 | from riskManager import riskManagement 5 | from optionPrimitives import optionPrimitive 6 | from typing import Optional 7 | 8 | 9 | class PutVerticalManagementStrategyTypes(enum.Enum): 10 | HOLD_TO_EXPIRATION = 0 11 | CLOSE_AT_50_PERCENT = 1 12 | CLOSE_AT_50_PERCENT_OR_21_DAYS = 2 13 | CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS = 3 14 | CLOSE_AT_21_DAYS = 4 15 | 16 | 17 | class PutVerticalRiskManagement(riskManagement.RiskManagement): 18 | """This class handles risk management strategies for put verticals.""" 19 | 20 | def __init__(self, managementType: PutVerticalManagementStrategyTypes, closeDuration: Optional[int]) -> None: 21 | """This class handles the risk management for the put vertical. 22 | 23 | Attributes: 24 | managementType: predetermined management strategies. 25 | closeDuration: number of days from expiration to close the trade. 26 | """ 27 | self.__managementType = managementType 28 | self.__closeDuration = closeDuration 29 | 30 | def managePosition(self, currentPosition: optionPrimitive) -> bool: 31 | """Manages the current position in the portfolio. 32 | Managing the position means indicating whether the position should be removed from the portfolio. 33 | In addition, we could create another signalEvent here if we want to do something like roll the strategy to 34 | the next month. 35 | 36 | :param currentPosition: Current position in the portfolio. 37 | """ 38 | if self.__closeDuration is not None: 39 | # Closes position out when number of days left is less than or equal to closeDuration. 40 | if currentPosition.calcProfitLossPercentage() >= 50 or ( 41 | currentPosition.getNumberOfDaysLeft() <= self.__closeDuration): 42 | return True 43 | # This is a backup if an option is chosen where numberofdaysleft <= 1. 44 | if currentPosition.getNumberOfDaysLeft() <= 1: 45 | # Indicates that the options are expiring on (or near) this date. 46 | return True 47 | elif self.__managementType == PutVerticalManagementStrategyTypes.HOLD_TO_EXPIRATION: 48 | # Setting this to '1' since I've been using SPX data, which has European style options where trading ends 49 | # the day before expiration. 50 | if currentPosition.getNumberOfDaysLeft() <= 1: 51 | # Indicates that the options are expiring on (or near) this date. 52 | return True 53 | elif self.__managementType == PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT: 54 | if currentPosition.calcProfitLossPercentage() >= 50: 55 | return True 56 | if currentPosition.getNumberOfDaysLeft() <= 1: 57 | # Indicates that the options are expiring on (or near) this date. 58 | return True 59 | elif self.__managementType == PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS: 60 | if currentPosition.calcProfitLossPercentage() >= 50 or currentPosition.getNumberOfDaysLeft() <= 21: 61 | return True 62 | elif self.__managementType == PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS: 63 | if currentPosition.calcProfitLossPercentage() >= 50 or currentPosition.getNumberOfDaysLeft() <= 21 or ( 64 | currentPosition.calcProfitLossPercentage() <= -50): 65 | return True 66 | elif self.__managementType == PutVerticalManagementStrategyTypes.CLOSE_AT_21_DAYS: 67 | if currentPosition.getNumberOfDaysLeft() <= 21: 68 | return True 69 | else: 70 | raise NotImplementedError('No management strategy was specified or has not yet been implemented.') 71 | return False 72 | 73 | def getRiskManagementType(self) -> PutVerticalManagementStrategyTypes: 74 | """Returns the risk management type being used.""" 75 | return self.__managementType 76 | -------------------------------------------------------------------------------- /riskManager/putVerticalRiskManagementTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import unittest 4 | from base import put 5 | from optionPrimitives import putVertical 6 | from optionPrimitives import optionPrimitive 7 | from parameterized import parameterized 8 | from riskManager import putVerticalRiskManagement 9 | 10 | 11 | class TestPutVerticalRiskManagement(unittest.TestCase): 12 | 13 | def setOptionsHelper(self, closeDuration: int, expirationDateTime: datetime.datetime, 14 | settlementPricePutToBuy: decimal.Decimal, settlementPricePutToSell: decimal.Decimal, 15 | managementType: putVerticalRiskManagement.PutVerticalManagementStrategyTypes): 16 | """Helper to set values for testing. """ 17 | orderQuantity = 1 18 | contractMultiplier = 100 19 | putToBuy = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(359.69), 20 | strikePrice=decimal.Decimal(325), 21 | dateTime=datetime.datetime.strptime('01/02/1990', "%m/%d/%Y"), 22 | expirationDateTime=expirationDateTime, 23 | tradeDateTime=datetime.datetime.strptime('12/22/1989', "%m/%d/%Y"), 24 | tradePrice=decimal.Decimal(0.5005), settlementPrice=settlementPricePutToBuy) 25 | putToSell = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(359.69), 26 | strikePrice=decimal.Decimal(345), 27 | dateTime=datetime.datetime.strptime('01/02/1990', "%m/%d/%Y"), 28 | expirationDateTime=expirationDateTime, 29 | tradeDateTime=datetime.datetime.strptime('12/22/1989', "%m/%d/%Y"), 30 | tradePrice=decimal.Decimal(1.125), settlementPrice=settlementPricePutToSell) 31 | self.__shortPutVertical = putVertical.PutVertical(orderQuantity, contractMultiplier, putToBuy, putToSell, 32 | optionPrimitive.TransactionType.SELL) 33 | 34 | # Set up risk management strategy. The first argument doesn't matter if closeDuration is set. 35 | self.__riskManagementObj = putVerticalRiskManagement.PutVerticalRiskManagement(managementType=managementType, 36 | closeDuration=closeDuration) 37 | 38 | @parameterized.expand([ 39 | ("CloseDurationNumberOfDays", 3, datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 40 | decimal.Decimal(0.5005), decimal.Decimal(1.125), None), 41 | ("CloseDurationPercentage", 0, datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 42 | decimal.Decimal(0.7), decimal.Decimal(0.7), None), 43 | ("CloseDurationPercentageBackup", 0, datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 44 | decimal.Decimal(0.5005), decimal.Decimal(1.125), None), 45 | ("HoldToExpiration", None, datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 46 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 47 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.HOLD_TO_EXPIRATION), 48 | ("CloseAt50Percent", None, datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 49 | decimal.Decimal(0.7), decimal.Decimal(0.7), 50 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT), 51 | ("CloseAt50PercentBackup", None, datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 52 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 53 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT), 54 | ("CloseAt50PercentOr21Days50Percent", None, 55 | datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 56 | decimal.Decimal(0.7), decimal.Decimal(0.7), 57 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 58 | ("CloseAt50PercentOr21Days21Days", None, 59 | datetime.datetime.strptime('01/23/1990', "%m/%d/%Y"), 60 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 61 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 62 | ("CloseAt50PercentOr21DaysBackup", None, 63 | datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 64 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 65 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 66 | ("CloseAt50PercentOr21DaysOrHalfLoss21Days", None, 67 | datetime.datetime.strptime('01/23/1990', "%m/%d/%Y"), 68 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 69 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS), 70 | ("CloseAt50PercentOr21DaysOrHalfLossGreaterThan50Percent", None, 71 | datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 72 | decimal.Decimal(0.7), decimal.Decimal(0.7), 73 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS), 74 | ("CloseAt50PercentOr21DaysOrHalfLossLessThan50Percent", None, 75 | datetime.datetime.strptime('01/05/1990', "%m/%d/%Y"), 76 | decimal.Decimal(0.4), decimal.Decimal(2.0), 77 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS), 78 | ("CloseAt50PercentOr21DaysOrHalfLossBackup", None, 79 | datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 80 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 81 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS_OR_HALFLOSS), 82 | ("CloseAt21Days", None, 83 | datetime.datetime.strptime('01/23/1990', "%m/%d/%Y"), 84 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 85 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_21_DAYS), 86 | ("CloseAt21DaysBackup", None, 87 | datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 88 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 89 | putVerticalRiskManagement.PutVerticalManagementStrategyTypes.CLOSE_AT_21_DAYS), 90 | ("InvalidManagementStrategy", None, 91 | datetime.datetime.strptime('01/03/1990', "%m/%d/%Y"), 92 | decimal.Decimal(0.5005), decimal.Decimal(1.125), 93 | None), 94 | ]) 95 | def testManagePosition(self, name, closeDuration, expirationDateTime, settlementPricePutToBuy, 96 | settlementPricePutToSell, managementType): 97 | """Tests all cases for managePosition.""" 98 | self.setOptionsHelper(closeDuration, expirationDateTime, settlementPricePutToBuy, settlementPricePutToSell, 99 | managementType) 100 | if not name == 'InvalidManagementStrategy': 101 | self.assertTrue(self.__riskManagementObj.managePosition(self.__shortPutVertical)) 102 | else: 103 | with self.assertRaisesRegex( 104 | NotImplementedError, 105 | 'No management strategy was specified or has not yet been implemented.'): 106 | self.__riskManagementObj.managePosition(self.__shortPutVertical) 107 | print('Test %s passed.' % name) 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /riskManager/riskManagement.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from optionPrimitives import optionPrimitive 3 | 4 | 5 | class RiskManagement(abc.ABC): 6 | """This class is a generic type for handling risk management strategies.""" 7 | 8 | @abc.abstractmethod 9 | def managePosition(self, currentPosition: optionPrimitive) -> bool: 10 | """Manages the current position in the portfolio. 11 | Managing the position means indicating whether the position should be removed from the portfolio. 12 | In addition, we could create another signalEvent here if we want to do something like roll the strategy to 13 | the next month. 14 | 15 | :param currentPosition: Current position in the portfolio. 16 | """ 17 | pass 18 | 19 | def getRiskManagementType(self) -> int: 20 | """Returns the risk management type being used.""" 21 | pass 22 | -------------------------------------------------------------------------------- /riskManager/strangleRiskManagement.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from riskManager import riskManagement 3 | from optionPrimitives import optionPrimitive 4 | 5 | 6 | class StrangleManagementStrategyTypes(enum.Enum): 7 | HOLD_TO_EXPIRATION = 0 8 | CLOSE_AT_50_PERCENT = 1 9 | CLOSE_AT_50_PERCENT_OR_21_DAYS = 2 10 | 11 | 12 | class StrangleRiskManagement(riskManagement.RiskManagement): 13 | """This class handles risk management strategies for strangles.""" 14 | 15 | def __init__(self, managementType: StrangleManagementStrategyTypes) -> None: 16 | self.__managementType = managementType 17 | 18 | def managePosition(self, currentPosition: optionPrimitive) -> bool: 19 | """Manages the current position in the portfolio. 20 | Managing the position means indicating whether the position should be removed from the portfolio. In addition, 21 | we could create another signalEvent here if we want to do something like roll the strategy to the next month. 22 | 23 | :param currentPosition: Current position in the portfolio. 24 | """ 25 | if self.__managementType == StrangleManagementStrategyTypes.HOLD_TO_EXPIRATION: 26 | # Setting this to '1' since I've been using SPX data, which has European style options where trading ends 27 | # the day before expiration. 28 | if currentPosition.getNumberOfDaysLeft() <= 1: 29 | # Indicates that the options are expiring on (or near) this date. 30 | return True 31 | # This is a backup if an option is chosen where numberofdaysleft <= 1. 32 | if currentPosition.getNumberOfDaysLeft() <= 1: 33 | # Indicates that the options are expiring on (or near) this date. 34 | return True 35 | elif self.__managementType == StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT: 36 | if currentPosition.calcProfitLossPercentage() >= 50: 37 | return True 38 | if currentPosition.getNumberOfDaysLeft() <= 1: 39 | return True 40 | elif self.__managementType == StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS: 41 | if currentPosition.calcProfitLossPercentage() >= 50 or currentPosition.getNumberOfDaysLeft() <= 21: 42 | return True 43 | else: 44 | raise NotImplementedError('No management strategy was specified or has not yet been implemented.') 45 | return False 46 | 47 | def getRiskManagementType(self) -> StrangleManagementStrategyTypes: 48 | """Returns the risk management type being used.""" 49 | return self.__managementType 50 | -------------------------------------------------------------------------------- /riskManager/strangleRiskManagementTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import unittest 4 | from base import call 5 | from base import put 6 | from optionPrimitives import strangle 7 | from optionPrimitives import optionPrimitive 8 | from parameterized import parameterized 9 | from riskManager import strangleRiskManagement 10 | 11 | 12 | class TestStrangleRiskManagement(unittest.TestCase): 13 | 14 | def setOptionsHelper(self, expirationDateTime: datetime.datetime, settlementPricePut: decimal.Decimal, 15 | settlementPriceCall: decimal.Decimal, 16 | managementType: strangleRiskManagement.StrangleManagementStrategyTypes): 17 | """Helper to set values for testing. """ 18 | orderQuantity = 1 19 | contractMultiplier = 100 20 | putOpt = put.Put(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 21 | strikePrice=decimal.Decimal(2690), delta=0.15, vega=0.04, theta=-0.07, gamma=0.11, 22 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 23 | expirationDateTime=expirationDateTime, 24 | tradePrice=decimal.Decimal(7.475), settlementPrice=settlementPricePut) 25 | callOpt = call.Call(underlyingTicker='SPX', underlyingPrice=decimal.Decimal(2786.24), 26 | strikePrice=decimal.Decimal(2855), delta=-0.16, vega=0.05, theta=-0.06, gamma=0.12, 27 | dateTime=datetime.datetime.strptime('01/01/2021', "%m/%d/%Y"), 28 | expirationDateTime=expirationDateTime, 29 | tradePrice=decimal.Decimal(5.30), settlementPrice=settlementPriceCall) 30 | self.__strangleObj = strangle.Strangle(orderQuantity=orderQuantity, contractMultiplier=contractMultiplier, 31 | callOpt=callOpt, putOpt=putOpt, 32 | buyOrSell=optionPrimitive.TransactionType.SELL) 33 | 34 | # Set up risk management strategy. The first argument doesn't matter if closeDuration is set. 35 | self.__riskManagementObj = strangleRiskManagement.StrangleRiskManagement(managementType=managementType) 36 | 37 | @parameterized.expand([ 38 | ("HoldToExpiration", datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 39 | decimal.Decimal(7.475), decimal.Decimal(5.30), 40 | strangleRiskManagement.StrangleManagementStrategyTypes.HOLD_TO_EXPIRATION), 41 | ("CloseAt50Percent", datetime.datetime.strptime('01/03/2021', "%m/%d/%Y"), 42 | decimal.Decimal(2.0), decimal.Decimal(2.0), 43 | strangleRiskManagement.StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT), 44 | ("CloseAt50PercentBackup", datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 45 | decimal.Decimal(7.475), decimal.Decimal(5.30), 46 | strangleRiskManagement.StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT), 47 | ("CloseAt50PercentOr21Days50Percent", 48 | datetime.datetime.strptime('01/03/2021', "%m/%d/%Y"), 49 | decimal.Decimal(2.0), decimal.Decimal(2.0), 50 | strangleRiskManagement.StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 51 | ("CloseAt50PercentOr21Days21Days", 52 | datetime.datetime.strptime('01/22/2021', "%m/%d/%Y"), 53 | decimal.Decimal(7.475), decimal.Decimal(5.30), 54 | strangleRiskManagement.StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 55 | ("CloseAt50PercentOr21DaysBackup", 56 | datetime.datetime.strptime('01/02/2021', "%m/%d/%Y"), 57 | decimal.Decimal(7.475), decimal.Decimal(5.30), 58 | strangleRiskManagement.StrangleManagementStrategyTypes.CLOSE_AT_50_PERCENT_OR_21_DAYS), 59 | ("InvalidManagementStrategy", 60 | datetime.datetime.strptime('01/03/2021', "%m/%d/%Y"), 61 | decimal.Decimal(7.475), decimal.Decimal(5.30), 62 | None), 63 | ]) 64 | def testManagePosition(self, name, expirationDateTime, settlementPricePut, settlementPriceCall, managementType): 65 | """Tests all cases for managePosition.""" 66 | self.setOptionsHelper(expirationDateTime, settlementPricePut, settlementPriceCall, managementType) 67 | if not name == 'InvalidManagementStrategy': 68 | self.assertTrue(self.__riskManagementObj.managePosition(self.__strangleObj)) 69 | else: 70 | with self.assertRaisesRegex( 71 | NotImplementedError, 72 | 'No management strategy was specified or has not yet been implemented.'): 73 | self.__riskManagementObj.managePosition(self.__strangleObj) 74 | print('Test %s passed.' % name) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /sampleData/bad_column_name.csv: -------------------------------------------------------------------------------- 1 | symbol,exchange,company_name,bad_date_col_name,stock_price_close,option_symbol,option_expiration,strike,call/put,style,ask,bid,mean_price,settlement,iv,volume,open_interest,stock_price_for_iv,forward_price,isinterpolated,delta,vega,gamma,theta,rho 2 | AAPL,NASDAQ,APPLE INC,8/7/2014,94.48,AAPL 140808C00055000,8/8/2014,55,C,A,40.45,38.4,39.425,0,0.577382,0,0,94.48,,*,1,0,0,-0.000242,0.001507 -------------------------------------------------------------------------------- /strategyManager/StrangleStrat.py: -------------------------------------------------------------------------------- 1 | from strategyManager import strategy 2 | from events import tickEvent, signalEvent 3 | from optionPrimitives import optionPrimitive, strangle 4 | from base import option 5 | from riskManager import riskManagement 6 | from typing import Optional, Text, Tuple, Mapping 7 | import datetime 8 | import decimal 9 | import enum 10 | import json 11 | import logging 12 | import queue 13 | 14 | 15 | # Used to keep track of reasons why options could not be found for the strategy. 16 | class NoUpdateReason(enum.Enum): 17 | OK = 0 18 | NO_DELTA = 1 19 | NO_SETTLEMENT_PRICE = 2 20 | MIN_DTE = 3 21 | MAX_DTE = 4 22 | MIN_MAX_DELTA = 5 23 | MAX_BID_ASK = 6 24 | WRONG_TICKER = 7 25 | 26 | 27 | class StrangleStrat(strategy.Strategy): 28 | """This class sets up strangle strategy, which involves buying or selling strangles. 29 | 30 | Strangle specific attributes: 31 | optCallDelta: Optimal delta for call. 32 | maxCallDelta: Max delta for call. 33 | minCallDelta: Min delta for call 34 | optPutDelta: Optimal delta for put. 35 | maxPutDelta: Max delta for put. 36 | minPutDelta: Min delta for put. 37 | 38 | General strategy attributes: 39 | startDateTime: Date/time to start the backtest. 40 | buyOrSell: Do we buy or sell a strangle. 41 | underlyingTicker: Which underlying to use for the strategy. 42 | orderQuantity: Number of strangles. 43 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. 44 | (E.g. 100 for options and 50 for ES futures options). 45 | riskManagement: Risk management strategy (how to manage the trade; e.g., close, roll, hold to expiration). 46 | pricingSource -- Used to indicate which brokerage to use for commissions / fees. 47 | pricingSourceConfigFile -- File path to the JSON config file for commission / fees. 48 | 49 | Optional attributes: 50 | optimalDTE: Optimal number of days before expiration to put on strategy. 51 | minimumDTE: Minimum number of days before expiration to put on strategy. 52 | maximumDTE: Maximum days to expiration to put on strategy. 53 | maxBidAsk: Maximum price to allow between bid and ask prices of option (for any strike or put/call). 54 | maxCapitalToUsePerTrade: percent (as a decimal) of portfolio value we want to use per trade. 55 | minCreditDebit: Minimum credit / debit to receive upon trade entry. 56 | """ 57 | 58 | def __init__(self, eventQueue: queue.Queue, optCallDelta: float, maxCallDelta: float, minCallDelta, 59 | optPutDelta: float, maxPutDelta: float, minPutDelta: float, buyOrSell: optionPrimitive.TransactionType, 60 | underlyingTicker: Text, orderQuantity: int, contractMultiplier: int, 61 | riskManagement: riskManagement.RiskManagement, pricingSource: Text, pricingSourceConfigFile: Text, 62 | optimalDTE: Optional[int] = None, minimumDTE: Optional[int] = None, maximumDTE: Optional[int] = None, 63 | maxBidAsk: Optional[decimal.Decimal] = None, maxCapitalToUsePerTrade: Optional[decimal.Decimal] = None, 64 | startDateTime: Optional[datetime.datetime] = None, minCreditDebit: Optional[decimal.Decimal] = None): 65 | 66 | self.__eventQueue = eventQueue 67 | self.__optCallDelta = optCallDelta 68 | self.__maxCallDelta = maxCallDelta 69 | self.__minCallDelta = minCallDelta 70 | self.__optPutDelta = optPutDelta 71 | self.__maxPutDelta = maxPutDelta 72 | self.__minPutDelta = minPutDelta 73 | 74 | self.startDateTime = startDateTime 75 | self.buyOrSell = buyOrSell 76 | self.underlyingTicker = underlyingTicker 77 | self.orderQuantity = orderQuantity 78 | self.contractMultiplier = contractMultiplier 79 | self.riskManagement = riskManagement 80 | self.pricingSourceConfigFile = pricingSourceConfigFile 81 | self.pricingSource = pricingSource 82 | self.optimalDTE = optimalDTE 83 | self.minimumDTE = minimumDTE 84 | self.maximumDTE = maximumDTE 85 | self.maxBidAsk = maxBidAsk 86 | self.maxCapitalToUsePerTrade = maxCapitalToUsePerTrade 87 | self.minCreditDebit = minCreditDebit 88 | 89 | # Open JSON file and select the pricingSource. 90 | self.pricingSourceConfig = None 91 | if self.pricingSource is not None and self.pricingSourceConfigFile is not None: 92 | with open(self.pricingSourceConfigFile) as config: 93 | fullConfig = json.load(config) 94 | self.pricingSourceConfig = fullConfig[self.pricingSource] 95 | 96 | def __updateWithOptimalOption(self, currentOption: option.Option, 97 | optimalOption: option.Option) -> Tuple[bool, option.Option, enum.Enum]: 98 | """Find the option that is closest to the requested parameters (delta, expiration). 99 | 100 | :param currentOption: current option from the option chain. 101 | :param optimalOption: current optimal option based on expiration and delta. 102 | :return: tuple of (updateOption: bool, optimalOpt: option.Option, noUpdateReason: enum.Enum ). 103 | updateOption bool is used to indicate if we should update the optimal option with the current option. 104 | """ 105 | # TODO: Add support for expiration cycles other than monthly. 106 | 107 | # Check that we are using the right ticker symbol. 108 | if self.underlyingTicker not in currentOption.underlyingTicker: 109 | return (False, optimalOption, NoUpdateReason.WRONG_TICKER) 110 | 111 | # Check that delta is present in the data (could have bad data). 112 | if currentOption.delta is None: 113 | return (False, optimalOption, NoUpdateReason.NO_DELTA) 114 | 115 | # There is an error case in the input data where the options may have zero credit / debit when put on. 116 | if currentOption.settlementPrice is None: 117 | return (False, optimalOption, NoUpdateReason.NO_SETTLEMENT_PRICE) 118 | 119 | # Check that DTE is greater than the minimum. 120 | if self.minimumDTE: 121 | if not self.hasMinimumDTE(currentOption.dateTime, currentOption.expirationDateTime): 122 | return (False, optimalOption, NoUpdateReason.MIN_DTE) 123 | 124 | # Check that DTE is less than the maximum. 125 | if self.maximumDTE: 126 | if not self.hasMaximumDTE(currentOption.dateTime, currentOption.expirationDateTime): 127 | return (False, optimalOption, NoUpdateReason.MAX_DTE) 128 | 129 | # Check that delta is between the minimum and maximum delta. 130 | if currentOption.optionType == option.OptionTypes.CALL: 131 | if currentOption.delta > self.__maxCallDelta or currentOption.delta < self.__minCallDelta: 132 | return (False, optimalOption, NoUpdateReason.MIN_MAX_DELTA) 133 | else: 134 | # PUT option. 135 | if currentOption.delta < self.__maxPutDelta or currentOption.delta > self.__minPutDelta: 136 | return (False, optimalOption, NoUpdateReason.MIN_MAX_DELTA) 137 | 138 | # Check if bid / ask of option < maxBidAsk specific in strangle strategy. 139 | if self.maxBidAsk: 140 | if self.calcBidAskDiff(currentOption.bidPrice, currentOption.askPrice) > self.maxBidAsk: 141 | return (False, optimalOption, NoUpdateReason.MAX_BID_ASK) 142 | 143 | # Get current DTE in days. 144 | currentDTE = self.getNumDays(currentOption.dateTime, currentOption.expirationDateTime) 145 | optimalDTE = self.getNumDays(optimalOption.dateTime, 146 | optimalOption.expirationDateTime) if optimalOption else None 147 | requestedDTE = self.optimalDTE 148 | 149 | # Check if there is no current optimal DTE or an expiration closer to the requested expiration. 150 | newOptimalOption = optimalOption 151 | if optimalDTE is None or (abs(currentDTE - requestedDTE) < abs(optimalDTE - requestedDTE)): 152 | newOptimalOption = currentOption 153 | # Option has same DTE as optimalOpt; check deltas to choose best option. 154 | elif currentDTE == optimalDTE: 155 | currentDelta = currentOption.delta 156 | optimalDelta = optimalOption.delta 157 | if currentOption.optionType == option.OptionTypes.CALL: 158 | requestedDelta = self.__optCallDelta 159 | else: 160 | requestedDelta = self.__optPutDelta 161 | 162 | if abs(currentDelta - requestedDelta) < abs(optimalDelta - requestedDelta): 163 | newOptimalOption = currentOption 164 | # We should not be able to enter this else. 165 | # else: 166 | # return (False, newOptimalOption, NoUpdateReason.OK) 167 | 168 | return (True, newOptimalOption, NoUpdateReason.OK) 169 | 170 | def checkForSignal(self, event: tickEvent, portfolioNetLiquidity: decimal.Decimal, 171 | availableBuyingPower: decimal.Decimal) -> Mapping[Text, NoUpdateReason]: 172 | """Criteria that we need to check before generating a signal event. 173 | We go through each option in the option chain and find all the options that meet the criteria. If there are 174 | multiple options that meet the criteria, we choose the first one, but we could use some other type of rule. 175 | 176 | Attributes: 177 | event - Tick data we parse through to determine if we want to create a strangle for the strategy. 178 | portfolioNetLiquidity: Net liquidity of portfolio. 179 | availableBuyingPower: Amount of buying power available to use. 180 | Return: 181 | Dictionary of reasons for why option(s) could not be updated. Empty dictionary if options updated. 182 | """ 183 | # These variables will be used to keep track of the optimal options as we go through the option chain. 184 | optimalCallOpt = None 185 | optimalPutOpt = None 186 | 187 | # Get the data from the tick event. 188 | eventData = event.getData() 189 | 190 | if not eventData: 191 | return 192 | 193 | if self.startDateTime is not None: 194 | if eventData[0].dateTime < self.startDateTime: 195 | return 196 | 197 | # Dictionary to keep track of the reasons we couldn't find acceptable options for the strategy. 198 | noUpdateReasonDict = {} 199 | # Process one option at a time from the option chain (objects of option class). 200 | for currentOption in eventData: 201 | if currentOption.optionType == option.OptionTypes.CALL: 202 | updateOption, callOpt, noUpdateReason = self.__updateWithOptimalOption(currentOption, optimalCallOpt) 203 | if updateOption: 204 | optimalCallOpt = callOpt 205 | noUpdateReasonDict['callOption'] = noUpdateReason 206 | else: 207 | # PUT option 208 | updateOption, putOpt, noUpdateReason = self.__updateWithOptimalOption(currentOption, optimalPutOpt) 209 | if updateOption: 210 | optimalPutOpt = putOpt 211 | noUpdateReasonDict['putOption'] = noUpdateReason 212 | 213 | if not optimalPutOpt or not optimalCallOpt: 214 | logging.warning('Could not find both an optimal put and call.') 215 | return noUpdateReasonDict 216 | 217 | # If we require a minimum credit / debit to put on the trade, check here. 218 | if self.minCreditDebit: 219 | totalCreditDebit = optimalCallOpt.tradePrice + optimalPutOpt.tradePrice 220 | if totalCreditDebit < self.minCreditDebit: 221 | logging.warning('Total credit for the trade was less than the minCreditDebit specified.') 222 | return 223 | 224 | # Must check that both a CALL and PUT were found which meet criteria and are in the same expiration. 225 | if (optimalPutOpt.expirationDateTime == optimalCallOpt.expirationDateTime 226 | and not (optimalPutOpt.strikePrice == optimalCallOpt.strikePrice)): 227 | strangleObj = strangle.Strangle(self.orderQuantity, self.contractMultiplier, optimalCallOpt, optimalPutOpt, 228 | self.buyOrSell) 229 | 230 | # There is a case in the input data where the delta values are incorrect, and this results the strike price 231 | # of the short put being greater than the strike price of the short call, and this results in negative 232 | # buying power. To handle this error case, we return if the buying power is zero or negative. 233 | capitalNeeded = strangleObj.getBuyingPower() 234 | if capitalNeeded <= 0: 235 | return 236 | 237 | # Calculate opening and closing fees for the strangle. 238 | opening_fees = strangleObj.getCommissionsAndFees('open', self.pricingSource, 239 | self.pricingSourceConfig) 240 | strangleObj.setOpeningFees(opening_fees) 241 | strangleObj.setClosingFees(strangleObj.getCommissionsAndFees('close', self.pricingSource, 242 | self.pricingSourceConfig)) 243 | 244 | # Update capitalNeeded to include the opening fees. 245 | capitalNeeded += opening_fees 246 | 247 | if self.maxCapitalToUsePerTrade: 248 | portfolioToUsePerTrade = decimal.Decimal(self.maxCapitalToUsePerTrade) * portfolioNetLiquidity 249 | maxBuyingPowerPerTrade = min(availableBuyingPower, portfolioToUsePerTrade) 250 | else: 251 | maxBuyingPowerPerTrade = availableBuyingPower 252 | 253 | numContractsToAdd = int(maxBuyingPowerPerTrade / capitalNeeded) 254 | if numContractsToAdd < 1: 255 | return 256 | 257 | strangleObj.setNumContracts(numContractsToAdd) 258 | 259 | # Create signal event to put on strangle strategy and add to queue. 260 | signalObj = [strangleObj, self.riskManagement] 261 | event = signalEvent.SignalEvent() 262 | event.createEvent(signalObj) 263 | self.__eventQueue.put(event) 264 | else: 265 | logging.warning('Could not execute strategy. Reason: %s', noUpdateReasonDict) 266 | 267 | return noUpdateReasonDict 268 | -------------------------------------------------------------------------------- /strategyManager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/strategyManager/__init__.py -------------------------------------------------------------------------------- /strategyManager/putVerticalStrat.py: -------------------------------------------------------------------------------- 1 | from strategyManager import strategy 2 | from events import tickEvent, signalEvent 3 | from optionPrimitives import optionPrimitive, putVertical 4 | from base import option 5 | from riskManager import riskManagement 6 | from typing import Optional, Text, Tuple, Mapping 7 | import datetime 8 | import decimal 9 | import enum 10 | import json 11 | import logging 12 | import queue 13 | 14 | 15 | # Used to keep track of reasons why options could not be found for the strategy. 16 | class NoUpdateReason(enum.Enum): 17 | OK = 0 18 | NO_DELTA = 1 19 | NO_SETTLEMENT_PRICE = 2 20 | MIN_DTE = 3 21 | MAX_DTE = 4 22 | MIN_MAX_DELTA = 5 23 | MAX_BID_ASK = 6 24 | WRONG_TICKER = 7 25 | 26 | 27 | class PutVerticalStrat(strategy.Strategy): 28 | """This class sets up the strategy which to put on put verticals. 29 | 30 | Specific strategy attributes: 31 | optPutToBuyDelta: Optimal delta for the put to buy. 32 | maxPutToBuyDelta: Maximum delta for the put to buy. 33 | minPutToBuyDelta: Minimum delta for the put to buy 34 | optPutToSellDelta: Optimal delta for the put to sell. 35 | maxPutToSellDelta: Maximum delta for the put to sell. 36 | minPutToSellDelta: Minimum delta for the put to sell. 37 | 38 | General strategy attributes: 39 | underlyingTicker: Which underlying to use for the strategy. 40 | orderQuantity: Number of verticals to sell. 41 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. (E.g. 100 for 42 | options and 50 for ES futures options). 43 | riskManagement: Risk management strategy (how to manage the trade; e.g., close, roll, hold to expiration). 44 | pricingSource: Used to indicate which brokerage to use for commissions / fees. 45 | pricingSourceConfigFile: File path to the JSON config file for commission / fees. 46 | 47 | Optional attributes: 48 | startDateTime: Date/time to start the backtest. 49 | optimalDTE: Optimal number of days before expiration to put on strategy. 50 | minimumDTE: Minimum number of days before expiration to put on strategy. 51 | maximumDTE: Maximum days to expiration to put on strategy. 52 | maxBidAsk: Maximum price to allow between bid and ask prices of option (for any strike or put/call). 53 | maxCapitalToUsePerTrade: percent (as a decimal) of portfolio value we want to use per trade. 54 | minCreditDebit: Minimum credit / debit to receive upon trade entry. 55 | """ 56 | 57 | def __init__(self, eventQueue: queue.Queue, optPutToBuyDelta: float, maxPutToBuyDelta: float, 58 | minPutToBuyDelta: float, optPutToSellDelta: float, maxPutToSellDelta: float, minPutToSellDelta: float, 59 | underlyingTicker: Text, orderQuantity: int, contractMultiplier: int, 60 | riskManagement: riskManagement.RiskManagement, pricingSource: Text, pricingSourceConfigFile: Text, 61 | startDateTime: Optional[datetime.datetime] = None, optimalDTE: Optional[int] = None, 62 | minimumDTE: Optional[int] = None, maximumDTE: Optional[int] = None, 63 | maxBidAsk: Optional[decimal.Decimal] = None, maxCapitalToUsePerTrade: Optional[decimal.Decimal] = None, 64 | minCreditDebit: Optional[decimal.Decimal] = None): 65 | 66 | self.__eventQueue = eventQueue 67 | self.__optPutToBuyDelta = optPutToBuyDelta 68 | self.__maxPutToBuyDelta = maxPutToBuyDelta 69 | self.__minPutToBuyDelta = minPutToBuyDelta 70 | self.__optPutToSellDelta = optPutToSellDelta 71 | self.__maxPutToSellDelta = maxPutToSellDelta 72 | self.__minPutToSellDelta = minPutToSellDelta 73 | 74 | self.startDateTime = startDateTime 75 | self.buyOrSell = optionPrimitive.TransactionType.SELL 76 | self.underlyingTicker = underlyingTicker 77 | self.orderQuantity = orderQuantity 78 | self.contractMultiplier = contractMultiplier 79 | self.riskManagement = riskManagement 80 | self.pricingSourceConfigFile = pricingSourceConfigFile 81 | self.pricingSource = pricingSource 82 | self.optimalDTE = optimalDTE 83 | self.minimumDTE = minimumDTE 84 | self.maximumDTE = maximumDTE 85 | self.maxBidAsk = maxBidAsk 86 | self.maxCapitalToUsePerTrade = maxCapitalToUsePerTrade 87 | self.minCreditDebit = minCreditDebit 88 | 89 | # Open JSON file and select the pricingSource. 90 | self.pricingSourceConfig = None 91 | if self.pricingSource is not None and self.pricingSourceConfigFile is not None: 92 | with open(self.pricingSourceConfigFile) as config: 93 | fullConfig = json.load(config) 94 | self.pricingSourceConfig = fullConfig[self.pricingSource] 95 | 96 | def __updateWithOptimalOption(self, currentOption: option.Option, optimalOption: option.Option, maxPutDelta: float, 97 | optPutDelta: float, minPutDelta: float) -> Tuple[bool, option.Option, enum.Enum]: 98 | """Find the option that is closest to the requested parameters (delta, expiration). 99 | 100 | :param currentOption: current option from the option chain. 101 | :param optimalOption: current optimal option based on expiration and delta. 102 | :param maxPutDelta: maximum delta of the put option. 103 | :param optPutDelta: optimal delta of the put option. 104 | :param minPutDelta: minimum delta of the put option. 105 | :return: tuple of (updateOption: bool, optimalOpt: option.Option, noUpdateReason: enum.Enum ). updateOption bool 106 | is used to indicate if we should update the optimal option with the current option. 107 | """ 108 | # TODO: Add support for selecting specific expiration cycles (e.g., quarterly, monthly). 109 | 110 | # Check that we are using the right ticker symbol. This will match any substring; e.g., SPXPM will be matched 111 | # if underlyingTicker = SPX. 112 | if self.underlyingTicker not in currentOption.underlyingTicker: 113 | return (False, optimalOption, NoUpdateReason.WRONG_TICKER) 114 | 115 | # Check that delta is present in the data (could have bad data). 116 | if currentOption.delta is None: 117 | return (False, optimalOption, NoUpdateReason.NO_DELTA) 118 | 119 | # There is an error case in the input data where the options may have zero credit / debit when put on. 120 | if currentOption.settlementPrice is None: 121 | return (False, optimalOption, NoUpdateReason.NO_SETTLEMENT_PRICE) 122 | 123 | # Check that DTE is greater than the minimum. 124 | if self.minimumDTE: 125 | if not self.hasMinimumDTE(currentOption.dateTime, currentOption.expirationDateTime): 126 | return (False, optimalOption, NoUpdateReason.MIN_DTE) 127 | 128 | # Check that DTE is less than the maximum. 129 | if self.maximumDTE: 130 | if not self.hasMaximumDTE(currentOption.dateTime, currentOption.expirationDateTime): 131 | return (False, optimalOption, NoUpdateReason.MAX_DTE) 132 | 133 | # Check that delta is between the minimum and maximum delta. 134 | if currentOption.delta < maxPutDelta or currentOption.delta > minPutDelta: 135 | return (False, optimalOption, NoUpdateReason.MIN_MAX_DELTA) 136 | 137 | # Check if bid / ask of option < maxBidAsk specific in put vertical strategy. 138 | # This can't be used for futures option data since bid and ask price are not reliable / zero / etc. 139 | if self.maxBidAsk: 140 | if self.calcBidAskDiff(currentOption.bidPrice, currentOption.askPrice) > self.maxBidAsk: 141 | return (False, optimalOption, NoUpdateReason.MAX_BID_ASK) 142 | 143 | # Get current DTE in days. 144 | currentDTE = self.getNumDays(currentOption.dateTime, currentOption.expirationDateTime) 145 | optimalDTE = self.getNumDays(optimalOption.dateTime, 146 | optimalOption.expirationDateTime) if optimalOption else None 147 | requestedDTE = self.optimalDTE 148 | 149 | # Check if there is no current optimal DTE or an expiration closer to the requested expiration. 150 | newOptimalOption = optimalOption 151 | if optimalDTE is None or (abs(currentDTE - requestedDTE) < abs(optimalDTE - requestedDTE)): 152 | newOptimalOption = currentOption 153 | # Option has same DTE as optimalOpt; check deltas to choose best option. 154 | elif currentDTE == optimalDTE: 155 | currentDelta = currentOption.delta 156 | optimalDelta = optimalOption.delta 157 | if abs(currentDelta - optPutDelta) < abs(optimalDelta - optPutDelta): 158 | newOptimalOption = currentOption 159 | # We should not be able to enter this else. 160 | # else: 161 | # return (False, newOptimalOption, NoUpdateReason.OK) 162 | 163 | return (True, newOptimalOption, NoUpdateReason.OK) 164 | 165 | def checkForSignal(self, event: tickEvent, portfolioNetLiquidity: decimal.Decimal, 166 | availableBuyingPower: decimal.Decimal) -> Mapping[Text, NoUpdateReason]: 167 | """Criteria that we need to check before generating a signal event. 168 | We go through each option in the option chain and find all the options that meet the criteria. If there are 169 | multiple options that meet the criteria, we choose the first one, but we could use some other type of rule. 170 | 171 | Attributes: 172 | event: Tick data we parse through to determine if we want to create a putVertical for the strategy. 173 | portfolioNetLiquidity: Net liquidity of portfolio. 174 | availableBuyingPower: Amount of buying power available to use. 175 | Return: 176 | Dictionary of reasons for why option(s) could not be updated. Empty dictionary if options updated. 177 | """ 178 | # These variables will be used to keep track of the optimal options as we go through the option chain. 179 | optimalPutOptionToSell = None 180 | optimalPutOptionToBuy = None 181 | 182 | # Get the data from the tick event. 183 | eventData = event.getData() 184 | 185 | if not eventData: 186 | return 187 | 188 | if self.startDateTime is not None: 189 | if eventData[0].dateTime < self.startDateTime: 190 | return 191 | 192 | # Dictionary to keep track of the reasons we couldn't find acceptable options for the strategy. 193 | noUpdateReasonDict = {} 194 | # Process one option at a time from the option chain (objects of option class). 195 | for currentOption in eventData: 196 | if currentOption.optionType == option.OptionTypes.PUT: 197 | # Handle put to buy first. 198 | updateOption, putOptToBuy, noUpdateReason = self.__updateWithOptimalOption( 199 | currentOption, optimalPutOptionToBuy, self.__maxPutToBuyDelta, self.__optPutToBuyDelta, 200 | self.__minPutToBuyDelta) 201 | if updateOption: 202 | optimalPutOptionToBuy = putOptToBuy 203 | if noUpdateReasonDict.get('putToBuy', None) is None or ( 204 | noUpdateReasonDict['putToBuy'] != NoUpdateReason.OK): 205 | noUpdateReasonDict['putToBuy'] = noUpdateReason 206 | 207 | # Handle put to sell. 208 | updateOption, putOptToSell, noUpdateReason = self.__updateWithOptimalOption( 209 | currentOption, optimalPutOptionToSell, self.__maxPutToSellDelta, self.__optPutToSellDelta, 210 | self.__minPutToSellDelta) 211 | if updateOption: 212 | optimalPutOptionToSell = putOptToSell 213 | if noUpdateReasonDict.get('putToSell', None) is None or ( 214 | noUpdateReasonDict['putToSell'] != NoUpdateReason.OK): 215 | noUpdateReasonDict['putToSell'] = noUpdateReason 216 | 217 | if not optimalPutOptionToBuy or not optimalPutOptionToSell: 218 | logging.warning('Could not find both an optimal put to buy and optimal put to sell.') 219 | return noUpdateReasonDict 220 | 221 | # If we require a minimum credit / debit to put on the trade, check here. 222 | if self.minCreditDebit: 223 | totalCreditDebit = -optimalPutOptionToBuy.tradePrice + optimalPutOptionToSell.tradePrice 224 | if totalCreditDebit < self.minCreditDebit: 225 | logging.warning('Total credit for the trade was less than the minCreditDebit specified.') 226 | return 227 | 228 | # Must check that both PUTs were found which are in the same expiration but do not have the same strike price. 229 | if (optimalPutOptionToBuy.expirationDateTime == optimalPutOptionToSell.expirationDateTime) and not ( 230 | optimalPutOptionToBuy.strikePrice == optimalPutOptionToSell.strikePrice): 231 | putVerticalObj = putVertical.PutVertical(self.orderQuantity, self.contractMultiplier, optimalPutOptionToBuy, 232 | optimalPutOptionToSell, self.buyOrSell) 233 | 234 | # There is a case in the input data where the delta values are incorrect, and this results the strike price 235 | # of the long put being greater than the strike price of the short put, and this results in negative buying 236 | # power. To handle this error case, we return if the buying power is zero or negative. 237 | capitalNeeded = putVerticalObj.getBuyingPower() 238 | if capitalNeeded <= 0: 239 | logging.warning('Capital needed to put on trade was <= 0; likely a data problem.') 240 | return 241 | 242 | # Calculate opening and closing fees for the putVertical. 243 | opening_fees = putVerticalObj.getCommissionsAndFees('open', self.pricingSource, 244 | self.pricingSourceConfig) 245 | putVerticalObj.setOpeningFees(opening_fees) 246 | putVerticalObj.setClosingFees(putVerticalObj.getCommissionsAndFees('close', self.pricingSource, 247 | self.pricingSourceConfig)) 248 | 249 | # Update capitalNeeded to include the opening fees. 250 | capitalNeeded += opening_fees 251 | 252 | if self.maxCapitalToUsePerTrade: 253 | portfolioToUsePerTrade = decimal.Decimal(self.maxCapitalToUsePerTrade) * portfolioNetLiquidity 254 | maxBuyingPowerPerTrade = min(availableBuyingPower, portfolioToUsePerTrade) 255 | else: 256 | maxBuyingPowerPerTrade = availableBuyingPower 257 | 258 | numContractsToAdd = int(maxBuyingPowerPerTrade / capitalNeeded) 259 | if numContractsToAdd < 1: 260 | return 261 | 262 | putVerticalObj.setNumContracts(numContractsToAdd) 263 | 264 | # Create signal event to put on put vertical strategy and add to queue. 265 | signalObj = [putVerticalObj, self.riskManagement] 266 | event = signalEvent.SignalEvent() 267 | event.createEvent(signalObj) 268 | self.__eventQueue.put(event) 269 | else: 270 | logging.warning('Could not execute strategy. Reason: %s', noUpdateReasonDict) 271 | 272 | return noUpdateReasonDict 273 | -------------------------------------------------------------------------------- /strategyManager/strategy.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import datetime 3 | import decimal 4 | import enum 5 | from optionPrimitives import optionPrimitive 6 | from typing import Optional, Text 7 | 8 | 9 | class ExpirationTypes(enum.Enum): 10 | MONTHLY = 0 11 | WEEKLY = 1 12 | QUARTERLY = 2 13 | ANY = 3 14 | 15 | 16 | @dataclasses.dataclass 17 | class Strategy: 18 | """This class sets up the basics for every strategy that will be used; For example, if we want to do an iron condor 19 | or a strangle, there are certain parameters that must be defined. 20 | 21 | Attributes: 22 | startDateTime: Date/time to start the backtest. 23 | buyOrSell: Do we buy or sell the strategy? E.g. sell a strangle. 24 | underlyingTicker: Which underlying to use for the strategy. 25 | orderQuantity: Number of the strategy, e.g. number of strangles. 26 | contractMultiplier: scaling factor for number of "shares" represented by an option or future. (E.g. 100 for 27 | options and 50 for ES futures options). 28 | expCycle: Specifies if we want to do monthly, weekly, quarterly, etc. 29 | optimalDTE: Optimal number of days before expiration to put on strategy. 30 | minimumDTE: Minimum number of days before expiration to put on strategy. 31 | maximumDTE: Maximum number of days to expiration for the strategy. 32 | minimumROC: Minimum return on capital for overall trade as a decimal. 33 | minCredit: Minimum credit to collect on overall trade. 34 | maxBidAsk: Maximum price to allow between bid and ask prices of option (for any strike or put/call). 35 | maxCapitalToUsePerTrade: percent (as a decimal) of portfolio value we want to use per trade. 36 | """ 37 | 38 | startDateTime: datetime.datetime 39 | buyOrSell: optionPrimitive.TransactionType 40 | underlyingTicker: Text 41 | orderQuantity: int 42 | contractMultiplier: int 43 | expCycle: Optional[ExpirationTypes] = None 44 | optimalDTE: Optional[int] = None 45 | minimumDTE: Optional[int] = None 46 | maximumDTE: Optional[int] = None 47 | minimumROC: Optional[float] = None 48 | minCredit: Optional[decimal.Decimal] = None 49 | maxBidAsk: Optional[decimal.Decimal] = None 50 | maxCapitalToUsePerTrade: Optional[decimal.Decimal] = None 51 | 52 | def __post_init__(self): 53 | if self.__class__ == Strategy: 54 | raise TypeError('Cannot instantiate class.') 55 | 56 | def calcBidAskDiff(self, bidPrice: decimal.Decimal, askPrice: decimal.Decimal) -> decimal.Decimal: 57 | """ Calculate the absolute difference between the bid and ask price. 58 | 59 | :param bidPrice: price at which the option can be sold. 60 | :param askPrice: price at which the option can be bought. 61 | :return: Absolute difference; 62 | """ 63 | return abs(bidPrice - askPrice) 64 | 65 | def isMonthlyExp(self, dateTime: datetime.datetime): 66 | """Check if the option expiration falls on the third Friday of the month, or if the third Friday is a holiday, 67 | check if the expiration falls on the Thursday that precedes it. 68 | 69 | :param dateTime: option expiration date in mm/dd/yy format. 70 | :return: True if it's a monthly option; False otherwise. 71 | """ 72 | return (dateTime.weekday() == 4 and 14 < dateTime.day < 22) 73 | 74 | def hasMinimumDTE(self, curDateTime: datetime.datetime, expDateTime: datetime.datetime) -> bool: 75 | """"Determine if the current expiration date of the option is >= self.minimumDTE days from the current date. 76 | 77 | :param curDateTime: current date in mm/dd/yy format. 78 | :param expDateTime: option expiration date in mm/dd/yy format. 79 | :return: True if difference between current date and dateTime is >= self.minimumDTE; else False. 80 | """ 81 | return (expDateTime - curDateTime).days >= self.minimumDTE 82 | 83 | def hasMaximumDTE(self, curDateTime: datetime.datetime, expDateTime: datetime.datetime) -> bool: 84 | """"Determine if the current expiration date of the option is <= self.maximumDTE days from the current date. 85 | 86 | :param curDateTime: current date in mm/dd/yy format. 87 | :param expDateTime: option expiration date in mm/dd/yy format. 88 | :return: True if difference between current date and dateTime is <= self.maximumDTE; else False. 89 | """ 90 | return (expDateTime - curDateTime).days <= self.maximumDTE 91 | 92 | def getNumDays(self, curDateTime: datetime.datetime, expDateTime: datetime.datetime) -> int: 93 | """"Determine the number of days between the curDateTime and the expDateTime. 94 | :param curDateTime: current date in mm/dd/yy format. 95 | :param expDateTime: option expiration date in mm/dd/yy format. 96 | :return: Number of days between curDateTime and expDateTime. 97 | """ 98 | return (expDateTime - curDateTime).days 99 | -------------------------------------------------------------------------------- /strategyManager/strategyTest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | from optionPrimitives import optionPrimitive 4 | from strategyManager import strategy 5 | 6 | 7 | class TestStrategyClass(unittest.TestCase): 8 | 9 | def testStrategyClassCreation(self): 10 | """Tests than an exception is raised when class is instantiated.""" 11 | with self.assertRaisesRegex(TypeError, 'Cannot instantiate class.'): 12 | strategy.Strategy(startDateTime=datetime.datetime.now(), buyOrSell=optionPrimitive.TransactionType.SELL, 13 | underlyingTicker='SPY', orderQuantity=1, contractMultiplier=100) 14 | 15 | 16 | if __name__ == '__main__': 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirnfs/OptionSuite/5df8636fcb94462c44c5f3b778becefb592f4d88/utils/__init__.py -------------------------------------------------------------------------------- /utils/combineCSVs.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pandas as pd 3 | 4 | if __name__ == "__main__": 5 | 6 | # files = ['/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/ES/FUT_Option_20051101-20101102.csv', 7 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/ES/FUT_Option_20101103-20151104.csv', 8 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/ES/FUT_Option_20151105-20181105.csv', 9 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/ES/FUT_Option_20181106-20211105.csv', 10 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/ES/FUT_Option_20211108-20220516.csv'] 11 | 12 | # files_to_reindex = ['/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_1990_1999.csv', 13 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2000_2010.csv', 14 | # '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2011_2016.csv'] 15 | # 16 | # for file in files_to_reindex: 17 | # df = pd.read_csv(file) 18 | # df = df.reindex(columns=['date', 'symbol', 'exchange', 'company_name', 'stock_price_close', 'option_symbol', 19 | # 'option_expiration', 'strike', 'call/put', 'style', 'bid', 'ask', 'mean_price', 20 | # 'settlement', 'iv', 'volume', 'open_interest', 'stock_price_for_iv', 'forward_price', 21 | # 'isinterpolated', 'delta', 'vega', 'gamma', 'theta', 'rho']) 22 | # df.rename(columns={"call/put": "call_put"}, inplace=True) 23 | # base_name = file.split(".csv")[0] 24 | # df.to_csv(base_name + '_reindexed.csv', header=True, index=False) 25 | 26 | files = ['/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_1990_1999_reindexed.csv', 27 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2000_2010_reindexed.csv', 28 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2011_2016_reindexed.csv', 29 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2017_2018.csv', 30 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2019_2020.csv', 31 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2021.csv', 32 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2022.csv', 33 | '/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2023.csv'] 34 | 35 | chunkSize = 10000 36 | useHeader = True 37 | 38 | for file in files: 39 | for chunk in pd.read_csv(file, chunksize=chunkSize): 40 | if useHeader: 41 | chunk.to_csv('/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/combinedSPX_1990_2023.csv', 42 | header=True, mode='a', index=False) 43 | useHeader = False 44 | else: 45 | chunk.to_csv('/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/combinedSPX_1990_2023.csv', 46 | header=False, mode='a', index=False) 47 | 48 | # df = pd.read_csv('/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2011_2016.csv') 49 | # print("#rows is: ", len(df.index)) 50 | # df_new = df[pd.to_datetime(df['date'], format='%m/%d/%Y') < datetime.datetime.strptime("01/01/2017", "%m/%d/%Y")] 51 | # print("#rows is: ", len(df_new.index)) 52 | # df_new.to_csv('/Users/msantoro/PycharmProjects/Backtester/marketData/iVolatility/SPX/SPX_2011_2016_cropped.csv', 53 | # header=True, index=False) 54 | --------------------------------------------------------------------------------