Skip to main content

Parallel order execution for Alpaca

Project description

blockpaca

Parallel execution system for Alpaca broker API

In-depth description of architecture here

Installation

pip install blockpaca

Pip installation requires Python version >= 3.10

Alternatively, you can clone the repository in a new or existing environment.

In terminal: git clone https://github.com/henrysalkever/blockpaca.git

Repo structure

blockpaca/
├── LICENSE
├── pyproject.toml
├── README.md
└── src
    └── blockpaca
        ├── __init__.py
        ├── core.py
        ├── live_display.py
        └── tools.py

Running blockpaca

from blockpaca import run_trading

log_dir = "/Users/username/log_dir"
API_KEY = "custom_api_key" #API key obtained from your Alpaca account
SECRET_KEY = "custom_secret_key" #secret key obtained from your Alpaca account
TICKERS = ["AAPL", "TSLA", "NVDA"] #tickers to receive data for, up to 30 max for free accounts

def custom_strategy(context):
    quotes, positions = context['get_current_data']()
    #some custom trading logic
    return order_dict 

if __name__ == "__main__": 


    run_trading(
        strategy_callable = custom_strategy,
        trade_log_name = "custom_strat_name",
        trade_log_path = log_dir,
        api_key = API_KEY,
        secret_key = SECRET_KEY,
        tickers = TICKERS,
        frequency = 10, 
        run_seconds = 60,
        eos_behavior = "hold", 
        warm_up_period = 10, 
        live_display = True, 
        display_refresh_rate = 0.05, 
        max_trade_updates = 20, 
    )

Parameters

strategy_callable : custom trading logic

trade_log_name : name for trade log file (make this something identifiable to the strategy you are running)

trade_log_path : path for trade logs

api_key : your api key from Alpaca

secret_key : your secret key from Alpaca

tickers : a list of the tickers you wish to receive quotes for (max 30 for free tier)

frequency : frequency at which the trading logic executes. Accepts integers, floats as well as strings of types "1m", "15m", "1h", "2h" *

run_seconds : total seconds to run the trading loop for. Accepts integers, floats, as well as strings of types "1m", "15m", "1h", "2h" * and "EOD" which will input the seconds until the market closes for the day minus 30 seconds.

eos_behavior : dictates the behavior at the end of the trading session. Options are "liquidate", "hold" and "custom". See the section on custom eos behavior for further details.

warm_up_period : time to wait for quotes before starting the trading loop (5 seconds recommended)

live_display : Boolean value to toggle on/off for the rich terminal display of positions and trade status

display_refresh_rate : time between each refresh of values in live display

max_trade_updates : controls how many trade updates are shown in the live display

*(numbers are adjustable, though will throw an error if you input a value for hours that will make your trading session terminate after 30 seconds before the market closes, for example "9h")

Creating Custom Trading Logic

Custom trading functions must be set up as follows:

def custom_function(context):
    #trading logic
    return orders

Your function must be set up to accept the context block as the only argument. The context block is a dictionary that is passed in that contains the methods for getting current quotes and positions, and submitting orders. The context block also contains the addresses for the shared memory, the result queue, and the command queue that are necessary for sharing information across Python processes.

context = {
    "get_current_data": get_current_data, #callable
    "submit_order": submit order, #callable
    "shm_meta": shm_meta, #shared memory
    "result_queue": result_q, #result queue
    "command_queue": cmd_q, #command queue
}

Receiving Current Quotes and Positions

Within your trading function, run the following to get current quotes and positions:

quotes, positions = context['get_current_data']()

The schema for the quote data you recieve will be

{
   "AAPL": {"bid": 150.25, "ask": 150.30},
   "MSFT": {"bid": 299.50, "ask": 299.75},
   "GOOG": {"bid": 2800.00, "ask": 2801.50}
}

and the schema for position data received will be

[
   {"ticker": "AAPL", "quantity": 10, "bought_at": 145.00, "cost_basis": 1450.00},
   {"ticker": "MSFT", "quantity": -5, "bought_at": 300.00, "cost_basis": -1500.00},
   {"ticker": "GOOG", "quantity": 20, "bought_at": 2750.00, "cost_basis": 55000.00}
]

Persistent and Non-Persistent Data Between Trading Loops

If you would like to run strategies that depend on data collected over the course of the trading session (moving averages etc.), you should collect data in a structure outside of your custom trading function. For example, if you wanted to collect the quotes passed to the trading algorithm each time it was called, you would set up your code in the following way:

quote_collection = []

def trading_strat(context):
    quotes, positions = context['get_current_data']()
    quote_collection.append(quotes)

    #trading logic...

    return orders

if __name__ == "__main__":

    run_trading(
        strategy_callable = trading_strat,
        ...
    )

If you were to instead initialize your list inside the trading function, it would be wiped every time your trading logic runs. You can also read in csv files or other past data and have it persist from execution to execution, it just needs to be read in outside of the strategy function itself.

Composing and Submitting Orders

Sides: "buy", "sell" Actions: "buy_to_cover", "sell_long", "short_sell", "liquidate", "cover_short"

Side Action Result
buy none open long
buy buy_to_cover close short
buy cover_short close short (used in eos)
sell none/sell_long close long
sell short_sell open short
sell liquidate close long (used in eos)

