TRADEnosis™ — My Quest to Build a Mythical Stock Trading Platform

Chapter 4: An Alpaca Python SDK v2 Order Modal in Plotly Dash

Steve Mayo
13 min readAug 15, 2023

On our quest to build a stock trading application, we have imported a number of Python packages, configured Plotly Dash to use Dash Bootstrap Components (dbc) and some stylesheets, wrote a function to retrieve historic candlestick price data from the Alpaca Markets Python SDK, created a number of layout components within a Bootstrap-like container structure, and wrote our first Dash callbacks to collect API keys and update the stock graph. We also reviewed the many rules and requirements for submitting trades to a brokerage. We want to use Alpaca for that, but first we need a form to collect the trade instructions.

Before we break camp

Let’s make a few quick refinements to our code. First, let’s add the company name to our test Pandas dataFrame so that we can pass it through to our forthcoming orders form. Again, this is still just example data. Once we have the basic app, I’m expecting you will want to add your own trading algorithm that will include generating a custom ticker list and additional columns with trading signals.

# region SETUP
stock_list = ["AAPL", "MSFT", "GOOG", "TSLA", "NVDA", "AMZN", "META"]
companies = ['Apple Inc.', 'Microsoft Inc.', 'Alphabet Inc.', 'Tesla Inc.',
'NVIDIA Corporation', 'Amazon Inc.', 'Meta Platforms Inc.']
df_watchlist = pd.DataFrame(data={"TICKER": stock_list, "COMPANY": companies})

As we configured our watchlist to use all columns, the company names will appear there as well. You may now see part of the COMPANY column peeking through and the columns may look a bit crowded. My goal is to support screens widths from the iPhone-8 (375 x 667px) up through a full desktop so we may want to hide this column on smaller screens.

Time for a quick layout tweak?

One way to do that is with some style directives, as shown by the watchlist layout code below. Notice, I added a font-size CSS setting to the style_header and a new style_data_conditional parameter that targets just the first column. You may need to tweak the minWidth setting for the devices you intend to support.

# region LAYOUT - WATCHLIST
watchlist = dash_table.DataTable(
id='watchlist-table',
data=df_watchlist.to_dict('records'),
columns=[{"name": i, "id": i} for i in df_watchlist.columns],
style_cell={'textAlign': 'left'},
style_as_list_view=True,
style_header={'borderTop': '0px', 'font-size': '12px', 'font-weight': '600'},
style_data={'border': '0px'},
style_data_conditional=[
{
'if': {
'column_id': 'TICKER',
},
'minWidth': '100px', 'font-size': '12px'
}
]
)
# endregion /watchlist

Another, less fidgety, way is to simply not display company name; it will still be in the data we passed to this component so we can show it elsewhere. In this alternative example, I configured just a single column rather than parsing all records.

# region LAYOUT - WATCHLIST
watchlist = dash_table.DataTable(
id='watchlist-table',
data=df_watchlist.to_dict('records'),
columns=[
{"name": 'Ticker', "id": 'TICKER', 'type': "text"},
],
style_cell={'textAlign': 'left'},
style_as_list_view=True,
style_header={'borderTop': '0px', 'font-size': '12px', 'font-weight': '600'},
style_data={'border': '0px', 'font-size': '12px'},
)
# endregion /watchlist

Adding a Order Submission Modal

Now we are ready to create our trade order modal. First, we need a button to open a modal. Let’s add one just below the chart. Notice in this code I also added an orders_modal variable at the top of our main container.

# region LAYOUT - CONTAINER
app.layout = dbc.Container(
[
alpaca_modal,
orders_modal, # Chpt4: New layout variable for the Order modal
navbar,
dbc.Row(
[
dbc.Col(watchlist,
xs=3, sm=2, md=1, style={"maxHeight": "375px", "overflow": "scroll"}, ),
dbc.Col(chart, xs=9, sm=10, md=11)
],
className="mt-1",
),
# Chpt4: New row to insert a 'TRADE' button below the stock graph
dbc.Row(
[
html.Div(
[
dbc.Button("Trade", id="btn-orders-modal", n_clicks=0,
color="primary", disabled=True, size="sm")
], className="my-1 col-8 col-md-5 d-grid mx-auto",
)
]
),
dbc.Row(
[
dbc.Col(
dbc.Tabs(
[
dbc.Tab(orders_card, label="Orders",
activeLabelClassName="fw-bold",
),
dbc.Tab(positions_card, label="Positions", activeLabelClassName="fw-bold"),
],
),
id="tabs",
width=12)
],
)
],
fluid=False,
)

