-
Notifications
You must be signed in to change notification settings - Fork 4
/
cc.py
269 lines (198 loc) · 10.7 KB
/
cc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
from configuration import configuration, dbName, debugEverythingNeedsRolling
from optionChain import OptionChain
from statistics import median
from tinydb import TinyDB, Query
import datetime
import time
import alert
import support
class Cc:
def __init__(self, asset):
self.asset = asset
def findNew(self, api, existing, existingPremium):
asset = self.asset
newccExpDate = support.getNewCcExpirationDate()
# get option chain of third thursday and friday of the month
optionChain = OptionChain(api, asset, newccExpDate, 1)
chain = optionChain.get()
if not chain:
return alert.botFailed(asset, 'No chain found on the third thursday OR friday')
# get closest chain to days
# it will get friday most of the time, but if a friday is a holiday f.ex. the chain will only return a thursday date chain
closestChain = chain[-1]
atmPrice = api.getATMPrice(asset)
strikePrice = atmPrice + configuration[asset]['minGapToATM']
minStrike = configuration[asset]['minStrike']
if existing:
maxDrawdownStrike = existing['strike'] - configuration[asset]['maxDrawdownGap']
else:
maxDrawdownStrike = None
if maxDrawdownStrike and maxDrawdownStrike > strikePrice:
print('Applying max drawdown (' + str(maxDrawdownStrike) + ') ...')
strikePrice = maxDrawdownStrike
if minStrike > strikePrice:
print('Min strike hit, applying (' + str(minStrike) + ') ...')
strikePrice = minStrike
# get the best matching contract
contract = optionChain.getContractFromDateChain(strikePrice, closestChain['contracts'])
if not contract:
return alert.botFailed(asset, 'No contract over minStrike found')
# check minYield
projectedPremium = median([contract['bid'], contract['ask']])
if projectedPremium < existingPremium:
if existing:
print('Failed to write contract for CREDIT with CC (' + str(contract['strike']) + '), now trying to get a lower strike ...')
# we need to get a lower strike instead to not pay debit
contract = optionChain.getContractFromDateChainByMinYield(existing['strike'], strikePrice, existingPremium, closestChain['contracts'])
# edge case where this new contract fails:
# - If even a calendar roll wouldn't result in a credit
# - If we have a 301 f.ex. but the new chain only has 300 or 305 with less premium than the 301
# to prevent failing, we could f.ex. check maxRollupGap, ignoring deepITMLimit and if we can rollup for debit, then do that instead of failing
if not contract:
return alert.botFailed(asset, 'couldn\'t find contract for CREDIT above last strike price')
deepItmLimitStrike = atmPrice - configuration[asset]['deepITMLimit']
# allow to pay for roll up if we are too far itm
if configuration[asset]['maxRollupGap'] > 0 and contract['strike'] < deepItmLimitStrike:
maxRollupGapStrike = existing['strike'] + configuration[asset]['maxRollupGap']
# rollup to deepITMLimit, with a max jump of maxRollupGap per month
rollUpStrike = maxRollupGapStrike if maxRollupGapStrike < deepItmLimitStrike else deepItmLimitStrike
if rollUpStrike > contract['strike']:
print('Could roll to ' + str(contract['strike']) + ' for CREDIT, but its too far ITM ...')
print('Rolling towards deepITMLimit instead with this contract: ' + str(rollUpStrike) + ', paying debit ...')
contract = optionChain.getContractFromDateChain(rollUpStrike, closestChain['contracts'])
# todo should we check if the account has enough cash to rollup to this contract?
projectedPremium = median([contract['bid'], contract['ask']])
else:
return alert.botFailed(asset, 'Api / code error: No existing contract and projected premium for ' +
str(strikePrice) + ' is smaller than ' + str(existingPremium))
return {
'date': closestChain['date'],
'days': closestChain['days'],
'contract': contract,
'projectedPremium': projectedPremium
}
def existing(self):
db = TinyDB(dbName)
ret = db.search(Query().stockSymbol == self.asset)
db.close()
return ret
def writeCcs(api):
for asset in configuration:
asset = asset.upper()
cc = Cc(asset)
try:
existing = cc.existing()[0]
except IndexError:
existing = None
if (existing and needsRolling(existing)) or not existing:
amountToSell = configuration[asset]['amountOfHundreds']
if existing:
existingSymbol = existing['optionSymbol']
amountToBuyBack = existing['count']
existingPremium = api.getATMPrice(existing['optionSymbol'])
else:
existingSymbol = None
amountToBuyBack = 0
existingPremium = 0
new = cc.findNew(api, existing, existingPremium)
print('The bot wants to write the following contract:')
print(new)
if not api.checkAccountHasEnoughToCover(asset, existingSymbol, amountToBuyBack, amountToSell, new['contract']['strike'], new['date']):
return alert.botFailed(asset, 'The account doesn\'t have enough shares or options to cover selling '
+ str(amountToSell) + ' cc(\'s)')
writeCc(api, asset, new, existing, existingPremium, amountToBuyBack, amountToSell)
else:
print('Nothing to write ...')
def needsRolling(cc):
if debugEverythingNeedsRolling:
return True
# needs rolling on date BEFORE expiration (if the market is closed, it will trigger ON expiration date)
nowPlusOffset = (datetime.datetime.utcnow() + datetime.timedelta(days=support.ccExpDaysOffset)).strftime('%Y-%m-%d')
return nowPlusOffset >= cc['expiration']
def writeCc(api, asset, new, existing, existingPremium, amountToBuyBack, amountToSell, retry=0, partialContractsSold=0):
maxRetries = 75
# lower the price by 1% for each retry if we couldn't get filled
orderPricePercentage = 100 - retry
if retry > maxRetries:
return alert.botFailed(asset, 'Order cant be filled, tried with ' + str(orderPricePercentage + 1) + '% of the price.')
if existing and existingPremium:
orderId = api.writeNewContracts(
existing['optionSymbol'],
amountToBuyBack,
existingPremium,
new['contract']['symbol'],
amountToSell,
new['projectedPremium'],
orderPricePercentage
)
else:
orderId = api.writeNewContracts(
None,
0,
0,
new['contract']['symbol'],
amountToSell,
new['projectedPremium'],
orderPricePercentage
)
checkFillXTimes = 12
if retry > 0:
# go faster through it
checkFillXTimes = 6
for x in range(checkFillXTimes):
# try to fill it for x * 5 seconds
print('Waiting for order to be filled ...')
time.sleep(5)
checkedOrder = api.checkOrder(orderId)
if checkedOrder['filled']:
print('Order has been filled!')
break
if not checkedOrder['filled']:
api.cancelOrder(orderId)
print('Cant fill order, retrying with lower price ...')
if checkedOrder['partialFills'] > 0:
if checkedOrder['complexOrderStrategyType'] is None or (checkedOrder['complexOrderStrategyType'] and checkedOrder['complexOrderStrategyType'] != 'DIAGONAL'):
# partial fills are only possible on DIAGONAL orders, so this should never happen
return alert.botFailed(asset, 'Partial fill on custom order, manual review required: ' + str(checkedOrder['partialFills']))
# on diagonal fill is per leg, 1 fill = 1 bought back and 1 sold
# quick verification, this should never be true
if not (amountToBuyBack == amountToSell and amountToBuyBack > checkedOrder['partialFills']):
return alert.botFailed(asset, 'Partial fill amounts do not match, manual review required')
diagonalAmountBothWays = amountToBuyBack - checkedOrder['partialFills']
receivedPremium = checkedOrder['typeAdjustedPrice'] * checkedOrder['partialFills']
alert.alert(asset, 'Partial fill: Bought back ' + str(checkedOrder['partialFills']) + 'x ' + existing['optionSymbol'] + ' and sold ' + str(
checkedOrder['partialFills']) + 'x ' +
new['contract']['symbol'] + ' for ' + str(receivedPremium))
return writeCc(api, asset, new, existing, existingPremium, diagonalAmountBothWays, diagonalAmountBothWays, retry + 1,
partialContractsSold + checkedOrder['partialFills'])
return writeCc(api, asset, new, existing, existingPremium, amountToBuyBack, amountToSell, retry + 1, partialContractsSold)
receivedPremium = checkedOrder['typeAdjustedPrice'] * amountToSell
if existing:
if amountToBuyBack != amountToSell:
# custom order, price is not per contract
receivedPremium = checkedOrder['typeAdjustedPrice']
alert.alert(asset, 'Bought back ' + str(amountToBuyBack) + 'x ' + existing['optionSymbol'] + ' and sold ' + str(amountToSell) + 'x ' +
new['contract']['symbol'] + ' for ' + str(receivedPremium))
else:
alert.alert(asset, 'Sold ' + str(amountToSell) + 'x ' + new['contract']['symbol'] + ' for ' + str(receivedPremium))
if partialContractsSold > 0:
amountHasSold = partialContractsSold + amountToSell
receivedPremium = receivedPremium + checkedOrder['typeAdjustedPrice'] * partialContractsSold
# shouldn't happen
if amountHasSold != configuration[asset]['amountOfHundreds']:
return alert.botFailed(asset, 'Unexpected amount of contracts sold: ' + str(amountHasSold))
else:
amountHasSold = amountToSell
soldOption = {
'stockSymbol': asset,
'optionSymbol': new['contract']['symbol'],
'expiration': new['date'],
'count': amountHasSold,
'strike': new['contract']['strike'],
'receivedPremium': receivedPremium
}
db = TinyDB(dbName)
db.remove(Query().stockSymbol == asset)
db.insert(soldOption)
db.close()
return soldOption