Skip to content

Commit

Permalink
Merge pull request #45 from dchrostowski/overhaul/refactor
Browse files Browse the repository at this point in the history
Overhaul/refactor
  • Loading branch information
dchrostowski authored Mar 7, 2024
2 parents ec54235 + 13c503c commit 725d23a
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 503 deletions.
156 changes: 144 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
## Description
A simple Python API for Investopedia's stock simulator games. I initially released this project around 2018/2019, but since then Investopedia made some significant changes which rendered this API useless. I'm currently working on overhauling it in 2024 and have some functionality restored.
A simple Python API for Investopedia's stock simulator games.

## Features
Currently you can programmatically:
* Read all positions in your stock, and short portfolios and get quotes for each position
* Fetch and cancel pending/open trades


Coming soon:
* Buy/Sell long positions
* Short sell/cover short positions
* Buy/sell options
Expand Down Expand Up @@ -50,9 +47,11 @@ Finally, try running the provided `example.py` file. This `example.py` file is
## Example
### code
```
from investopedia_api import InvestopediaApi, TradeExceedsMaxSharesException
from investopedia_api import InvestopediaApi
import json
import datetime
from datetime import datetime, timedelta
from api_models import OptionScope
from trade_common import OrderLimit, TransactionType, Expiration, StockTrade, OptionTrade
credentials = {}
with open('credentials.json') as ifh:
Expand All @@ -70,28 +69,31 @@ print("Buying Power: %s" % p.buying_power)
print("Annual Return Percent: %s" % p.annual_return_pct)
print("-------------------------------------------------")
print("\nOpen Orders:")
# To cancel a pending trade, run open_order.cancel()
for open_order in p.open_orders:
print("-------------------------------------------------")
print("Trade Type: %s" % open_order.trade_type)
print("Symbol: %s" % open_order.symbol)
print("Quantity: %s" % open_order.quantity)
print("Price: %s" % open_order.order_price)
print("-------------------------------------------------")
print("-------------------------------------------------")
stock_portfolio = p.stock_portfolio
short_portfolio = p.short_portfolio
option_portfolio = p.option_portfolio
print("\nStock Portfolio Details:")
print("-------------------------------------------------")
print("Market Value: %s" % stock_portfolio.market_value)
print("Today's Gain: %s (%s%%)" % (stock_portfolio.day_gain_dollar, stock_portfolio.day_gain_percent))
print("Total Gain: %s (%s%%)" % (stock_portfolio.total_gain_dollar, stock_portfolio.total_gain_percent))
print("Market Value: %s" % p.stock_portfolio.market_value)
print("Today's Gain: %s (%s%%)" % (p.stock_portfolio.day_gain_dollar, p.stock_portfolio.day_gain_percent))
print("Total Gain: %s (%s%%)" % (p.stock_portfolio.total_gain_dollar, p.stock_portfolio.total_gain_percent))
print("-------------------------------------------------")
print("\nLong Positions:")
for position in stock_portfolio:
for position in p.stock_portfolio:
print("-------------------------------------------------")
print("Company: %s (%s)" % (position.description, position.symbol))
print("Shares: %s" % position.quantity)
Expand All @@ -111,7 +113,7 @@ for position in stock_portfolio:
print("\nShort Positions:")
for position in short_portfolio:
for position in p.short_portfolio:
print("-------------------------------------------------")
print("Company: %s (%s)" % (position.description, position.symbol))
print("Shares: %s" % position.quantity)
Expand All @@ -129,4 +131,134 @@ for position in short_portfolio:
print("\t------------------------------")
print("-------------------------------------------------")
print("\nOption Positions:")
for position in p.option_portfolio:
print("-------------------------------------------------")
print("Company: %s (%s)" % (position.description, position.underlying_symbol))
print("Symbol: %s" % position.symbol)
print("Contracts: %s" % position.quantity)
print("Purchase Price: %s" % position.purchase_price)
print("Current Price: %s" % position.current_price)
print("Today's Gain: %s (%s%%)" % (position.day_gain_dollar, position.day_gain_percent))
print("Total Gain: %s (%s%%)" % (position.total_gain_dollar, position.total_gain_percent))
print("Market/Total Value: %s" % position.market_value)
print("\t------------------------------")
print("\tQuote")
print("\t------------------------------")
quote = position.quote
for k,v in quote.__dict__.items():
print("\t%s: %s" % (k,v))
print("\t------------------------------")
print("-------------------------------------------------")
# Make a stock trade
# Buy 2 shares of GOOG with limit $100 and no expiration
tt1 = TransactionType.BUY
ol1 = OrderLimit.LIMIT(100)
exp1 = Expiration.GOOD_UNTIL_CANCELLED()
trade1 = StockTrade(portfolio_id=p.portfolio_id, symbol="GOOG", quantity=2, transaction_type=tt1, order_limit=ol1, expiration=exp1)
trade1.validate()
trade1.execute()
# Buy 3 shares of AAPL at market value with expiration set to end of day
# defaults order_limit to OrderLimit.MARKET() and expiration to Expiration.END_OF_DAY())
trade2 = StockTrade(portfolio_id=p.portfolio_id, symbol='AAPL', quantity=3, transaction_type=TransactionType.BUY)
trade2.validate()
trade2.execute()
# short sell 1 share of AMZN
trade3 = StockTrade(portfolio_id=p.portfolio_id, symbol='AMZN', quantity=1, transaction_type=TransactionType.SELL_SHORT)
trade3.validate()
trade3.execute()
client.refresh_portfolio()
p = client.portfolio
for open_order in p.open_orders:
if open_order.symbol == 'GOOG' and open_order.quantity == 2:
# cancel GOOG trade
open_order.cancel()
if open_order.symbol == 'AAPL' and open_order.quantity == 3:
# cancel AAPL trade
open_order.cancel()
if open_order.symbol == 'AMZN' and open_order.quantity == 1:
# cancel AMZN trade
open_order.cancel()
stock_portfolio = p.stock_portfolio
if len(p.stock_portfolio) > 0:
# first long position in portfolio
first_long_position = p.stock_portfolio[0]
symbol = first_long_position.symbol
quantity = first_long_position.quantity
# execute trade to sell position in portfolio
first_long_position.sell()
client.refresh_portfolio()
p = client.portfolio
for oo in p.open_orders:
if oo.symbol == symbol and oo.quantity == quantity:
# cancel trade to sell first position in portfolio
oo.cancel()
short_portfolio = p.short_portfolio
if len(p.short_portfolio) > 0:
# first short position in portfolio
first_short_position = p.short_portfolio[0]
symbol = first_short_position.symbol
quantity = first_short_position.quantity
# execute trade to cover position in portfolio
first_short_position.cover()
client.refresh_portfolio()
p = client.portfolio
for oo in p.open_orders:
# cancel cover trade you just made
if oo.symbol == symbol and oo.quantity == quantity:
# cancel trade to cover first position in portfolio
oo.cancel()
if len(p.option_portfolio) > 0:
first_option_contract = p.option_portfolio[0]
symbol = first_option_contract.symbol
quantity = first_option_contract.quantity
# close out first option contract in portfolio
first_option_contract.close()
client.refresh_portfolio()
p = client.portfolio
for oo in p.open_orders:
# cancel order to close out contract
if oo.symbol == symbol and oo.quantity == quantity:
oo.cancel()
# Gets all available option contracts for AAPL
oc = client.get_option_chain('AAPL')
all_options = oc.all()
print("There are %s available option contracts for AAPL" % len(all_options))
two_weeks_from_today = datetime.now() + timedelta(days=14)
print("AAPL in-the-money put options expiring within two weeks:")
put_options_near_expiration_itm = oc.search(before=two_weeks_from_today, puts=True, calls=False, scope=OptionScope.IN_THE_MONEY)
for option in put_options_near_expiration_itm:
print("%s:\n\tbid: %s\n\task: %s\n\tlast price: %s\n\texpires:%s" % (option.symbol, option.bid, option.ask, option.last, option.expiration.strftime("%m/%d/%Y") ))
option_to_buy = put_options_near_expiration_itm[0]
trade4 = OptionTrade(portfolio_id=p.portfolio_id, symbol=option_to_buy.symbol, quantity=1, transaction_type=TransactionType.BUY)
trade4.validate()
trade4.execute()
client.refresh_portfolio()
p = client.portfolio
for oo in p.open_orders:
if oo.symbol == option_to_buy.symbol:
oo.cancel()
```
142 changes: 122 additions & 20 deletions api_models.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
from constants import *
from session_singleton import Session
from utils import UrlHelper
from itertools import chain
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal

