TRADEnosis™ — My Quest to Build a Mythical Stock Trading Platform
Chapter 5: Submitting a Trade Order to Alpaca
As our journey has been long and you, my brave fellow knight, are sure to be weary, I’ll catapult right into our next challenge of writing the callback in Plotly Dash to submit a trade order to Alpaca Markets using their Python SDK version 2. If you are new to our quest to build a stock trading platform, or ponder my medieval parlance, start with Chapter 1.
During our exploits so far, we learned that Alpaca expects us to submit a minimum set of instructions for a basic trade, plus even more instructions for advanced multi-leg orders. Further, we learned of a complex set of rules that must be followed when submitting orders to a broker.
As a quick starting point, let’s add the imports that we’ll need in this chapter. I also removed the ones we won’t be using.
from typing import Union
import pandas as pd
import json
import pytz
from dash import Dash, dcc, html, callback, Input, Output, State, dash_table, no_update, ctx
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import dash_tvlwc
from datetime import datetime
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest, StockSnapshotRequest
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,
)
from alpaca.trading import (
OrderType,
OrderSide,
OrderClass,
)
from alpaca.common.exceptions import APIError
from pydantic import ValidationError
We’ve already created a layout to collect the necessary instructions, so let’s start by adding those fields to a new “COMPILE ORDERS” callback as our Input
arguments. For the Output
arguments (which must come first), we will reference an order-summary html component (where we will display compiled instructions for user review), the submit button, and an argument that will close the modal after submission. Additionally, as State
arguments, we must reference the Alpaca keys and our stored data being passed from other callbacks. As you can see, this is going to be a long and complex callback!
# region CALLBACK - COMPILE ORDERS
@callback(
# TODO: Add "disable_n_clicks=True" to non-btns in layout -- helps screen readers and reduces traffic
Output('order-summary', 'children'),
Output('submit-button', 'disabled'), # True or False
Output('order-status-store', 'data'), # "submitted" or ""
Output('orders-modal', 'is_open', allow_duplicate=True), # True or False
Output('order-toast', 'is_open'), # True or False
Output('order-toast', 'children'),
Input('account', 'value'), # 'paper' or 'live'
Input('order-side', 'value'), # 'buy' or 'sell'
Input('qty', 'value'),
Input('qty-icon', 'children'), # '$' or '%'
Input('tif', 'value'),
Input('limit-entry', 'value'),
Input('stop-entry', 'value'),
Input('trailing-stop', 'value'),
Input('trailing-icon', 'children'), # '$' or '%'
Input('profit-leg-limit', 'value'),
Input('loss-leg-limit', 'value'),
Input('loss-leg-stop', 'value'),
Input('oco_checked', 'value'),
Input('submit-button', 'n_clicks'),
State('selected-symbol-store', 'data'),
State("alpaca-key-paper", "value"),
State("alpaca-secret-paper", "value"),
State("alpaca-key-live", "value"),
State("alpaca-secret-live", "value"),
State('order-status-store', 'data'),
State('bid-price-store', 'data'),
State('ask-price-store', 'data'),
State('last-price-store', 'data'),
prevent_initial_call=True
)
def compile_order(
account, side, qty, qty_icon, tif, limit, stop, trailing_stop, trailing_icon,
profit_leg_limit, loss_leg_limit, loss_leg_stop, oco_checked, n_submit,
symbol, paper_key, paper_secret, live_key, live_secret, order_status, equity, buy_power,
cash, bid, ask, last):
""" Compile and validate parameters for a trade order, submit to Alpaca brokerage if user confirms. """
...
Next, we need to validate our inputs and enforce the trading rules. For that, we’ll use a series of conditional tests. If any test fails, our script will return a message to the order-summary
html component of our modal.
invalid = None
if not symbol: # Just in case, but shouldn't happen as Trade btn hidden until symbol selected
invalid = html.P("Symbol must be selected.", className="text-info")
if not account:
invalid = html.P("Account must be selected.", className="text-info")
if not side:
invalid = html.P("Side must be selected.", className="text-info")
# If you will be trading penny stocks (or modifying code for crypto), modify this to allow 4 digits if <$1
if limit is not None and (not isinstance(limit, (float, int)) or (limit * 100) % 1 != 0):
invalid = html.P("Limit price must have no more than two decimal places.", className="text-info")
if stop is not None and (not isinstance(stop, (float, int)) or (stop * 100) % 1 != 0):
invalid = html.P("Stop price must have no more than two decimal places.", className="text-info")
if not qty or not isinstance(qty, (float, int)):
invalid = html.P("Qty must be a non-blank number > zero.", className="text-info")
if tif == "ext" and not limit:
invalid = html.P("Extended-hours orders must be Limit.", className="text-info")
if tif == "ext" and (stop or trailing_stop or profit_leg_limit or loss_leg_stop or loss_leg_limit):
invalid = html.P("Only Limit orders are allowed to use Extended Hours.", className="text-info")
if tif == "opg" and (stop or trailing_stop or profit_leg_limit or loss_leg_stop or loss_leg_limit):
invalid = html.P("OPG orders must be Limit or Market.", className="text-info")
if tif == "cls" and (stop or trailing_stop or profit_leg_limit or loss_leg_stop or loss_leg_limit):
invalid = html.P("CLS orders must be Limit or Market.", className="text-info")
if qty and tif == "day" and qty_icon == "$" and \
(limit or stop or trailing_stop or profit_leg_limit or loss_leg_stop or loss_leg_limit):
invalid = html.P("Notional-Day orders can only be Market.", className="text-info")
if trailing_stop and (limit or stop):
invalid = html.P("Trailing-stop orders cannot have limit or stop entries.", className="text-info")
if oco_checked and (limit or stop or trailing_stop):
invalid = html.P("An OCO order cannot open a new entry position.", className="text-info")
if oco_checked and not qty:
invalid = html.P("An OCO order must have a qty (<= qty in an open position).", className="text-info")
if oco_checked and not profit_leg_limit:
invalid = html.P("An OCO order must have both legs, missing Take-Profit Limit.", className="text-info")
if oco_checked and not (loss_leg_limit or loss_leg_stop):
invalid = html.P("An OCO order must have both legs, missing Stop-Loss Limit and/or Stop.",
className="text-info")
if loss_leg_limit and not loss_leg_stop:
invalid = html.P("For the Stop-Loss leg, Alpaca only supports Stop or Stop-Limit, "
"not Limit alone, orders.", className="text-info")
if loss_leg_limit and loss_leg_stop and (side == "buy") and not (loss_leg_stop >= (loss_leg_limit + 0.01)):
invalid = html.P("In a sell-to-close Stop-Loss leg, the Stop Price must be at least $0.01 above"
" the Limit Price for that leg.", className="text-info")
if limit and loss_leg_stop and (side == "buy") and not (loss_leg_stop <= (limit + 0.01)):
invalid = html.P("In a sell-to-close Stop-Loss leg, the Stop Price must be at least $0.01 below"
" the Limit price for the entry order.", className="text-info")
if (bid != 0) and loss_leg_stop and (side == "buy") and not (loss_leg_stop <= (bid + 0.01)):
invalid = html.P("In a sell-to-close Stop-Loss leg, the Stop Price must be at least $0.01 below"
" the current Bid price for the entry order.", className="text-info")
if loss_leg_limit and loss_leg_stop and (side == "sell") and not (loss_leg_stop <= (loss_leg_limit + 0.01)):
invalid = html.P("In a buy-to-close Stop-Loss leg, the Stop Price must be at least $0.01 below"
" the Limit Price for that leg.", className="text-info")
if limit and loss_leg_stop and (side == "sell") and not (loss_leg_stop >= (limit + 0.01)):
invalid = html.P("In a buy-to-close Stop-Loss leg, the Stop Price must be at least $0.01 above "
"the Limit price for the entry order.", className="text-info")
if (ask != 0) and loss_leg_stop and (side == "sell") and not (loss_leg_stop <= (ask + 0.01)):
invalid = html.P("In a buy-to-close Stop-Loss leg, the Stop Price must be at least $0.01 below"
" the current Ask price.", className="text-info")
if profit_leg_limit and (side == "buy") and loss_leg_stop and not (profit_leg_limit >= (loss_leg_stop + 0.01)):
invalid = html.P("In a sell-to-close Take-Profit leg, the Limit Price must be at least $0.01 above"
" the Stop-Loss for Stop-Loss leg.", className="text-info")
if profit_leg_limit and (side == "buy") and stop and not (profit_leg_limit <= (stop + 0.01)):
invalid = html.P("In a sell-to-close Take-Profit leg, the Limit Price must be at least $0.01 below"
" the Stop Price for the entry leg.", className="text-info")
if profit_leg_limit and (side == "buy") and limit and not (profit_leg_limit >= (limit + 0.01)):
invalid = html.P("In a sell-to-close Take-Profit leg, the Limit Price must be at least $0.01 above"
" the Limit Price for the entry leg.", className="text-info")
if profit_leg_limit and (side == "buy") and (bid != 0) and not (profit_leg_limit >= (bid + 0.01)):
invalid = html.P("In a sell-to-close Take-Profit leg, the Limit Price must be at least $0.01 above"
" the current Bid price.", className="text-info")
if profit_leg_limit and (side == "sell") and loss_leg_stop and not (profit_leg_limit <= (loss_leg_stop + 0.01)):
invalid = html.P("In a buy-to-close Take-Profit leg, the Limit Price must be at least $0.01 below"
" the Stop Price for the Stop-Loss leg.", className="text-info")
if profit_leg_limit and (side == "sell") and stop and not (profit_leg_limit <= (stop + 0.01)):
invalid = html.P("In a buy-to-close Take-Profit leg, the Limit Price must be at least $0.01 below"
" the Stop Price for the entry leg.", className="text-info")
if profit_leg_limit and (side == "sell") and limit and not (profit_leg_limit <= (limit + 0.01)):
invalid = html.P("In a buy-to-close Take-Profit leg, the Limit Price must be at least $0.01 below"
" the Limit Price for the entry leg.", className="text-info")
if profit_leg_limit and (side == "sell") and (ask != 0) and not (profit_leg_limit <= (ask + 0.01)):
invalid = html.P("In a buy-to-close Take-Profit leg, the Limit Price must be at least $0.01 below"
" the current Ask price.", className="text-info")
if invalid:
# ord-summary, submit-btn (disabled), order-status, modal (is_open)
return invalid, True, "", True
...
Shortly, we will march down our list of input fields to compile a set of order instructions to append to this starting list, an order_params
dictionary.
order_params = {
'symbol': symbol,
'side': OrderSide.BUY if side == 'buy' else OrderSide.SELL,
'time_in_force': tif,
'order_id': None,
}
But first, Alpaca allows either qty (#shares) or a nominal ($USD) entry. Our qty
input from Chapter 4 had the option of selecting either # or $ so we need to handle those options. Also, from our rules discussion in Chapter 3, we learned that fractional shares (as well as notional values) are only allowed for Market-Day orders. We partially handle that here and then finish when we get to the Market-Day type.
# Set Qty Param
if qty_icon == "$": # notional, will confirm mkt-day below and override if not
order_params['notional'] = qty
num_shares = 0
num_shares_fractional = 0
qty_text = f"${qty:,.2f} of"
else: # qty_icon = '#' so entered as shares, possibly fractional which is handled below
num_shares = int(qty)
order_params['qty'] = num_shares
num_shares_fractional = qty # handing fractional-qty for mkt-day below, limited digits unless crypto
qty_text = f"{str(qty)} shares of"
Now, we can work through the 5 basic order types, adding to our order_params as needed for each type. Notice we are also deriving a how_traded
variable that we will use later in compiling order_summary
.
# Limit Order
if limit and not stop and not trailing_stop:
order_type = 'limit'
order_params['type'] = OrderType.LIMIT
order_params['limit_price'] = limit
order_params['extended_hours'] = True if tif == "ext" else False
order_params['time_in_force'] = "day" if tif == "ext" else tif
how_traded = f"at LIMIT ${limit:.2f} or better"
# Stop Order
elif stop and not limit and not trailing_stop:
order_type = 'stop'
order_params['type'] = OrderType.STOP
order_params['stop_price'] = stop
how_traded = f"at MARKET after ${stop:.2f} STOP crossed"
# Stop-Limit Order
elif stop and limit:
order_type = 'stop_limit'
order_params['type'] = OrderType.STOP_LIMIT
order_params['stop_price'] = stop
order_params['limit_price'] = limit
how_traded = f"at LIMIT ${limit:.2f} or better after ${stop:.2f} STOP crossed"
# Trailing-Stop Order
elif trailing_stop and not limit and not stop:
order_type = 'trailing_stop'
order_params['type'] = OrderType.TRAILING_STOP
if trailing_icon == "%":
order_params['trail_percent'] = trailing_stop
trailing_text = f"{trailing_stop}%"
else: # default $
order_params['trail_price'] = trailing_stop
trailing_text = f"${trailing_stop:,.2f}"
how_traded = f"at MARKET after a reversion of {trailing_text} " \
f"{'below peak price' if side == 'buy' else 'above trough price'} " \
f"as a TRAILING-STOP"
# Market Order - with potential notional
else:
order_type = 'market'
order_params['order_type'] = OrderType.MARKET
if tif == 'day' and qty_icon == "#": # Fractional entry allowed so over-write param
order_params['qty'] = num_shares_fractional # Float limited to 6 digits albeit Alpaca allows 9
how_traded = 'at MARKET'
Now we’ll handle the Advanced Order classes, which add exit legs to the basic-entry order types. Note that these are within a new if-else loop as they are optional. Also note where I’m using the oco_checked
variable to distinguish this close-legs-only class from the others.
# Profit-Leg OTO
if profit_leg_limit and not (loss_leg_limit or loss_leg_stop) and not oco_checked:
order_params['order_class'] = OrderClass.OTO
order_described = 'Take-Profit closing order once opening order is filled (OTO)'
profit_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"to TAKE PROFIT at ${profit_leg_limit:.2f} LIMIT or {'above' if side == 'buy' else 'below'}."
loss_line = ""
try:
order_params['take_profit'] = TakeProfitRequest(limit_price=profit_leg_limit)
except ValidationError as e:
return f"Invalid Order: {e}", True, "", True
# Loss-Leg OTO
if (loss_leg_limit or loss_leg_stop) and not profit_leg_limit and not oco_checked:
order_params['order_class'] = OrderClass.OTO
order_described = 'Stop-Loss closing order once opening order is filled (OTO)'
profit_line = ""
try:
if loss_leg_stop and not loss_leg_limit:
order_params['stop_loss'] = StopLossRequest(stop_price=loss_leg_stop)
loss_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"to STOP LOSS if ${loss_leg_stop:.2f} STOP " \
f"crossed {'below' if side == 'buy' else 'above'}."
else: # both limit and stop, Alpaca doesn't support stop_loss.limit_price alone!
order_params['stop_loss'] = StopLossRequest(limit_price=loss_leg_limit, stop_price=loss_leg_stop)
loss_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"at ${loss_leg_limit} LIMIT or better " \
f"to STOP LOSS if ${loss_leg_stop:.2f} STOP " \
f"crossed {'below' if side == 'buy' else 'above'}."
except ValidationError as e:
return f"Invalid Order: {e}", True, "", True
# Bracket Order (OTOCO) - opening order + 2 closing legs
if (loss_leg_limit or loss_leg_stop) and profit_leg_limit and not oco_checked:
order_params['order_class'] = OrderClass.BRACKET
order_described = "Closing orders once opening order is filled (Bracket OTO+OCO):"
profit_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"to TAKE PROFIT at ${profit_leg_limit:.2f} LIMIT or {'above' if side == 'buy' else 'below'}."
try:
order_params['take_profit'] = TakeProfitRequest(limit_price=profit_leg_limit)
if loss_leg_stop and not loss_leg_limit:
order_params['stop_loss'] = StopLossRequest(stop_price=loss_leg_stop)
loss_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"to STOP LOSS if ${loss_leg_stop:.2f} STOP " \
f"crossed {'below' if side == 'buy' else 'above'}."
else: # both limit and stop, Alpaca doesn't support stop_loss.limit_price alone!
order_params['stop_loss'] = StopLossRequest(limit_price=loss_leg_limit, stop_price=loss_leg_stop)
loss_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"at ${loss_leg_limit} LIMIT or better " \
f"to STOP LOSS if ${loss_leg_stop:.2f} STOP " \
f"crossed {'below' if side == 'buy' else 'above'}."
except ValidationError as e:
return f"Invalid Order: {e}", True, "", True
# OCO Order - closing legs only, no opening order. Side should be for the CLOSING order!!!
if (loss_leg_limit or loss_leg_stop) and profit_leg_limit and oco_checked:
order_params['order_class'] = OrderClass.OCO
try: # Note the change in side relative to other order types
profit_line = f"{'BUY' if side == 'buy' else 'SELL'} {num_shares} shares of {symbol} " \
f"to TAKE PROFIT at ${profit_leg_limit:.2f} LIMIT " \
f"or {'above' if side == 'sell' else 'below'}."
order_params['take_profit'] = TakeProfitRequest(limit_price=profit_leg_limit)
if loss_leg_stop and not loss_leg_limit:
order_params['stop_loss'] = StopLossRequest(stop_price=loss_leg_stop)
loss_line = f"{'BUY' if side == 'buy' else 'SELL'} {num_shares} shares of {symbol} " \
f"to STOP LOSS if ${loss_leg_stop:.2f} STOP " \
f"crossed {'above' if side == 'sell' else 'below'}."
else: # both limit and stop, Alpaca doesn't support stop_loss.limit_price alone!
order_params['stop_loss'] = StopLossRequest(limit_price=loss_leg_limit, stop_price=loss_leg_stop)
loss_line = f"{'SELL' if side == 'buy' else 'BUY'} {num_shares} shares of {symbol} " \
f"at ${loss_leg_limit} or better " \
f"to STOP LOSS if ${loss_leg_stop:.2f} " \
f"STOP crossed {'above' if side == 'sell' else 'below'}."
except ValidationError as e:
return f"Invalid Order: {e}", True, "", True
The new Version 2 of the Alpaca Python SDK Version 2 uses a Python package called Pydantic to validate order parameters. This means there is a unique class for each order type and you will get immediate feedback (in the console) if your parameters are incorrect. Notice that I said “parameters” but above we’ve been compiling a dictionary. That was so we can use Python’s iterable unpacking functionality in this next section to convert key:value pairs in our dictionary into the key=value parameters needed for the Pydantic classes. Notice the **order_params
argument which is what unpacks the dictionaries we’ve been customizing for each order type.
As an aside, there is a generic OrderRequest() class that one can use but doing so would reduce the benefit of this pre-submission validation. I worked on how best to configure the orders for quite a while! Logically, one might think it would be easier to just configure each type separately, maybe using the generic class, but the complication is in dealing with the OPTIONAL advanced classes; the Pydantic classes often don’t allow setting a parameter to None so I used this unpacking approach to overcome that. An alternative might be to just require the user to stipulate the order type in the interface then only build that set of params but I wanted to remove that complexity from the user interface.
# Create Order object using the unpacked order parameters
try:
if order_type == 'market':
order_obj = MarketOrderRequest(**order_params)
if order_type == 'limit':
order_obj = LimitOrderRequest(**order_params)
if order_type == "stop":
order_obj = StopOrderRequest(**order_params)
if order_type == 'stop-limit':
order_obj = StopLimitOrderRequest(**order_params)
if order_type == 'trailing_stop':
order_obj = TrailingStopOrderRequest(**order_params)
except ValidationError as e:
return f"Invalid Order: {e}", True, "", True
Now, let’s compile our order_summary, to give the user feedback on what they are about to submit to the broker. If desired, you could certainly make this less verbose, say by just listing the order components, but I wanted to provide as much helpful information as possible to my users who might not be familiar with advanced order types.
entry_line = f"{side.upper()} {tif.upper() if tif != 'ext' else 'during EXTENDED HOURS'} " \
f"{qty_text} {symbol} {how_traded}."
if not oco_checked and account == 'live' and qty and symbol and side:
account_html = html.Span([
html.Small("Opening order for ", style={'color': 'darkgray'}),
html.Small(f"{account.capitalize()}:", className="bg-warning")
])
elif not oco_checked and account == 'paper' and qty and symbol and side:
account_html = html.Span([
html.Small("Opening order for ", style={'color': 'darkgray'}),
html.Small("PAPER ACCOUNT:", style={'color': 'darkgray'})
])
elif oco_checked and account == 'live' and qty and symbol and side:
entry_line = ""
order_described = ""
account_html = html.Div([
html.Small("Closing orders for current position (OCO) in ", style={'color': 'darkgray'}),
html.Small("LIVE ACCOUNT:", className="bg-warning")
])
elif oco_checked and account == 'paper' and qty and symbol and side:
entry_line = ""
order_described = ""
account_html = html.Div([
html.Small("Closing order for open position (OCO) in ", style={'color': 'darkgray'}),
html.Small("PAPER ACCOUNT:", style={'color': 'darkgray'})
])
else:
account_html = ""
Once the user reviews the order summary, they will click the submit button to send the order to Alpaca. We need to handle that next as the first part of an if
statement which will submit the order to Alpaca and return the submission_details. As the model is also closed you will not actually see the submission_details but I send them through to support testing (by changing the True to False for the modal’s is_open argument). For the else clause, we’ll return the order_summary with some styling.
if ctx.triggered_id == "submit-button" and order_status != "submitted":
print("Submit Clicks:", n_submit)
key = live_key if account == "live" else paper_key
secret = live_secret if account == "live" else paper_secret
try:
order_details = submit_order_to_Alpaca(order_obj, account, key, secret)
except APIError as e:
error_data = json.loads(e.args[0])
msg = html.Div(
[
html.H4("Alpaca rejected the order!", className="bg-warning text-danger"),
html.P(error_data["message"].capitalize(), className="text-info")
]
)
return msg, True, "submitted", True
else: # order accepted
submission_details = html.Div(
[
html.P("Order Submitted", className="text-info"),
html.Table(
[
html.Tr([html.Td(f"{property_name}:"), html.Td(f"{value}")], className="text-info")
for property_name, value in order_details
]
)
]
)
order_status = order_details.status.value if order_details.status else "N/A"
return submission_details, True, "submitted", False
else: # Not yet submitted, so just return the summary
summary_text = html.Div(
[
html.Small(account_html),
html.P(entry_line),
html.Small(order_described, style={'color': 'darkgray'}),
html.P(profit_line, style={'color': 'green'}),
html.P(loss_line, style={'color': 'red'}),
]
)
return summary_text, False, "", True
Notice the submit_order_to_Alpaca
function call, wrapped within a try/except block. Even though we are validating our instructions both at the field level and again with the Pydantic classes implemented by Alpaca, there is still the chance that an order submission can fail. For example, Alpaca will reject a “reversing” order, meaning one that flips from, say, a long to a short position in a single order. Or, as another example, an order can get rejected for insufficient funds. In such obscure situations we just need to capture Alpaca’s rejection and display it on the frontend.
With the error handing in the callback, the function is rather simple. Here’s that code.
def submit_order_to_Alpaca(the_order, the_acct, the_key, the_secret):
if the_acct == 'paper':
trading_client = TradingClient(the_key, the_secret, paper=True)
else:
trading_client = TradingClient(the_key, the_secret, paper=False)
order_details = trading_client.submit_order(the_order)
return order_details
With this function plus our callback we can now submit orders to Alpaca! As I’ve said repeatedly, run lots of tests using the paper account before ever considering setting up a live account.
BID/ASK/LAST
We have a few more tasks to complete this chapter. Our Order model has fields for bid/ask and last price, plus the day’s change in price. We need another callback to update those. Unfortunately, all three values are in different Alpaca datasets, so let’s use the Snapshot request to load everything we need in one step.
Here’s the function we’ll use. Of course, we need the Alpaca access keys and the current stock from the selected-symbol-store. Quotes are updated in realtime, and Alpaca does provide a webSocket interface. However, Dash doesn’t provide an easy way to get a“push” data stream into a callback; it can be done with what’s called a ClientSide callback but it requires writing Javascript code. For our end-of-day trading application, we don’t need sub-second accuracy in our bid/ask/last prices. Besides, unless you pay for an expensive realtime SIP account, the free IEX data doesn’t change that fast anyway!
def retrieve_snapshot_for_single_stock(ticker: str, key: str, secret: str) -> object:
""" Get historical snapshot from Alpaca for specified ticker
Retrieves a dictionary-like (keyed by ticker) nested object with datasets: 'latest_trade',
'latest_quote', latest 'minute_bar', latest 'daily_bar' and previous 'daily_bar'.
Get keys using: snapshot.keys(). Get datasets and attributes using: snapshot.items().
Example for accessing attributes: snapshot[ticker].latest_quote.ask_price.
"""
try:
historical_client = StockHistoricalDataClient(key, secret)
request = StockSnapshotRequest(symbol_or_symbols=[ticker]) # multiple allowed but not supported
return historical_client.get_stock_snapshot(request)
except Exception as e:
print(e)
return None
Remember, awhile back, we set up the dcc.Interval component in our main layout. We will use that now as an input to a new callback that will trigger data collection at one second intervals. Snapshot retrieves several data objects as a list, so parsing to get the values we want is a bit harder but still relatively straightforward.
@callback(
Output('bid-text', 'children'), # $0.00 & exchange code
Output('bid-size', 'children'), # x 000
Output('ask-text', 'children'), # $0.00
Output('ask-size', 'children'), # x 000 & exchange code
Output('spread-text', 'children'), # $0.00 & time
Output('last-text', 'children'), # $0.00
Output('last-change', 'children'), # $0.00 (0.00%)
Output('bid-price-store', 'data'),
Output('ask-price-store', 'data'),
Output('last-price-store', 'data'),
Input('interval-quotes', 'n_intervals'),
Input('alpaca-key-paper', 'value'),
Input('alpaca-secret-paper', 'value'),
Input('alpaca-key-live', 'value'),
Input('alpaca-key-live', 'value'),
Input('account', 'value'),
State('selected-symbol-store', 'data'),
)
def update_quotes(interval, paper_key, paper_secret, live_key, live_secret, account, symbol):
""" Get an Alpaca Snapshot to compile data for the Order Modal's header.
Note: Snapshot uses IEX (unless a paid account) whereas historic bars use SIP-15m-delayed.
Small differences will be seen in this data compared to graph!
"""
if not symbol:
raise PreventUpdate
# Get Snapshot from Alpaca
key = live_key if account == "live" else paper_key
secret = live_secret if account == "live" else paper_secret
snapshot = retrieve_snapshot_for_single_stock(symbol, paper_key, paper_secret)
if not snapshot:
raise PreventUpdate
def convert_exchange(val):
if val == "V":
return "IEX"
elif val == "A":
return "AMEX"
elif val == "N":
return "NYSE"
elif val in ['Q', 'S', 'X']:
# Q is OMX (formerly Nordic), S is smallcap, X is OMX PSX (formerly Pacific). B is OMX-BX options exch.
return "NDQ"
else:
return val
# Get Bid/Ask/Spread data from Latest Quote
quote = snapshot[symbol].latest_quote
ask = quote.ask_price
ask_str = f"${float(ask):.2f}"
ask_size = f"{quote.ask_size * 100:.0f} {convert_exchange(quote.ask_exchange)}"
bid = quote.bid_price
bid_str = f"${float(bid):.2f}"
bid_size = f"{quote.bid_size * 100:.0f} {convert_exchange(quote.bid_exchange)}"
quote_time = quote.timestamp.astimezone(pytz.timezone("US/Eastern")).strftime("%H:%M:%S")
spread_val = float(bid) - float(ask)
spread = html.Div([html.Div(f"{spread_val:.2f}"), html.Div(quote_time)])
# Get Last-Trade from the latest minute bar
latest_minute_bar = snapshot[symbol].minute_bar
last = float(latest_minute_bar.close)
last_str = f"${last:.2f}"
# Get previous-day data to calculate change
prev_high = float(snapshot[symbol].previous_daily_bar.high)
prev_low = float(snapshot[symbol].previous_daily_bar.low)
prev_close = snapshot[symbol].previous_daily_bar.close
day_chg = last - prev_close
day_perc = day_chg / prev_close
if day_chg < 0:
last_change = html.Div(f"{format_dollar(day_chg)} ({day_perc:.2%})", className="text-warning")
else:
last_change = html.Div(f"${day_chg:.2f} ({day_perc:.2%})", className="")
return bid_str, bid_size, ask_str, ask_size, spread, last_str, last_change, bid, ask, last
There’s a useful little utility function, format_dollar
, in this code that I found on StackOverflow. This will put the negative sign ahead of the dollar sign when formatting negative dollar amounts, -$100 instead of $-100.
def format_dollar(amount):
# From https://stackoverflow.com/questions/30258593/python-format-negative-currency
formatted_absolute_amount = '${:,.2f}'.format(abs(amount))
if round(amount, 2) < 0:
return f'-{formatted_absolute_amount}'
return formatted_absolute_amount
A Summary of our Adventure:
- In Chapter 1, we created the UI for a basic trading webpage using Dash.
- In Chapter 2, created an Alpaca Settings dialog and added code to load candlestick price data into a stock chart.
- In Chapter 3, we reviewed Alpaca’s trading rules.
- In Chapter 4, we created the order form and associated code to compile trade instructions.
- In this chapter, we wrote the callback needed to submit an order to Alpaca.
- In Chapter 6, we’ll use the Alpaca account data to fill the Orders and Positions tabs.