├── requirements.txt ├── img ├── with_dd.PNG └── without_dd.PNG ├── LICENSE ├── README.md └── CPPI.py /requirements.txt: -------------------------------------------------------------------------------- 1 | alpaca-trade-api==0.42 2 | -------------------------------------------------------------------------------- /img/with_dd.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harkishan-99/Alpaca-CPPI/HEAD/img/with_dd.PNG -------------------------------------------------------------------------------- /img/without_dd.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harkishan-99/Alpaca-CPPI/HEAD/img/without_dd.PNG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Harkishan Singh Baniya 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 | # Alpaca-CPPI 2 | A constant proportion portfolio insurance (CPPI) trading algorithm on top of Alpaca's Trading API. 3 | 4 | ## Installation 5 | The algorithm was tested on the Alpaca Trade API version mentioned the requirements file and is considered as the stable version for this project. 6 | User may try different versions but author doesn't guarantee that will work. 7 | 8 | ```bash 9 | 10 | pip install -r requirements.txt 11 | ``` 12 | ## Usage 13 | 14 | ### Trading Strategy 15 | 16 | To run the strategy user is need to initialize the algorithm with the risky asset you want to invest in, a safe asset if you have one else it keeps 17 | the safe allocation as cash in the trading account, the initial investment capital, the floor percentage, the multiplier/leverage and the rebalancing frequency. 18 | If you are not sure what parameters to choose, I recommend playing with the parameters in the backtesting [notebook](https://github.com/Harkishan-99/Alpaca-CPPI/blob/main/notebook.ipynb). 19 | Once you are settled with the parameters, a strategy instance can be created from the ```CPPI``` class as shown in the below example. User can create multiple 20 | instances of the strategy by creating that many ```CPPI``` class instances. Remember, that if for any reason the strategy is interrupted by the user, it can be 21 | restarted without any need of closing the residual positions (the positions left open after the interruption), the strategy will detect any open position in 22 | the asset and rebalance them accordingly. 23 | 24 | ```python 25 | #import the CPPI class 26 | from CPPI import CPPI 27 | 28 | #set the strategy params 29 | r_asset = 'SPY'#risky_asset 30 | s_asset = None #safe_asset 31 | capital = 1000 32 | rebalance_freq = 1 #days or daily 33 | floor_pct = 0.8 #80% 34 | m = 3 #asset_muliplier 35 | 36 | #create a instance 37 | spy_cppi = CPPI(risky_asset=r_asset, cppi_budget=capital, safe_asset=s_asset, 38 | floor_percent=floor_pct, asset_muliplier=m) 39 | #start the strategy 40 | spy_cppi.run(period_in_days=reblance_freq) 41 | 42 | ``` 43 | 44 | 45 | ### Research Notebook 46 | 47 | The research [notebook](https://github.com/Harkishan-99/Alpaca-CPPI/blob/main/notebook.ipynb) can be used for backtesting CPPI strategies both with and without 48 | the drawdown constraint. It also provides various example's of different CPPI settings that was used for the testing, along with the backtest report and chart. 49 | 50 | * CPPI without drawdown constraint 51 | 52 | ![CPPI without drawdown constraint](https://github.com/Harkishan-99/Alpaca-CPPI/blob/main/img/without_dd.PNG) 53 | 54 | * CPPI with drawdown constraint 55 | 56 | ![CPPI with drawdown constraint](https://github.com/Harkishan-99/Alpaca-CPPI/blob/main/img/with_dd.PNG) 57 | 58 | 59 | ### Disclaimer 60 | The trading strategy discussed here is for educational purpose only doesn't guarantee to make profit. Trading involves a high risk of losing money. 61 | Use the code provided here at your own risk. The author and AlpacaDB, Inc. are not responsible for your trading results i.e. any profit or loss caused 62 | by the algorithm. 63 | User is advised to run the code on paper trading account only to understand the risk involved. 64 | -------------------------------------------------------------------------------- /CPPI.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | import time 4 | import numpy as np 5 | import pandas as pd 6 | import alpaca_trade_api as tradeapi 7 | 8 | 9 | API_KEY = "ENTER YOUR API KEY" 10 | API_SECRET = "ENTER YOUR API SECRET" 11 | api = tradeapi.REST(API_KEY, API_SECRET, base_url='https://paper-api.alpaca.markets', api_version='v2') 12 | 13 | class CPPI: 14 | """ 15 | The CPPI algorithm class. 16 | """ 17 | 18 | def __init__(self, risky_asset:str, cppi_budget:int, safe_asset:str=None, 19 | floor_percent:float=0.8, asset_muliplier:int=3): 20 | """ 21 | 22 | :param assets :(str) the ticker symbols of the risky assets to invest in. 23 | E.g. : 'AAPL' or 'GS' 24 | :param cppi_budget :(int) the budget to be allocated to CPPI algorithm. 25 | :param safe_asset :(str) the safe asset ticker symbol. Default is None and will 26 | keep the safe allocation as cash in the trading account. 27 | :param floor_percent :(float) this will be the floor percentage that the CPPI will 28 | try to maintain. Deafult is 80% of the initial budget. 29 | :param asset_muliplier :(int) the risky asset mutiplier for the CPPI. This is the 30 | risk aversion parameter and usually it is set between 3 and 6. 31 | Deafult is 3. 32 | """ 33 | #set the CPPI strategy params 34 | self.risky_asset = risky_asset 35 | self.safe_asset = safe_asset 36 | self.cppi_value = cppi_budget 37 | self.floor_percent = floor_percent 38 | self.floor_value = cppi_budget * floor_percent 39 | self.m = asset_muliplier 40 | self.max_cppi_value = cppi_budget 41 | self.position_value = None 42 | #check if the account permits the given budget 43 | self._check_budget(cppi_budget) 44 | #open a csv file to store the cppi metrics 45 | self.savefile = f'{risky_asset}_cppi.csv' 46 | if not os.path.exists(self.savefile): 47 | with open(self.savefile, 'w', newline='') as file: 48 | wr = csv.writer(file) 49 | #initialize the header 50 | header = ['cppi value', 'floor'] 51 | wr.writerow(header) 52 | 53 | 54 | def _check_budget(self, required_capital:float): 55 | """ 56 | A function that checks if the current account value meets the CPPI budget. 57 | """ 58 | available_cash = float(api.get_account().cash) 59 | if required_capital > available_cash: 60 | raise Exception("Not enough available cash") 61 | 62 | def place_order(self, symbol:str, dollar_amount:float): 63 | """ 64 | A function that places a market order in Alpaca based on the 65 | dollar amount to buy (e.g. $1000) or short (e.g. -$1000) 66 | for the given asset symbol. 67 | """ 68 | if np.sign(dollar_amount) > 0: 69 | side = 'buy' 70 | elif np.sign(dollar_amount) < 0: 71 | side = 'sell' 72 | current_asset_price = api.get_last_trade(symbol).price 73 | qty = int(abs(dollar_amount) / current_asset_price) 74 | if qty > 0: 75 | order = api.submit_order(symbol=symbol, 76 | qty=qty, 77 | side=side, 78 | type='market', 79 | time_in_force='day') 80 | 81 | 82 | def rebalance(self, risk_alloc:float, safe_alloc:float): 83 | """ 84 | This function will check if any reblancing is required based on the 85 | recent CPPI risky asset allocation and safe asset allocation. 86 | """ 87 | if self.position_value is None: 88 | #long the entire budget 89 | self.place_order(self.risky_asset, risk_alloc) 90 | #buy the safe asset also if given 91 | if self.safe_asset is not None: 92 | self.place_order(self.safe_asset, safe_alloc) 93 | else: 94 | #get the excess risk allocation 95 | excess_risk_alloc = risk_alloc - self.position_value[0] 96 | excess_safe_alloc = safe_alloc - self.position_value[1] 97 | #check if reblancing is required 98 | if abs(excess_risk_alloc) > 0: 99 | #reblance the risky asset 100 | self.place_order(self.risky_asset, excess_risk_alloc) 101 | #reblance the safe asset if available 102 | if self.safe_asset is not None: 103 | self.place_order(self.safe_asset, excess_safe_alloc) 104 | 105 | def get_position_value(self, symbol:str): 106 | """ 107 | Get the current value of the asset position. 108 | """ 109 | value, returns = None, None 110 | try: 111 | #get the position details 112 | pos = api.get_position(symbol) 113 | #return = (current_price/avg_entry_price) - 1 114 | returns = (float(pos.current_price)/float(pos.avg_entry_price)) - 1 115 | #value = current_price * qty 116 | value = float(pos.current_price) * int(pos.qty) 117 | 118 | except Exception as e: 119 | #position doesn't exists 120 | pass 121 | return value, returns 122 | 123 | def _check_market_open(self): 124 | """ 125 | A function to check if the market open. If not the sleep till 126 | the market opens. 127 | """ 128 | clock = api.get_clock() 129 | if clock.is_open: 130 | pass 131 | else: 132 | time_to_open = clock.next_open - clock.timestamp 133 | print( 134 | f"Market is closed now going to sleep for {time_to_open.total_seconds()//60} minutes") 135 | time.sleep(time_to_open.total_seconds()) 136 | 137 | def _check_position(self): 138 | """ 139 | A function to retrieve the current position value and return of 140 | risky and safe assets. 141 | """ 142 | risky_position, risky_ret = self.get_position_value(self.risky_asset) 143 | if risky_position is not None: 144 | if self.safe_asset is not None: 145 | safe_position, safe_ret = self.get_position_value(self.safe_asset) 146 | if safe_position is not None: 147 | #both position exists 148 | self.position_value = [risky_position, safe_position] 149 | else: 150 | #safe asset position doesn't exists 151 | self.position_value = [risky_position, 0] 152 | safe_ret = 0 153 | 154 | elif self.safe_asset is not None: 155 | safe_position, safe_ret = self.get_position_value(self.safe_asset) 156 | if safe_position is not None: 157 | #only safe asset position exists 158 | self.position_value = [0, safe_position] 159 | risk_ret = 0 160 | else: 161 | #no position exists for either 162 | self.position_value = None 163 | risky_ret = 0 164 | safe_ret = 0 165 | return risky_ret, safe_ret 166 | 167 | 168 | def save_cppi_metrics(self): 169 | with open(self.savefile, 'w', newline='') as file: 170 | wr = csv.writer(file) 171 | wr.writerow([self.cppi_value, self.floor_value]) 172 | 173 | def run(self, period_in_days:int=1): 174 | """ 175 | Start the CPPI algorithm. 176 | 177 | :param period_in_days :(int) rebalancing period in days. 178 | Default is 1 day. 179 | """ 180 | self._check_market_open() 181 | #check if any positions already exists for the risky asset 182 | _, _ = self._check_position() 183 | while True: 184 | self.max_cppi_value = max(self.max_cppi_value, self.cppi_value) 185 | self.floor_value = self.max_cppi_value*self.floor_percent 186 | #calculate the cushion 187 | cushion = self.cppi_value - self.floor_value 188 | #compute the allocations towards safe and risky assets 189 | risk_alloc = max(min(self.m*cushion, self.cppi_value), 0) 190 | safe_alloc = self.cppi_value - risk_alloc 191 | #order the allocation 192 | self.rebalance(risk_alloc, safe_alloc) 193 | #sleep till next rebalancing. 194 | time.sleep(period_in_days*24*60*60) 195 | self._check_market_open() 196 | #re-calculate the CPPI value based on the asset holding returns 197 | risky_ret, safe_ret = self._check_position() 198 | self.cppi_value = risk_alloc*(1 + risky_ret) + safe_alloc*(1 + safe_ret) 199 | #save the tracking metrics 200 | self.save_cppi_metrics() 201 | --------------------------------------------------------------------------------