Skip to content

Commit 250c8f0

Browse files
committed
A bunch of fixes
1 parent fdc6c22 commit 250c8f0

10 files changed

+676
-84
lines changed

bin/analyze_ccw.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#! /usr/bin/python3
2+
3+
import pandas as pd
4+
import argparse
5+
from screener_tools import read_json_file
6+
7+
DEFAULT_ANALYZER_CONFIG="./etc/analyzer.json"
8+
9+
COL_RANK="Score"
10+
COL_ROO="ROO Annualized(%)"
11+
COL_TOTAL_GAIN="Total Annualized(%)"
12+
COL_BETA="Beta"
13+
COL_DOWNSIDE="Downside Protection(%)"
14+
COL_DELTA="Delta"
15+
16+
WEIGHT_RANK="score_weight"
17+
WEIGHT_ROO="roo_weight"
18+
WEIGHT_TOTAL_GAIN="total_gain_weight"
19+
WEIGHT_BETA="beta_weight"
20+
WEIGHT_DOWNSIDE="downside_weight"
21+
WEIGHT_DELTA="delta_weight"
22+
23+
DEFAULT_WEIGHT_RANK=1.0
24+
DEFAULT_WEIGHT_ROO=2.0
25+
DEFAULT_WEIGHT_TOTAL_GAIN=2.0
26+
DEFAULT_WEIGHT_BETA=25
27+
DEFAULT_WEIGHT_DOWNSIDE=20.0
28+
DEFAULT_WEIGHT_DELTA=100.0
29+
30+
COL_RANK_SCORE="Rank Score"
31+
COL_RANK_ROO="ROO Score"
32+
COL_RANK_TOTAL_GAIN="Total Gain Score"
33+
COL_RANK_BETA="Beta Score"
34+
COL_RANK_DOWNSIDE="Downside Score"
35+
COL_RANK_DELTA="Delta Score"
36+
COL_RANK_OPTION="Total Option Score"
37+
38+
global GLOBAL_VERBOSE
39+
global GLOBAL_CONFIG
40+
41+
def main(results_csv,output_file):
42+
df = pd.read_csv(results_csv)
43+
44+
df[COL_RANK_SCORE] = calculate_simple_score(df[COL_RANK],GLOBAL_CONFIG.get(WEIGHT_RANK,DEFAULT_WEIGHT_RANK))
45+
df[COL_RANK_ROO] = calculate_simple_score(df[COL_ROO],GLOBAL_CONFIG.get(WEIGHT_ROO,DEFAULT_WEIGHT_ROO))
46+
df[COL_RANK_TOTAL_GAIN] = calculate_simple_score(df[COL_TOTAL_GAIN],GLOBAL_CONFIG.get(WEIGHT_TOTAL_GAIN,DEFAULT_WEIGHT_TOTAL_GAIN))
47+
df[COL_RANK_BETA] = calculate_beta_score(df[COL_BETA],GLOBAL_CONFIG.get(WEIGHT_BETA,DEFAULT_WEIGHT_BETA))
48+
df[COL_RANK_DOWNSIDE] = calculate_simple_score(df[COL_DOWNSIDE],GLOBAL_CONFIG.get(WEIGHT_DOWNSIDE,DEFAULT_WEIGHT_DOWNSIDE))
49+
df[COL_RANK_DELTA] = calculate_simple_score(df[COL_DELTA],GLOBAL_CONFIG.get(WEIGHT_DELTA,DEFAULT_WEIGHT_DELTA))
50+
51+
df[COL_RANK_OPTION] = (df[COL_RANK_SCORE] + df[COL_RANK_ROO] + df[COL_RANK_TOTAL_GAIN] + df[COL_RANK_BETA] + df[COL_RANK_DOWNSIDE] + df[COL_RANK_DELTA] ) / 6
52+
53+
print(df[COL_RANK_OPTION])
54+
55+
df.to_csv(output_file)
56+
57+
def calculate_simple_score(series,weight):
58+
return series * weight
59+
60+
def calculate_beta_score(series,weight):
61+
return (3 - series) * weight
62+
63+
def debug(msg):
64+
if GLOBAL_DEBUG:
65+
print(msg)
66+
67+
if __name__ == "__main__":
68+
# Setup the argument parsing
69+
parser = argparse.ArgumentParser()
70+
parser.add_argument('-c','--config-file', dest='config_file', help="analyzer configuration file", default=DEFAULT_ANALYZER_CONFIG)
71+
parser.add_argument('-r','--results', dest='results', required=True, help="CSV file with covered calls")
72+
parser.add_argument('-v','--verbose', dest='verbose', required=False,default=False,action='store_true',help="Increase verbosity")
73+
parser.add_argument('-d','--debug', dest='debug', required=False,default=False,action='store_true',help="Enable debugging")
74+
parser.add_argument('-o','--output-csv', dest='output', required=True, help="CSV file to write the output to")
75+
args = parser.parse_args()
76+
77+
GLOBAL_VERBOSE = args.verbose
78+
GLOBAL_DEBUG = args.debug
79+
80+
GLOBAL_CONFIG = read_json_file(args.config_file)
81+
82+
main(args.results,args.output)
83+