the schema for individual orders is

{
    "ticker": str,         # e.g. "AAPL"
    "quantity": float,     # e.g. 10
    "side": str,           # "buy" or "sell"
    "order_type": str,     # "market" or "limit"
    "action": str,         # optional: "buy_to_cover", "short_sell", "sell_long", "liquidate", "cover_short"
    "limit_price": float,  # required if order_type is "limit"
}

to submit a block of orders, simply combine them into a list

orders = [
    {"ticker": "AAPL", "quantity": 10, "side": "buy", "order_type": "market"},
    {"ticker": "MSFT", "quantity": 5, "side": "buy", "order_type": "market"},
    {"ticker": "GOOGL", "quantity": 3, "side": "sell", "order_type": "market"},
]

there are two options for submitting orders:

#Option 1

def trading_strat(context):
    #retrieve quotes, positions, run trading logic to yield orders 

    context['submit_order'](orders)
    return None

#Option 2

def trading_strat(context):
    #retrieve quotes, positions, run trading logic to yield orders 

    return orders

Both of these options are different ways to access the same execution function.

Calling Protocol

It is important to call run_trading as follows

#global variables for initialization

def trading_function(context):
    return orders

if __name__ == "__main__":
    run_trading(
        strategy_callable = trading_function,
        #.....
    )

because run_trading spawns child processes, and calling run_trading outside the main block may spawn extra child processes during import.

Custom EOS Behvaior

If you call run_trading with the eos_behavior parameter set to "custom", you must also provide a callable function to the custom_shutdown_action parameter. For example:

from blockpaca import run_trading

log_dir = "/Users/username/log_dir"
API_KEY = "custom_api_key" #API key obtained from your Alpaca account
SECRET_KEY = "custom_secret_key #secret key obtained from your Alpaca account
TICKERS = ["AAPL", "TSLA", "NVDA"] #tickers to receive data for, up to 30 max for free accounts

def custom_strategy(context):
    quotes, positions = context['get_current_data]()
    #some custom trading logic
    return order_dict

def shutdown_strategy(context):
    #custom shutdown logic
    return order_dict 

if __name__ == "__main__": 


    run_trading(
        strategy_callable = custom_strategy,
        trade_log_name = "custom_strat_name",
        trade_log_path = log_dir,
        api_key = API_KEY,
        secret_key = SECRET_KEY,
        tickers = TICKERS,
        frequency = 10, 
        run_seconds = 60 
        eos_behavior = "custom",
        custom_shutdown_action = shutdown_strategy, 
        warm_up_period = 10, 
        live_display = True, 
        display_refresh_rate = 0.05, 
        max_trade_updates = 20, 
    )

Rate Limiting and Retry Logic

Free tier customers of Alpaca are limited to 200 order submissions per minute. This system automatically handles overflow of orders per minute, and the orders will wait in the order they arrived until the rate replenishes. Future versions will include automatic toggle of this rate limit to a higher value for paying-tier Alpaca users, but for now if you have the paid tier you will need to go in and change the max trades per minute manually.

Additionally retry logic includes exponential backoff. For example if a POST request to Alpaca fails, the backoff time between retries will double after every fail, up to a maximum of three attempts. If you wish to allow more retries, it can be changed manually in the code (but not from the pip-installed package).

Trade Logs

Every order processed in a given trading session will be saved to "/Users/username/log_dir/trade_log_name.csv". These order updates come from the Alpaca trade update stream, and will include timestamps for pending_new, new, and filled for each order. The client_order_id field will also list the action as well as the execution worker that submitted the order. Trade logs will be saved as follows:

type,client_order_id,ticker,executed_price,executed_qty,status,side,action,timestamp,raw_status
execution,buy_long-7-2304b6b1,DIA,0.0,0.0,pending_new,buy,buy_long,1766515599.059126,OrderStatus.PENDING_NEW
execution,buy_long-7-2304b6b1,DIA,0.0,0.0,new,buy,buy_long,1766515599.067043,OrderStatus.NEW
execution,buy_long-7-2304b6b1,DIA,484.74,1.0,filled,buy,buy_long,1766515599.236655,OrderStatus.FILLED

In this case, execution worker 7 submitted this order.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

blockpaca-0.1.2.tar.gz (24.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

blockpaca-0.1.2-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

Details for the file blockpaca-0.1.2.tar.gz.

File metadata

  • Download URL: blockpaca-0.1.2.tar.gz
  • Upload date:
  • Size: 24.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for blockpaca-0.1.2.tar.gz
Algorithm Hash digest
SHA256 90b620f5f1354b274d007fb943ec1296f4314a6e0cf64ecc01755292cc289fbb
MD5 c63fb56b290a2ea7c88826e852b8cfed
BLAKE2b-256 07a19f569770df2866521ff9682ba9a5b00f6fecd81db53cfd54b4e84c3f2b63

See more details on using hashes here.

File details

Details for the file blockpaca-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: blockpaca-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 21.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for blockpaca-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 11e5668c2371645f5fceb214af7fcebafaeed4ad1f0e831586074d06be186add
MD5 d4ea41de8056e42d64858b408dfa47ed
BLAKE2b-256 00842bc156be741f42f776418312816e604cd7895a4410daa766acba245c1971

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page