Now, let’s add that new modal layout to collect the specifics of our order. Add this skeleton code below the KEYS MODAL.

# region LAYOUT - ORDERS MODAL
order_header = ""
account_radios = ""
order_side_buttons = ""
tif_input = ""
qty_input = ""
limit_price_input = ""
stop_price_input = ""
trailing-stop_input = ""
bracket_profit_input = ""
bracket_stop_input = ""

orders_modal = html.Div(
[
dbc.Modal(
[
dbc.ModalHeader(
[
order_header,

], className="bg-secondary"
),
dbc.ModalBody(
[
dbc.Container(
[
account_radios,
order_side_buttons,
qty_input,
tif_input,
html.H5(
"Entry Triggers",
id="entry-triggers-heading",
className="text-primary text-center border-bottom border-top "
"border-primary rfs-small py-1"
),
limit_price_input,
stop_price_input,
trailing_stop_input,
html.H5(
"Advanced Exits",
id="brackets-heading",
className="text-primary text-center border-bottom border-top "
"border-primary rfs-small py-1"
),
oco_checkbox,
html.I("Take-Profit Leg:", className="text-muted rfs-tiny"),
take_profit_leg,
html.I("Stop-Loss Leg:", className="text-muted rfs-tiny"),
stop_loss_leg_limit,
stop_loss_leg_stop,
html.H5(
"Order Summary",
id="order-summary-heading",
className="text-primary text-center border-bottom border-top "
"border-primary rfs-small py-1",
),
html.Div(id='order-summary'),

]
)
]
),
dbc.ModalFooter([
dbc.Row([
dbc.Button('Confirm Order', id='submit-button', n_clicks=0, disabled=True),
])

])
],
id="orders-modal",
is_open=False,
),
]
)
# endregion /order modal

We need a Dash callback to trigger displaying this new modal. This code should look familiar as it’s similar to the callback that triggers the Alpaca settings dialog. We’ll modify it later to add additional functionality.

# region CALLBACK - ORDERS MODAL
@app.callback(
Output("orders-modal", "is_open"),
Input("btn-orders-modal", "n_clicks"),
State("orders-modal", "is_open"),
)
def toggle_orders_modal(n1, is_open):
if n1:
return not is_open
return is_open
# endregion /orders modal

Now, let’s work on the layout for our order modal, starting with the header variable. I’ve configured two rows, wrapped within a div, and each having dbc column components. Our users will want to confirm they are trading the correct stock, so in the first row of this header we will display the symbol and company name. The second row will display the Bid and Ask prices and the spread between them. Remember, we are initially using the free IEX data feed, which can have sporadically-huge spreads, so these prices are a critical feature to include! We’ll get these values to update in the next chapter.