bin/ata.py

+2-59
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pandas_datareader import data as pdr
1010
from etrade_tools import *
11+
from screener_tools import *
1112
from stock_chart_tools.utils import get_historical_data, EMA, OBV, SSO, MACD
1213
from stock_chart_tools.utils import COLUMN_CLOSE, COLUMN_VOLUME, COLUMN_HIGH, COLUMN_LOW, MACD_DIVERGENCE, MACD_LABEL, OBV_LABEL, SS_K, SS_D
1314

@@ -32,17 +33,6 @@
3233
FRESHNESS_DAYS=1
3334
ONE_DAY = 24 * 60 * 60
3435

35-
CACHE_FRESHNESS_SECONDS=60*60 * 2
36-
37-
# Columns
38-
THREE_DAY_EMA="3dayEMA"
39-
FIVE_DAY_EMA="5dayEMA"
40-
NINE_DAY_EMA="9dayEMA"
41-
TWENTY_DAY_EMA="20dayEMA"
42-
HUNDRED_DAY_EMA="100dayEMA"
43-
VOL_THREE_DAY="Vol3DayEMA"
44-
VOL_TWENTY_DAY="Vol20DayEMA"
45-
4636
# Globals
4737
global GLOBAL_VERBOSE
4838

@@ -64,7 +54,7 @@ def analyze_symbol(screener_config,questions,symbol):
6454
answer_file = get_answer_file(screener_config.get(CACHE_DIR),symbol)
6555
answers = get_all_answers_from_cache(answer_file)
6656

67-
price_data = get_one_year_data(symbol,screener_config.get(CACHE_DIR))
57+
price_data = get_two_year_data(symbol,screener_config.get(CACHE_DIR))
6858

6959
(value,timestamp) = is_price_uptrending(symbol,price_data,answers)
7060
(value,timestamp) = is_price_above_20dayEMA(symbol,price_data,answers)
@@ -83,53 +73,6 @@ def analyze_symbol(screener_config,questions,symbol):
8373

8474
cache_answers(answer_file,answers)
8575

