forked from mattsta/tradeapis
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fees.py
173 lines (140 loc) · 5.77 KB
/
fees.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
from dataclasses import dataclass
from typing import Optional
def mn(val):
"""format numeric input as money"""
# We could use locale instead like:
# >>> import locale
# >>> locale.setlocale(locale.LC_ALL, '')
# >>> locale.currency(300000.22, grouping=True)
# but the format is faster and doesn't require extra "stuff"
# (locale benchmarks at 11 us per output;
# python format + replace benchmarks at 600 ns per output)
# If we eventually need additional currencies, just use the locale
# approach instead.
return f"${val:,.2f}".replace("$-", "-$")
@dataclass
class OptionFees:
# Note: we may not be doing proper rounding here.
# Brokers will round up each sub-calculation to the next cent, but
# here we're carrying through the fractional cents and letting
# python display whatever final truncated cent value it formats.
# Conditions (buys/sells) taken from: https://www.webull.com/pricing
# Fee structure taken from "Fee Schedule" link at:
# https://brokerage.tradier.com/pricing
# equivalent to a $22.10 fee per million dollars sold
# Update: fee lowered to $5.10 as of Feb 25, 2021
rate_sec: float = 0.00000510 # per dollar sold, min 0.01 per leg, SELLS ONLY
# this is an error in the tradier docs: it's 0.002 per option contract and
# TAF is 0.000119 per share for equity sales. Max is $5.95 per trade.
# (the robinhood docs have an error and says it's 0.002 per option SHARE which
# would be 100x more than the actual value of 0.002 per option CONTRACT)
rate_taf: float = 0.002 # per contract, min 0.01 per leg, SELLS ONLY
# Is tis also an error in the tradier docs?
# All other brokers show a 0.0388 fee while tradier shows 0.05.
# Let's go with the actual 0.0388 fee since it's probably what Apex is charging.
rate_orf: float = 0.0388 # per contract, all, no maximum
rate_occ: float = 0.045 # per contract, all, max $55.00 per leg
# Described as tuples (contractCount, contractPrice)
# note: 'contractPrice' is the contract price, not the total price
# (i.e. 2.30 instead of 230.00)
legs_buy: Optional[list[tuple[int, float]]] = None
legs_sell: Optional[list[tuple[int, float]]] = None
@staticmethod
def legsTwo(count, buy, sell):
return OptionFees(legs_buy=[(count, buy)], legs_sell=[(count, sell)])
@staticmethod
def legsFour(count, buy1, sell1, buy2, sell2):
return OptionFees(
legs_buy=[(count, buy1), (count, buy2)],
legs_sell=[(count, sell1), (count, sell2)],
)
@property
def buys(self):
"""Returns list for all buy legs: [(contractCounts), (contractPrices)]"""
return zip(*self.legs_buy)
@property
def sells(self):
"""Returns list for all sell legs: [(contractCounts), (contractPrices)]"""
return zip(*self.legs_sell)
def priceOf(self, legs):
"""Total dollar amount price for a leg.
`legs` is either `self.legs_buy` or `self.legs_sell`
"""
return sum([x[1] * 100 for x in legs])
@property
def sellPrice(self):
"""Total monetary inflow for the spread"""
return self.priceOf(self.legs_sell) * self.asells
@property
def buyPrice(self):
"""Total monetary outflow for the spread"""
return self.priceOf(self.legs_buy) * self.abuys
@property
def netPrice(self):
"""Total amount of spread sold or bought"""
return self.buyPrice - self.sellPrice
@property
def abuys(self):
"""Contract count for all buy legs"""
return sum([x[0] for x in self.legs_buy])
@property
def asells(self):
"""Contract count for all sell legs"""
return sum([x[0] for x in self.legs_sell])
@property
def contracts(self):
"""Contract count for all legs"""
return self.abuys + self.asells
def mathPerLeg(self, legs, evalUsing):
return sum(evalUsing(legContracts) for legContracts in legs)
@property
def sec(self):
# Sells based on total price only
# 0.01 floor PER LEG
# Calculated against the total dollar value of the sale,
# so we take the contract price, multiply for 100 shares, then multiply
# by total contracts in each sell leg.
# We run this per leg because each leg must charge a minimum 0.01 fee.
return self.mathPerLeg(
self.legs_sell,
lambda legContracts: max(
0.01, self.rate_sec * legContracts[1] * 100 * legContracts[0]
),
)
@property
def taf(self):
# Sells based on contract count only
# 0.01 floor PER LEG minimum
# 5.95 ceiling PER LEG maximum
return self.mathPerLeg(
self.legs_sell,
lambda legContracts: min(5.95, max(0.01, self.rate_taf * legContracts[0])),
)
@property
def orf(self):
# All contracts
# aggregate value across all legs, no per-leg math needed
return self.rate_orf * self.contracts
@property
def occ(self):
# All contracts
# $55.00 ceiling (1,222 contracts max fee) PER LEG
# https://www.theocc.com/Company-Information/Schedule-of-Fees
return self.mathPerLeg(
self.legs_sell + self.legs_buy,
lambda legContracts: min(55.00, self.rate_occ * legContracts[0]),
)
@property
def total(self):
return self.sec + self.taf + self.orf + self.occ
def __repr__(self):
fees = f"""BUY {self.abuys}; SELL {self.asells}; TOTAL {self.contracts}
Selling: {mn(self.sellPrice)}
Buying: {mn(self.buyPrice)}
Net: {mn(self.netPrice)}
SEC: {mn(self.sec)}
TAF: {mn(self.taf)}
ORF: {mn(self.orf)}
OCC: {mn(self.occ)}
Total estimate: {mn(self.total)}"""
return fees