order_header = html.Div([
dbc.Row(
[
dbc.Col(
[
html.Span(id="symbol"),
html.Span(" - "),
html.Span(id="company"),
], width=12, className="rfs-large"),
],
className="mb-2"
),
dbc.Row(
[
dbc.Col(
[
html.Div("BID", className="rfs-small"),
html.Div(id="bid-text", className="rfs-small", n_clicks=0),
html.Div(id="bid-size", className="rfs-tiny")
], width=4,
),
dbc.Col(
[
html.Div(id="spread-text", className="rfs-tiny text-muted", n_clicks=0),
]
),
dbc.Col(
[
html.Div("ASK", className="rfs-small"),
html.Div(id="ask-text", className="rfs-small", n_clicks=0),
html.Div(id="ask-size", className="rfs-tiny"),
], width=4,
),
])

Here’s code to display a set of account radio buttons for specifying whether the order should use a paper or live account. Back when we created the Alpaca Keys Modal, we only created fields for a paper account. I’ll leave it to you to go back and add live-account inputs there.

account_radios = dbc.Row(
[
dbc.Label(html.Small("Account*", className="rfs-small"), width=3),
dbc.Col(
[
dbc.RadioItems(
id="account",
className="btn-group btn-lg",
inputClassName="btn-check",
labelClassName="btn btn-outline-primary",
labelCheckedClassName="active",
persistence_type="local", persistence=True,
options=[
{"label": "Paper", "value": 'paper'},
{"label": "Live", "value": 'live'},
],
value='paper',
),
],
width=4
),
dbc.Col(
[
html.Small(id='account-details', className="text-muted rfs-tiny")
],
width={'offset': 3, 'size': 9}
)
],
className="radio-buttons",
style={'marginBottom': '5px'}, # className not working
)

The order_side buttons are used to designate whether the order is to buy or sell the security.

order_side_buttons = dbc.Row(
[
dbc.Label(html.Small("Side*", className="rfs-small"), width=3),
dbc.Col(
[
dbc.RadioItems(
id="order-side",
className="btn-group btn-lg",
inputClassName="btn-check",
labelClassName="btn btn-outline-primary",
labelCheckedClassName="active",
options=[
{"label": "Buy", "value": 'buy'},
{"label": "Sell", "value": 'sell'},
],
),
],
width=9
),
],
className="radio-buttons",
style={'marginBottom': '5px'}, # className not working
)

Both sets of buttons (account and side) use a bit of CSS in the external stylesheet to adjust their borders, alignment, and coloration. Add this to the styles.css file in your assets directory.

/* restyle Radio-Type Buttons used for side and account */
.radio-buttons .form-check {
padding-left: 0;
}
.radio-buttons .btn-group > .form-check:not(:last-child) > .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.radio-buttons .btn-group > .form-check:not(:first-child) > .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-left: -1px;
}

/* CSS rule to color the 'live' account button yellow when selected */
#account > div:nth-child(2) > input:checked ~ label {
color: rgba(255,69,0,1);
background-color: rgb(255, 193, 7);
}

For the time-in-force (tif) component, expand the placeholder variable as follows. In the previous chapter, I mentioned that Alpaca uses a separate parameter for signifying extended hours. However, I’m including ‘ext’ as a tif option for reasons you will understand when we work on adding form interactivity in the next chapter. You may also notice that I’m using the Dash Core Component (dcc) rather than the Dash Bootstrap Component (dbc) for the dropdown as the latter does not support labels in the option dictionary; the styling difference is not appreciable in this instance. This code also shows how to use the dbc.Tooltip component for adding a roll-over to the input label.

tif_input = dbc.Row(
[
dbc.Label(html.Small("TIF*", className="rfs-small", id="tif-label"), width=3),
dbc.Tooltip(html.Div(
[
html.P("•DAY orders trade during regular hours (9:30AM-4:00PM Eastern) and are "
"cancelled at the close."),
html.P("•EXT orders trade between 4AM-8PM, must be LIMIT, and are cancelled "
"after extended-hours close."),
html.P("•GTC orders are only executed during regular hours and remain "
"open until filled or cancelled."),
html.P("•OPG orders trade at session opening price, "
"must be MKT or LIMIT and submitted between 7:00PM and 9:28AM next day."),
html.P("•CLS orders trade at session closing price, "
"must be MKT or LIMIT and submitted between 7:00PM and 3:50PM next day."),
html.P("•IOC orders are filled immediately at Market. Any unfilled "
"portion is cancelled."),
html.P("•FOK orders are filled immediately at Market. If it cannot be "
"fully-executed, it is cancelled "),
]
), target="tif-label", className="rfs-tiny my-2"
),
dbc.Col(
[
dcc.Dropdown(
options=[
{'label': 'DAY', 'value': 'day'},
{'label': 'EXT', 'value': 'ext'},
{'label': 'GTC', 'value': 'gtc'},
{'label': 'OPG', 'value': 'opg'},
{'label': 'CLS', 'value': 'cls'},
{'label': 'IOC', 'value': 'ioc'},
{'label': 'FOK', 'value': 'fok'},
],
id='tif',
value='day',
clearable=False,
),
], width=9,
)
],
className="mb-2",
)

For the qty_input component and similar numeric fields, we wrap the input in a dbc.InputGroup() so we can prepend a symbol to specify the data type, a “#” is the default and ending with a dropdown for selecting between # and $ entries. In the browser, Dash server will convert this code to a standard HTML5 input field that limits the entry to numeric values.