86-
def get_cache_filename(symbol,cache_dir):
87-
return os.path.join(expanduser(cache_dir),f"{symbol}.year.cache")
88-
89-
def get_cached_historical_data(symbol,cache_dir):
90-
filename = get_cache_filename(symbol,cache_dir)
91-
try:
92-
print(f"trying to get the cached data")
93-
file_mtime = os.path.getmtime(filename)
94-
if (time.time() - file_mtime) < CACHE_FRESHNESS_SECONDS:
95-
data = pd.read_csv(filename,index_col=0)
96-
return data
97-
else:
98-
print(f"{filename} cache is not fresh enough")
99-
except Exception as e:
100-
debug(f"could not read cache {filename}: {e}")
101-
102-
return None
103-
104-
def cache_historical_data(symbol,cache_dir,price_data):
105-
filename = get_cache_filename(symbol,cache_dir)
106-
price_data.to_csv(filename)
107-
108-
def get_one_year_data(symbol,cache_dir):
109-
110-
# Try to get the price data from cache
111-
price_data = get_cached_historical_data(symbol,cache_dir)
112-
if price_data is not None:
113-
debug(f"{symbol} returning historical data from cache")
114-
return price_data
115-
116-
# Nothing fresh in the cache
117-
debug(f"{symbol} getting historical data from API")
118-
price_data = get_historical_data(symbol)
119-
120-
price_data[THREE_DAY_EMA] = EMA(price_data[COLUMN_CLOSE],3)
121-
price_data[FIVE_DAY_EMA] = EMA(price_data[COLUMN_CLOSE],5)
122-
price_data[NINE_DAY_EMA] = EMA(price_data[COLUMN_CLOSE],9)
123-
price_data[TWENTY_DAY_EMA] = EMA(price_data[COLUMN_CLOSE],20)
124-
price_data[HUNDRED_DAY_EMA] = EMA(price_data[COLUMN_CLOSE],100)
125-
126-
price_data[VOL_THREE_DAY] = EMA(price_data[COLUMN_VOLUME],3)
127-
price_data[VOL_TWENTY_DAY] = EMA(price_data[COLUMN_VOLUME],20)
128-
129-
cache_historical_data(symbol,cache_dir,price_data)
130-
131-
return price_data
132-
13376
def is_fresh(cached_answer):
13477

13578
# Check to see if "-f" was passed, if so, ignore freshness

bin/buy_stock.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#! /usr/bin/python3
2+
3+
import argparse
4+
import json
5+
from etrade_tools import *
6+
7+
DEFAULT_CONFIG_FILE="./etc/etrade.json"
8+
DEFAULT_ACCOUNT_NAME="Rollover IRA"
9+
10+
def main(config_file, account_name):
11+
acct = get_account_by_name(config_file, account_name)
12+
print(f"acct name={acct.get_display_name()} balance=${acct.get_balance():.2f}")
13+
14+
def get_account_by_name(config_file, account_name):
15+
accounts = get_account_list(config_file)
16+
return accounts.get_account_by_name(account_name)
17+
18+
if __name__ == "__main__":
19+
# Setup the argument parsing
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument('-c','--config-file', dest='config_file', help="etrade configuration file", default=DEFAULT_CONFIG_FILE)
22+
parser.add_argument('-a','--account', dest='account_name', required=False,default=DEFAULT_ACCOUNT_NAME,action='store_true',help="Account Name")
23+
args = parser.parse_args()
24+
main(args.config_file,args.account_name)
25+

bin/find_roll_outs.py

+73-15
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,22 @@
2828

2929
# How many days out to look for rollouts
3030
MAX_ROLLOUT_DAYS = 65
31+
MIN_ROLLOUT_DAYS = 1
3132

3233
# This is the highest strike price to consider
3334
MAX_STRIKE_FACTOR = 1.05
3435

35-
def main(config_file, symbol, existing_expiration, strike, verbose):
36+
def main(config_file, symbol, existing_expiration, strike, min_days, max_days, verbose):
3637

3738
quote = get_quote(config_file, symbol)
3839
price = quote.get_price()
3940
exdate = quote.get_exdate()
4041
dividend = quote.get_dividend()
4142

43+
intrinsic_value = price - strike
44+
if intrinsic_value < 0:
45+
intrinsic_value = 0
46+
4247
if price <= float(strike * (100 - PRICE_PROXIMITY)/100):
4348
print(f"{symbol}: price ${price:.2f} is more than {PRICE_PROXIMITY}% below strike {strike}")
4449
return
@@ -50,16 +55,25 @@ def main(config_file, symbol, existing_expiration, strike, verbose):
5055

5156
print(f"{symbol}: ${price:.2f}")
5257
print(f"{call_option.get_display_symbol()}")
58+
59+
ask = call_option.get_ask()
60+
time_value = ask - intrinsic_value
61+
tv_prct = 100 * (time_value / strike)
62+
63+
itm_prct = 100 * (intrinsic_value / strike)
64+
65+
print(f"Ask: {ask} (time value remaining=${time_value:.2f} / {tv_prct:.1f}%)")
66+
print(f"Current intrinsic value: ${intrinsic_value:.2f} ({itm_prct:.1f}%)")
5367
print(f"-----")
54-
ask = call_option.get_bid()
5568