from utils import subclass_method, coerce_method_params, date_regex
from utils import subclass_method, coerce_method_params
from trade_common import StockTrade, TransactionType, OptionTrade


from queries import Queries
from session_singleton import Session
from constants import API_URL
import json

class InvalidSecurityTypeException(Exception):
pass


class OpenOrder(object):
@coerce_method_params
def __init__(
Expand All @@ -27,8 +24,6 @@ def __init__(
):
self.order_id = order_id
self.cancel_fn = cancel_fn
# strptime with %-m/%-d/%Y %-I:%M:%S %p SHOULD WORK
# because it looks like this: 4/1/2019 11:10:35 PM
self.trade_type = trade_type
self.symbol = symbol
self.quantity = quantity
Expand All @@ -41,8 +36,6 @@ def cancel(self):
self.active = False
print("Order ID %s cancelled!" % self.order_id)



class SubPortfolio(object):
def __init__(self,portfolio_id,market_value,day_gain_dollar,day_gain_percent,total_gain_dollar,total_gain_percent):
self.portfolio_id = portfolio_id
Expand All @@ -61,8 +54,6 @@ def find(self,symbol):

if symbol.upper() == p.symbol:
return p



class Portfolio(object):
allowable_portfolios = {
Expand Down Expand Up @@ -122,11 +113,6 @@ def open_orders(self):
# def refresh(self):
# self = Parsers.generate_portfolio(self.portfolio_id,self.game_id,self.game_name)






class StockPortfolio(SubPortfolio, list):
def __init__(self, positions=[], **kwargs):
super().__init__(**kwargs)
Expand Down Expand Up @@ -319,3 +305,119 @@ def __init__(
self.last = self.ask
self.change = self.ask - self.previous_close
self.change_percent = round(self.change / self.last * 100,2)

class OptionScope(object):
IN_THE_MONEY = 'IN_THE_MONEY'
NEAR_THE_MONEY = 'NEAR_THE_MONEY'
OUT_OF_THE_MONEY = 'OUT_OF_THE_MONEY'
ALL = 'ALL'


class OptionChain(object):
def __init__(self,symbol):
self.expirations = []
self.options = {}
self.chain = {}
symbol = symbol.upper()

session = Session()
exp_resp = session.post(API_URL,data=Queries.option_expiration_dates(symbol))
exp_resp.raise_for_status()


exp_json = json.loads(exp_resp.text)
for expiration in exp_json['data']['readOptionsExpirationDates']['expirationDates']:
self.expirations.append(expiration)

for expiration in self.expirations:
self.chain[expiration] = {}
option_scopes = [OptionScope.IN_THE_MONEY, OptionScope.OUT_OF_THE_MONEY, OptionScope.NEAR_THE_MONEY]
for os in option_scopes:
self.chain[expiration][os] = {'calls': [], 'puts': []}
options_resp = session.post(API_URL, data=Queries.options_by_expiration(symbol,expiration,os))
options_resp.raise_for_status()
options = json.loads(options_resp.text)['data']['readStock']['options']
call_options = options['callOptions']['list']
put_options = options['putOptions']['list']
for co_kwargs in call_options:
co_kwargs['expiration'] = expiration
co_kwargs['is_put'] = False
call_option = OptionContract(**co_kwargs)
self.options[call_option.symbol] = call_option
self.chain[expiration][os]['calls'].append(call_option.symbol)

for po_kwargs in put_options:
po_kwargs['expiration'] = expiration
po_kwargs['is_put'] = True
put_option = OptionContract(**po_kwargs)
self.options[put_option.symbol] = put_option
self.chain[expiration][os]['puts'].append(put_option.symbol)

def search(
self,
after=datetime.now() - timedelta(days=365),
before=datetime.now() + timedelta(days=365),
scope=OptionScope.ALL,
calls=True,
puts=True
):

eligible_expirations = []
eligible_scopes = []
eligible_types = []


after_ts = after.timestamp()
before_ts = before.timestamp()


for exp in self.expirations:
if exp/1000 >= after_ts and exp/1000 <= before_ts:
eligible_expirations.append(exp)

if scope == OptionScope.ALL:
eligible_scopes = [OptionScope.IN_THE_MONEY, OptionScope.OUT_OF_THE_MONEY, OptionScope.NEAR_THE_MONEY]
else:
eligible_scopes = [scope]

if calls:
eligible_types.append('calls')

if puts:
eligible_types.append('puts')

filtered_options = []
for exp in eligible_expirations:
filtered_expirations = self.chain[exp]
for esc in eligible_scopes:
filtered_scopes = filtered_expirations[esc]
for ety in eligible_types:
filtered_option_symbols = filtered_scopes[ety]
for opt_symbol in filtered_option_symbols:
filtered_options.append(self.options[opt_symbol])

return filtered_options


def all(self):
return [v for v in self.options.values()]

def lookup_by_symbol(self,symbol):
return self.options.get(symbol,None)

class OptionContract(object):
def __init__(self,**kwargs):
self.symbol = kwargs['symbol']
self.strike_price = kwargs['strikePrice']
self.last = kwargs['lastPrice']
self.day_change = kwargs['dayChangePrice']
self.day_change_percent = kwargs['dayChangePercent']
self.day_low = kwargs['dayLowPrice']
self.day_high = kwargs['dayHighPrice']
self.bid = kwargs['bidPrice']
self.ask = kwargs['askPrice']
self.volume = kwargs['volume']
self.open_interest = kwargs['openInterest']
self.in_the_money = kwargs['isInTheMoney']
self.expiration = datetime.fromtimestamp(kwargs['expiration']/1000)
self.is_put = kwargs['is_put']
Loading

0 comments on commit 725d23a

Please sign in to comment.