qty_input = dbc.Row(
[
dbc.Label(html.Small("Qty*", className="rfs-small", id="qty-label"), width=3),
dbc.Tooltip(html.Div(
[
html.P("• Fractional stock shares only allowed for Market-Day orders and with no more than "
"2 decimal places.),
html.P("• Notional ($) is only allowed for Market-Day orders."),
]
), target="qty-label", className="rfs-tiny my-2"
),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("#")
dbc.Input(id='qty', type='number'),
dbc.DropdownMenu([
dbc.DropdownMenuItem(
html.Small("Only Mkt-Day if fractional!", className="text-muted rfs-tiny"), header=True),
dbc.DropdownMenuItem("# Shares", id="qty-shares"),
dbc.DropdownMenuItem(
html.Small("Converted if not Mkt-Day:", className="text-muted rfs-tiny"), header=True),
dbc.DropdownMenuItem("$ Notional", id="qty-dollars"),

]
),
width=9
),
],
id='qty-input',
className="mb-2",
)

Importantly, we do NOT want to require any of the price fields to be non-blank (using required or min parameters). We will be using blank values in our callback to distinguish the order types. Notice on the Trailing Stop input, I’ve added a dropdown component to the InputGroup for selecting between percent and dollar value formats.

limit_price_input = dbc.Row(
[
dbc.Label(html.Small("Limit", className="rfs-small"), width=3),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("$", id='limit-icon'),
dbc.Input(id='limit-entry', type='number' ),
]
),
width=9,
),
],
id='limit-price-input',
className="mb-2",
)

stop_price_input = dbc.Row(
[
dbc.Label(html.Small("Stop", className="rfs-small"), width=3),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("$", id='stop-icon'),
dbc.Input(id='stop-entry', type='number'),
]
),
width=9,
),
],
id='stop-price-input',
className="mb-2",
)

trailing_stop_input = dbc.Row(
[
dbc.Label(html.Small("Trailing", className="rfs-small", id="ts-label"), width=3),
dbc.Tooltip(html.Div(
[
html.P("• Trailing-stop will only trigger during regular market hours and must be DAY or GTC only."),
html.P("• Enter $values as actual trigger price, %values as delta above/below high-water-mark (HWM)."),
]
), target="ts-label",
),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("$", id='trailing-icon'),
dbc.Input(id='trailing-stop', type='number'),
dbc.DropdownMenu([
dbc.DropdownMenuItem("% Trigger", id="trail-percent"),
dbc.DropdownMenuItem("$ Trigger", id="trail-dollars"),
])
]
),
width=9,
),
],
id='trailing-stop-input',
className="mb-2",
)

bracket_profit_input = dbc.Row(
[
dbc.Label(html.Small("Profit Leg", className="rfs-small"), width=3),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("$", id='profit-leg-icon'),
dbc.Input(id='profit-leg', type='number'),
]
),
width=9,
),
],
id='profit-leg-input',
className="mb-2",
)

bracket_stop_input = dbc.Row(
[
dbc.Label(html.Small("Loss Leg", className="rfs-small"), width=3),
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupText("$", id='loss-leg-icon'),
dbc.Input(id='loss-leg', type='number'),
]
),
width=9,
),
],
id='loss-leg-input',
className="mb-2",
)

With those changes, our app now looks like this when we click the TRADE button.

Our order form does not know which stock we are trying to trade! Currently, we can select a ticker in our watchlist and that triggers a callback to load that symbol’s data into the chart. Now, we will need some of that same data in a new callback. Thankfully, Dash has a feature called a Store; a location to where we can easily and temporarily copy the symbol and company name until we need them.

Implementing Dash Stores to Pass Data to our Order

In the main app.layout container, add dcc.Store components set to use ‘memory’ as the storage type as we only want data retention to be temporary. Add stores for the symbol, company, order status, bid price and ask price, which we’ll implement later.

Dash supports three storage types:

  • memory — retains the Store’s data only in memory, so it will not be retained if the browser is refreshed or the server is restarted.
  • local — retains the Store’s data in a file on the local device, similar to a cookie but encrypted. We used something similar when we set persistence_type=local to retain the Alpaca keys across restarts.
  • session — retains the Store’s data in the browser’s session but it is lost if the browser is restarted.