5669
if ask == 0:
5770
print("ERROR: ask is 0.00, maybe the market will open soon?")
5871
return
5972

60-
option_chain_list = get_matching_option_chains(config_file, symbol, existing_expiration, MAX_ROLLOUT_DAYS)
73+
option_chain_list = get_matching_option_chains(config_file, symbol, existing_expiration, min_days, max_days)
6174
for option_chain in option_chain_list:
62-
roll_out_days = option_chain.get_expiration() - existing_expiration
75+
#roll_out_days = option_chain.get_expiration() - existing_expiration
76+
roll_out_days = option_chain.get_expiration() - datetime.datetime.now()
6377
risky_exdate = False
6478
div_ex_date_from_expiration = option_chain.get_expiration() - exdate
6579
if 0 < div_ex_date_from_expiration.days < EXDATE_THRESHOLD:
@@ -74,34 +88,76 @@ def main(config_file, symbol, existing_expiration, strike, verbose):
7488
if bid < ask:
7589
# Skip this, it would end with a debit
7690
continue
91+
7792
credit = bid - ask
7893
buy_up = strike_price - strike
7994

8095
total_gain = credit + buy_up
81-
prct = 100 * (total_gain / strike)
82-
bpd = 100 * (prct / roll_out_days.days)
96+
total_prct = 100 * (total_gain / strike)
97+
bpd = 100 * (total_prct / roll_out_days.days)
8398

8499
buy_up_prct = 100 * (buy_up/strike)
85-
buy_up_score = (100 * BUY_UP_SCORE_FACTOR * buy_up_prct) / (roll_out_days.days ** 2)
100+
credit_prct = 100 * (credit/strike)
101+
102+
#buy_up_score = (100 * BUY_UP_SCORE_FACTOR * buy_up_prct) / (roll_out_days.days ** 2)
103+
#credit_score = (100 * CREDIT_FACTOR * credit ) / (roll_out_days.days ** 2)
86104

87-
credit_score = (100 * CREDIT_FACTOR * credit ) / (roll_out_days.days ** 2)
105+
(total_score) = get_scores(buy_up_prct, credit_prct, roll_out_days.days, risky_exdate)
88106

89-
total_score = credit_score + buy_up_score + bpd
90-
if risky_exdate:
91-
total_score = total_score * RISKY_EXDATE_FACTOR
107+
print(f"{call.get_display_symbol():28}: credit=${credit:5.2f}({credit_prct:4.2f}%) buy_up=${buy_up:5.2f}({buy_up_prct:4.2f}%) total={total_prct:5.2f}% days={roll_out_days.days:2d} bpd={bpd:5.2f} exdate_risk={risky_exdate} score={total_score:5.2f}")
92108

93-
print(f"{call.get_display_symbol()}: credit=${credit:5.2f} buy_up=${buy_up:5.2f} gain={prct:5.2f}% days={roll_out_days.days:2d} bpd={bpd:5.2f} c_score={credit_score:5.2f} b_score={buy_up_score:5.2f} exdate_risk={risky_exdate} score={total_score:5.2f}")
109+
def get_scores(buy_up_prct, credit_prct, num_days, risky_exdate):
110+
# What makes a good score?
111+
# The best score has a good credit, and a roll up, is soon not later than the next monthly expiration
112+
# Points:
113+
# buy up score: multiply by 100 (on the order of 0.5 to 4.0)
114+
# credit score: multiply by 100 (on the order of 0.5 to 4.0)
115+
# credit score > buy up score: if buy up == 0, add 1 else divide credit by buy up (0.5 to 2.0)
116+
# Risky ex-date: subtract 2
117+
# Date: (10 - (days / 7) )/ 5 (0.00 to 1.8)
94118

