TRADEnosis™ — My Quest to Build a Mythical Stock Trading Platform
Chapter 2: Updating the chart with Alpaca and a little magic
This is the second chapter of a story sharing several of the things I learned while building a stock trading platform in Python code using Plotly Dash to generate a responsive front-end web app, and the recently released Alpaca Markets v2 API for price data and trade executions.
A seasoned developer would likely have used React or NodeJS. Had I done so, this task might not have felt so much like I was battling dragons on a quest for the Holy Grail. But, I had just spent two years learning Python and machine learning (a/k/a Artificial Intelligence) to implement my idea for a new trading strategy and didn’t want to climb yet another steep learning curve. Sometimes you just have to follow your heart, or that mysterious voice that speaks to you from the heavens. And sometimes you just need to add a little alliterative allegory to a tedious topic!
In the last chapter, we created a basic user interface for our clone of what eventually became a commercial offering, the TRADEnosis AI-powered stock trading platform. To help keep us oriented as this script gets longer, here’s a code “rollup” created using PyCharm, my preferred integrated development environment (IDE) for Python, using its handy# region
and # endregion
comment directives to collapse key code sections. The full code, so far, is in the prior chapter.
Keys to Success
Remember that DropDownMenuItem, essentially a button, in the navbar?dbc.DropdownMenuItem("Alpaca", id="btn-alpaca-settings", n_clicks=0)
? Let’s use it to trigger a modal for entering Alpaca API v2 access keys.
First, between the positions card and the main container, add a new layout variable using the following code. Notice we are dutifully following Bootstrap’s container → row → column structure within the body portion of the surrounding dbc.Modal
component. The two columns, each 12 column-widths so they fill an entire line, use an html.Div
to group a dbc.Label
and dbc.Input
component. Importantly, the inputs are configured to use local persistence
, meaning the entries will be saved in a browser cookie and thus not need to be re-entered every session. This works well for a paper account. For a funded live account, you might want to use something more secure, like Alpaca OAuth, which is beyond the current scope of this series.
# region LAYOUT - KEYS MODAL
alpaca_modal = html.Div(
[
dbc.Modal(
[
dbc.ModalHeader([
dbc.ModalTitle("Alpaca Access Keys"),
]),
dbc.ModalBody(
[
dbc.Container(
[
dbc.Row(
[
dbc.Col(
html.Div([
dbc.Label("Paper Account Key"),
dbc.Input(
id="alpaca-key-paper", type="text",
persistence=True, persistence_type='local',
className="mb-2"
),
]),
width=12
)
]
),
dbc.Row(
[
dbc.Col(
html.Div([
dbc.Label("Paper Secret Key"),
dbc.Input(
id="alpaca-secret-paper", type="text",
persistence=True, persistence_type='local',
className="mb-2"
),
]),
width=12
)
]
),
]
)
]
),
],
id="alpaca-modal",
is_open=False,
),
]
)
# endregion /alpaca keys modal
Next, add the alpaca_modal
variable to the main layout. Put it at the top of the container, as modals can act strangely if embedded deep within a layout. This dialog won’t show up until we take the next step.
# region LAYOUT — CONTAINER
# <-- Put the reference to our new modal here
app.layout = dbc.Container(
[
alpaca_modal,
Callbacks — A Bit of Merlin Magic
Now, we need a function to display the modal when we click our settings button. Add this code, called a Dash Callback, near the bottom of our script, below all the layout pieces and just above the “if…main” conditional. By convention, we write variable & property names with underscores and id names with dashes. Within the callback configuration, the part above the function that’s called a decorator, said references must be in quotes.
# region CALLBACK - ALPACA SETTINGS MODAL
@app.callback(
Output("alpaca-modal", "is_open"),
Input("btn-alpaca-settings", "n_clicks"),
State("alpaca-modal", "is_open"),
)
def toggle_alpaca_modal(n1, is_open):
if n1:
return not is_open
return is_open
# endregion /settings modal
The @app.callback(...)
decorator and the function it wraps, in this case def toggle_alpaca_modal(...)
, is the magic that adds interactivity to our otherwise-static web page. How you may ask? (I’ll assume, oh noble knight, you are are asking in the proper Old Brittonic dialect of King Authur’s time!)
Well, My Liege, an Output
argument specifies the component property that will get changed by the function’s return
statement. An Input
argument, which gets mapped to a function parameter (in the order in which it occurs), becomes a trigger to run the function. A State
argument, like an Input, also gets mapped to a function parameter, but changes to that State argument, say by another callback or a user’s action, won’t trigger the function. Think of State as read-only.
Virtually any aspect of a Dash component, with a similar bit of Merlin-like callback magic, can be modified interactively. In this example, clicking the button with the id btn-alpaca-settings
sends a click message within the browser DOM that will trigger our callback to toggle the is_open
parameter and show our new modal.
Now that we have the layout and callback, try running our app and clicking the Settings button. You should see our new dialog. After creating and logging into your Alpaca.markets account, you can generate these keys on the Alpaca Trading Dashboard by following this helpful article. Start with a paper account and go ahead and enter your keys now as we’ll need them for the next step.
Enough! Draw forth Excalibur, and let’s get that chart working!
Finally, we are ready to add Alpaca to our armaments, which we’ll first use to collect stock price data for our chart component. In late 2022, Alpaca updated their Python SDK to version 2. Unfortunately, their documentation for this new version is spare, and you’ll find few working examples elsewhere.
First, we need to import the packaged Alpaca classes. We might not use all of these, but at least you now have them all in one place; well, there’s a few more in the documentation if you need crypto prices. Add these at the top of our long script.
from datetime import datetime, timedelta
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest, StockLatestQuoteRequest
from alpaca.data.timeframe import TimeFrame
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
MarketOrderRequest,
StopOrderRequest,
LimitOrderRequest,
StopLimitOrderRequest,
TrailingStopOrderRequest,
TakeProfitRequest,
StopLossRequest,
GetOrdersRequest,
GetOrderByIdRequest,
ReplaceOrderRequest,
ClosePositionRequest,
GetAssetsRequest,
GetPortfolioHistoryRequest,
CancelOrderResponse,
GetCalendarRequest
)
from alpaca.trading.enums import (
OrderSide,
TimeInForce,
AssetClass,
QueryOrderStatus,
OrderClass,
)
from alpaca.common.exceptions import APIError
When a user clicks on a stock ticker, we’ll load the price data (Open, High, Low, Close) into our candlestick chart. But, first we need to assign an id to the table; add the id=’watchlist-table’
to the watchlist
layout variable.
# region LAYOUT - WATCHLIST
watchlist = dash_table.DataTable(
id='watchlist-table', # <-- Add this reference
...
# endregion /watchlist
Now, we need a callback function, triggered by the user selecting a table row. Add this code below the modal callback we added previously. Notice that we have an Output argument directing our callback to return ‘seriesData’
to our chart (id=‘stock-chart’
), an Input argument that maps the user’s selection (‘active_cell’
) to the first parameter in the function, and three State arguments, which are also sequentially mapped to function parameters. The first is the data parameter from our table of stock symbols, and the other two are the keys that we just entered in our new modal.
I’m using State arguments here rather than Input as I don’t want the function to run when the keys are entered or when symbols are added to the table’s data, as there would be no table selection yet. Importantly, Dash runs all callbacks when it is first started, which would cause an error if I had not added the if…None statement; PreventUpdate (one of the Dash packages we imported in the last chapter) tells Dash to disregard the callback, in this case, when there has been no user selection yet.
# region CALLBACK - LOAD CHART DATA
@callback(
Output('stock-chart', 'seriesData'),
Input('watchlist-table', 'active_cell'),
State('watchlist-table', 'data'),
State('alpaca-key-paper', 'value'),
State('alpaca-secret-paper', 'value'),
)
def handle_watchlist_selection(active_cell, table_data, paper_key, paper_secret):
# Do nothing if no selection
if active_cell is None:
raise PreventUpdate
# Get the ticker using the selected row's index
selected_row_index = active_cell["row"]
symbol = table_data[selected_row_index]['TICKER']
# Call function to retrieve bars data and return as a chart series
return retrieve_eod_bars_for_single_stock_ticker(symbol, paper_key, paper_secret)
# endregion /load chart cb
After extracting the symbol from the table’s data object, we call a function to load price-bar data from Alpaca for that symbol, and also provide those all-important keys from our modal. Here’s the code for that function, which you should add between the setup and layout sections.
# region FUNCTIONS
def retrieve_eod_bars_for_single_stock_ticker(ticker: str, key: str, secret: str) -> list[dict]:
""" Load end-of-data data from Alpaca for a SINGLE STOCK ticker.
:param ticker A single stock ticker, ex. 'AAPL'
:param key: Alpaca access key.
:param secret: Alpaca secret key.
:return: A list of dictionaries, one per symbol per day, each with the keys:
'symbol', 'open', 'high', 'low', close', 'volume',
'trade_count', 'vwap' and 'time' in ISO-8601 format (YYYY-MM-DD HH:MM:SS).
"""
# Configure the request
alpaca_eod_request = StockBarsRequest(
symbol_or_symbols=[ticker],
timeframe=TimeFrame.Day,
start=datetime(2017, 1, 1), # IEX bars available from mid-2020, SIP from early-2017
# limit=5000,
feed= "sip",
adjustment=None,
)
# Configure the client and fetch the request
try:
alpaca_historical_client = StockHistoricalDataClient(key, secret)
bars = alpaca_historical_client.get_stock_bars(alpaca_eod_request)
df_ohlc = bars.df # multi-index by symbol & UTC timestamp
df_ohlc = df_ohlc.reset_index()
# Filter to get only the selected symbol
df_ohlc = df_ohlc[df_ohlc['symbol'] == ticker].copy()
# Convert to Eastern timezone and ISO-8601
df_ohlc['timestamp'] = df_ohlc['timestamp'].dt.tz_convert('US/Eastern')
df_ohlc['time'] = df_ohlc['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
df_ohlc = df_ohlc.drop(columns='timestamp')
# print(df_ohlc.head().to_dict('records'))
return [df_ohlc.to_dict('records')]
except APIError as n:
print("APIError while trying to load EOD data: ", n)
return []
except KeyError as n:
print(f"KeyError while trying to load possibly-invalid {ticker}:", n)
return []
except ValueError as n:
print("ValueError while trying to load EOD data:", n)
return []
Remember, we are using the version 2 Python SDK. Be careful that you don’t mix version 2 and the superseded, now-obsolete version 1 code. Version 2 is class-based, so the first thing we have to do is instantiate and configure StockBarsRequest
class. The request can be for multiple symbols at one time, so you have to put the ticker/symbol variable into a list.
The StockBarsRequest()
Alpaca has about 7 years of historic stock data from the Securities Information Processors organization (feed=“sip”
) , and about 5 years from Investors Exchange (feed=“iex”
). The SIP organization consolidates data from all major exchanges; that’s important as a stock can trade on multiple exchanges at somewhat different prices which SIP somehow reconciles into a single quote. Conversely, IEX is a small exchange that competes with NYSE. Because it has vastly fewer trade executions, the spreads between bid and ask price can be considerably wider than what is on SIP, and even the price bars may vary from SIP as well.
Given this difference, why would we ever want to use IEX as our data source? Well, the main reason is that IEX data is often available for free, even as a realtime data feed, while SIP realtime data is very expensive. Alpaca charges $99/month for realtime SIP data while other vendors may charge even more, particularly if they have ultralow latency feeds. However, Alpaca will give us 15-minute delayed SIP data for free, and that is fine for daily bars, what we call end-of-day data because it was historically published in newspapers after market hours. All that said, we’ll use the delayed SIP feed to get 7 years of data, as this request will only give us bars through the last market close anyway.
Alpaca can provide prices, as actually reported by SIP/IEX (adjustment=None
), or prices that have been adjusted for each stock split (adjustment=“split”
) or dividend (adjustment=“dividend”
) or both (adjustment=“all”
) since the start date of the request. Which one you should use depends on how you intend to use the data. Use unadjusted prices if you are using the price bars to make a purchase decision on the current bar. If instead, you are using the price bars to evaluate past-performance of a trading system you should use adjusted data. Algorithmic trading, say using a calculated indicator or a machine learning algorithm, is adversely impacted by the non-routine price changes caused by stock splits and dividend payments, potentially resulting in an inaccurate or biased buy/sell signal.
The StockHistoricalDataClient and preparing the data
The StockHistoricalDataClient
class is used to configure a client to execute the request. Here’s where we need those Alpaca keys that we entered in the modal and passed to this function. If one or both keys are missing, we’ll get a ValueError with a “you must supply a method of authentication” message. If the keys are invalid, we will get an APIError with a “forbidden” message. If the stock symbol is invalid, Alpaca will return nothing and, when the code attempts to extract the symbol from the response, we’ll get a KeyError from Pandas with a message that the symbol (or timestamp) is not in the dataFrame. Note where I therefore wrapped the request-response-extraction code in a try/except block to capture these potential errors. I just printed the error messages to the console. Later, you might want to add a layout location and return the messages there as a second output of this callback.
To execute the request, we use it as a parameter in the client’s get_stock_bars()
method. In similar fashion, you can alternatively execute a StockQuotesRequest()
, StockTradesRequest()
, StockLatestQuoteRequest()
, StockLatestTradeRequest()
,or aStockSnapshotRequest()
. Refer to the Alpaca Python API v2 documentation for the proper parameters and responses, and be sure to import the necessary package.
Breaking Camp
A few housekeeping tasks remain. In the SETUP code block, you can now delete the test_data
variable. Also, correct the symbol for ‘NVDA’ in the stock_list
variable as I had entered ‘NVDIA’ to test the try/except block. At least that is what I will tell the King if any of you knaves tell him otherwise!
# region SETUP
stock_list = ["AAPL", "MSFT", "GOOG", "TSLA", "NVDA", "AMZN", "META"] # Correct NVDIA to NVDA after testing!
df_watchlist = pd.DataFrame(data={"TICKER": stock_list})
test_data = [
{'symbol': 'GOOG', 'open': 37.36, 'high': 38.45, 'low': 37.34, 'close': 38.35, 'volume': 42692420.0,
'trade_count': 35176.0, 'vwap': 38.09, 'time': '2015-12-01 00:00:00'},
{'symbol': 'GOOG', 'open': 38.45, 'high': 38.8, 'low': 37.95, 'close': 38.12,
'volume': 44607460.0, 'trade_count': 34959.0, 'vwap': 38.34, 'time': '2015-12-02 00:00:00'},
{'symbol': 'GOOG', 'open': 38.3, 'high': 38.45, 'low': 37.28, 'close': 37.63,
'volume': 51812820.0, 'trade_count': 38195.0, 'vwap': 37.77, 'time': '2015-12-03 00:00:00'},
{'symbol': 'GOOG', 'open': 37.66, 'high': 38.42, 'low': 37.5, 'close': 38.34, 'volume': 55145660.0,
'trade_count': 33245.0, 'vwap': 38.2, 'time': '2015-12-04 00:00:00'},
{'symbol': 'GOOG', 'open': 38.39, 'high': 38.44, 'low': 37.75, 'close': 38.16, 'volume': 36246280.0,
'trade_count': 23534.0, 'vwap': 38.13, 'time': '2015-12-07 00:00:00'}
]
Our Journey So Far
With the changes we’ve made in Chapter 2, our app now dynamically loads data from Alpaca.Markets and displays a nice candlestick chart for the selected ticker. With the mouse (or your fingers once we get it hosted and accessible to a phone), one can expand/collapse the date or price axes, Also, on mouse-over, the cursor will show the close price (and corresponding date) of the underlying bar.
In the next chapter, we’ll face a far more difficult challenge! We’ll explore all the complicated brokerage rules, then create an order form that complies with those rules.
A Summary of our Adventure:
- In Chapter 1, we created the UI for a basic trading webpage using Dash.
- In this chapter, we created an Alpaca Settings dialog and added code to load candlestick price data into a stock chart.
- In Chapter 3, we’ll review Alpaca’s trading rules.
- In Chapter 4, we will creat the order form and associated code to compile trade instructions.
- In Chapter 5, we will write the callback needed to submit an order to Alpaca.
- In Chapter 6, we will compiled data for the Orders and Positions tabs.