While there, let’s also add a dcc.Interval component configured to “fire” at, say, 1-second intervals. We will use this later to update bid/ask prices.

# region LAYOUT - CONTAINER
app.layout = dbc.Container(
[
# Stores and Interval added for Chapter 4
dcc.Store(id='selected-symbol-store', storage_type='memory'),
dcc.Store(id='selected-company-store', storage_type='memory'),
dcc.Store(id='order-status-store', storage_type='memory'),
dcc.Store(id='bid-price-store', storage_type='memory'),
dcc.Store(id='ask-price-store', storage_type='memory'),

dcc.Interval(
id='interval-quotes',
interval=1 * 1000, # in milliseconds
n_intervals=0,
disabled=False,
),

alpaca_modal,
orders_modal,
...

We want to put the symbol and company into their Stores when the user selects a row in the watchlist. Ah! We need to update that callback. Here’s the revised code for the “LOAD CHART DATA” callback. I’ve added Output arguments referencing the stores, a line of code to extract the company from the table, and the symbol and company to the return statement.

# region CALLBACK - LOAD CHART DATA
@callback(
Output('stock-chart', 'seriesData'),
Output('selected-symbol-store', 'data'), # Chpt4 - add this
Output('selected-company-store', 'data'), # Chpt4 - add this
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']
company = table_data[selected_row_index]['COMPANY'] # Chpt4 - add this


# 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), \
symbol, company # Chpt4 - add this
# endregion /load chart cb

A logical place to update the header of the order modal with our stored values is in our “Orders Modal” callback. Here’s the revised code where I’ve added references to our Store values as State arguments (instead of Inputs as we just want to read these locations, not have them trigger anything) and also added them as parameters to the function. Then, all that is needed is to pass these values through to our layout locations as children of the html components with the matching ids when if the dialog is being opened.

# region CALLBACK - ORDERS MODAL
@app.callback(
Output("orders-modal", "is_open"),
Output('symbol', 'children'),
Output('company', 'children'),
Input("btn-orders-modal", "n_clicks"),
State("orders-modal", "is_open"),
State('selected-symbol-store', 'data'),
State('selected-company-store', 'data'),
)
def toggle_orders_modal(n1, is_open, symbol, company):
if n1:
return not is_open, symbol, company
return is_open, no_update, no_update
# endregion /settings modal

We could use this same callback to add other components to the header, such as the bid/ask price. But, those components must be loaded from Alpaca and for that we need the API keys. We’ll also need the Alpaca access keys to submit orders, so the logical place to load the rest of the header is from the callback that submits the order. We’ll work on that in the next chapter.

To close out this chapter, let’s add a quick callback to handle the field format toggles. This code shows a nice way to handle multiple toggles in a single callback using a “toggle_map” as an alternative to a long if-else clause. I learned this bit of magic from a wizard called ChatGPT.

# region CALLBACK - TOGGLE BUTTONS
toggle_map = {
"qty-dollars": ("$", "bg-warning", no_update, no_update),
"qty-shares": ("#", "", no_update, no_update),
"trail-percent": (no_update, no_update, "%", "bg-warning"),
"trail-dollars": (no_update, no_update, "$", ""),
}
@app.callback(
Output("qty-icon", "children"),
Output("qty-icon", "className"),
Output('trailing-icon', 'children'),
Output('trailing-icon', 'className'),
[Input(button_id, "n_clicks") for button_id in toggle_map.keys()],
)
def toggle_icons(*n_clicks):
""" Use toggle_map to change prepended icons and background colors in input fields of Order modal. """

triggered_id = ctx.triggered_id
if triggered_id in toggle_map:
print(f"{triggered_id} triggered")
return toggle_map[triggered_id]
else:
raise PreventUpdate
# endregion CALLBACK - TOGGLE BUTTONS

A Summary of our Adventure:

  • In Chapter 1, we created the UI for a basic trading webpage using Dash.
  • In Chapter 2, we 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 this chapter, we created 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’ll use the Alpaca account data to fill the Orders and Positions tabs.

--

--

Steve Mayo

Steve is a Doctor of Pharmacy, a retired clinical researcher, an avid investor, and the creator of TRADEnosis.com, where he applies AI to stock trading.