119+
# What makes a bad score?
120+
# Too much buy up, not enough credit
121+
# Has a risky exdate
122+
123+
# Add credit and buy up
124+
#print(f"credit prct = {credit_prct:.2f}")
125+
#print(f"buy_up = {buy_up_prct:.2f}")
126+
127+
score = (credit_prct + buy_up_prct)
128+
129+
# Add the credit preference
130+
credit_preference = 0
131+
if buy_up_prct > 0:
132+
133+
if buy_up_prct > credit_prct:
134+
credit_preference = credit_prct
135+
else:
136+
credit_preference = (credit_prct) + (buy_up_prct)
137+
138+
# print(f"credit prefernece = {credit_preference:.2f}")
139+
score += credit_preference
140+
141+
duration_factor = (10 - ((num_days / 7)*3))
142+
score += duration_factor
143+
144+
exdate_factor = 0
145+
if risky_exdate:
146+
exdate_factor = -2
147+
148+
score += exdate_factor
149+
150+
return score
95151

96-
def get_matching_option_chains(config_file, symbol, existing_expiration, MAX_ROLLOUT_DAYS):
152+
def get_matching_option_chains(config_file, symbol, existing_expiration, min_days, max_days):
97153
option_chain_list = list()
98154
dates = get_expiration_dates(config_file, symbol)
99155

100156
for (expiration_date, expiration_type) in dates:
101157
if expiration_date > existing_expiration:
102158
elapsed = expiration_date - existing_expiration
103159
days = elapsed.days
104-
if days < MAX_ROLLOUT_DAYS:
160+
if min_days < days < max_days:
105161
option_chain = get_option_chain(config_file, symbol, expiration_date)
106162
if option_chain:
107163
option_chain_list.append(option_chain)
@@ -131,11 +187,13 @@ def get_expiration_dates(config_file, symbol):
131187
parser.add_argument('-s','--symbol', dest='symbol', required=True,help="Symbol of the call" )
132188
parser.add_argument('-e','--expiration', dest='expiration', required=True,help="Symbol to search" )
133189
parser.add_argument('-p','--strike-price', dest='strike', required=True,help="Strike price" )
190+
parser.add_argument('--min-days', dest='min_days', required=False, default=MIN_ROLLOUT_DAYS,help="Minimum number of days until expiration" )
191+
parser.add_argument('--max-days', dest='max_days', required=False, default=MAX_ROLLOUT_DAYS,help="Maximum number of days until expiration" )
134192
parser.add_argument('-v','--verbose', dest='verbose', required=False,default=False,action='store_true',help="Increase verbosity")
135193
args = parser.parse_args()
136194

137195
if args.expiration is not None:
138196
(y,m,d) = args.expiration.split("-")
139197
expiration = datetime.datetime(year=int(y),month=int(m), day=int(d))
140198

141-
main(args.config_file, args.symbol, expiration, float(args.strike), args.verbose)
199+
main(args.config_file, args.symbol, expiration, float(args.strike), int(args.min_days), int(args.max_days), args.verbose)

bin/get_account.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#! /usr/bin/python3
2+
3+
import argparse
4+
import json
5+
from etrade_tools import *
6+
7+
DEFAULT_CONFIG_FILE="./etc/etrade.json"
8+
9+
def main(config_file, verbose):
10+
accounts = get_account_list(config_file)
11+
rollover = accounts.get_account_by_name("Rollover IRA")
12+
if rollover:
13+
print(f"Account {rollover.get_name()}")
14+
15+
for p in rollover.get_positions():
16+
quantity = p.get_quantity()
17+
print(f"{p.get_display_name()} quantity={quantity}")
18+
19+
print(f"Cash: ${rollover.get_balance():.2f}")
20+
21+
22+
23+
24+
if __name__ == "__main__":
25+
# Setup the argument parsing
26+
parser = argparse.ArgumentParser()
27+
parser.add_argument('-c','--config-file', dest='config_file', help="etrade configuration file", default=DEFAULT_CONFIG_FILE)
28+
parser.add_argument('-v','--verbose', dest='verbose', required=False,default=False,action='store_true',help="Increase verbosity")
29+
args = parser.parse_args()
30+
main(args.config_file,args.verbose)
31+

0 commit comments

Comments
 (0)