Welcome back to the Build Your Own Brokerage series. The last article enabled brokerage account funding via automated clearing house relationships. Now that funds can move in and out of the brokerage, let’s implement the functionality to move assets between accounts inside the brokerage.
Alpaca-py implements cash and securities transfers between accounts in the Journals API. Let’s start the coding so we can understand what this API does. The end product can be found on Alpaca’s GitHub.
Implementing Journals API
As the name suggests, the Journals API allows users to create journals. You can think of a journal as a transaction that moves assets between two accounts. There are two types of journals: cash journals and security journals. When creating journals, you can make one request for each journal (from one account to one account) at a time or make one request for a series of journals. One way Journals API allows for a series of journals to be made with one request is through batch journals. Batch journals enable you to journal from one account to many accounts at the same time. Let’s start by implementing one-to-one Journals.
One-to-One Journals
Starting off with the schema, the API docs showcase what a sample request body looks like. Write your schema to model that example.
class JournalParams(BaseModel):
from_account: str
to_account: str
entry_type: str
amount: Optional[float]
symbol: Optional[str]
qty: Optional[float]
The entry type of a journal dictates whether it’s a securities or cash transfer. Entry types are located inside alpaca.broker.enums and can be imported as such. Create a dictionary for these enums inside of your utils module.
from alpaca.broker.enums import JournalEntryType
journal_entry_type = {
"JNLC": JournalEntryType.CASH,
"JNLS": JournalEntryType.SECURITY
}
This will simplify the validation of entry types on incoming requests. You may have noticed that cash and security journals have different required fields in their request bodies. Write a quick validator that verifies the entry type is valid and that the corresponding fields exist.
def validate_journal_request(request_params: Union[schemas.JournalParams, schemas.JournalEntry]):
if request_params.entry_type not in journal_entry_type:
raise HTTPException(status_code=422, detail="Journal entry type must be JNLC or JNLS")
if isinstance(request_params, schemas.JournalParams):
if request_params.entry_type == "JNLC" and request_params.amount is None:
raise HTTPException(status_code=400, detail="Cash journals require amount in the request")
elif request_params.entry_type == "JNLS" and (request_params.symbol is None or request_params.qty is None):
raise HTTPException(status_code=400, detail="Security journals require symbol and qty")
The route for this endpoint takes the request body and the request itself as parameters inside a POST request. Pass these parameters into the CRUD function that will be implemented next and return the response.
# Create a Journal between two accounts
@router.post("/journals")
async def create_journal(request_params: schemas.JournalParams, request: Request):
journal = crud.create_journal(request_params, request)
return journal
Inside the create_journal function, validate the access token and request parameters before any operations happen. Send the valid request via the broker client as in this code snippet and return the response.
def create_journal(request_params: schemas.JournalParams, request: Request):
access_token = request.headers.get('access-token')
utils.authenticate_token(access_token)
utils.validate_journal_request(request_params)
entry_type = constants.journal_entry_type[request_params.entry_type]
if request_params.entry_type == "JNLC":
journal_data = CreateJournalRequest(
from_account=request_params.from_account,
entry_type=entry_type,
to_account=request_params.to_account,
amount=request_params.amount
)
else: # Is security journal
journal_data = CreateJournalRequest(
from_account=request_params.from_account,
entry_type=entry_type,
to_account=request_params.to_account,
symbol=request_params.symbol,
qty=request_params.qty
)
return journal_data
Your basic journals are now up and running! Let’s now start on batch journaling.
One-to-Many Journals (Batch Journals)
A popular use case of batch journaling is a stock or cash sign-up incentive. Instead of sending a new request every single time your service gets a sign-up and overloading your API, one can compile all the new sign-ups and create a single batch journal request to get the job done.
Referencing the API docs, we’ll model the schemas we need after what we see in the request body.
class JournalEntry(BaseModel):
to_account: str
amount: Optional[float]
symbol: Optional[str]
qty: Optional[float]
class BatchJournalParams(BaseModel):
entry_type: str
from_account: str
entries: List[JournalEntry]
The route for batch routes takes a BatchJournalParams object and the request itself as parameters, passes both of those into the corresponding CRUD function, and returns the result.
# Batch journal from one account to many
@router.post("/journals/batch")
async def create_batch_journal(request_params: schemas.BatchJournalParams, request: Request):
batch_journal = crud.create_batch_journal(request_params, request)
return batch_journal
Validate the access token and request fields when implementing the CRUD functionality. Then create a BatchJournalRequestEntry for each journal entry in the request body and compile them into a list. Creating journal entries will differ based on if it’s a cash or securities journal, so use a helper function to keep your main definition clean.
def create_batch_entry(entry_type: str, entry: schemas.JournalEntry):
if entry_type == "JNLC":
to_account, amount = entry.to_account, entry.amount
batch_journal_request = BatchJournalRequestEntry(to_account=to_account, amount=amount)
else: # entry_type == "JNLS"
to_account, symbol, qty = entry.to_account, entry.symbol, entry.qty
batch_journal_request = BatchJournalRequestEntry(to_account=to_account, symbol=symbol, qty=qty)
return batch_journal_request
Once you have the list, create a CreateBatchJournalRequest from it and send the request as in this code snippet. Returning the response completes this function.
def create_batch_journal(request_params: schemas.BatchJournalParams, request: Request):
access_token = request.headers.get('access-token')
utils.authenticate_token(access_token)
utils.validate_journal_request(request_params)
entry_type = constants.journal_entry_type[request_params.entry_type]
batch_entries = []
for entry in request_params.entries:
batch_journal_request = utils.create_batch_entry(request_params.entry_type, entry)
batch_entries.append(batch_journal_request)
batch_journal_data = CreateBatchJournalRequest(
entry_type=entry_type,
from_account=request_params.from_account,
entries=batch_entries
)
broker_client = get_broker_client()
batch_journal = broker_client.create_batch_journal(batch_data=batch_journal_data)
return batch_journal
Your basic journal endpoints are now done. Let’s see some example requests of these.
Example Journals
We’ll showcase three types of requests to better understand how these endpoints work. These are
- Cash Journal
- Security Journal
- Batch Journal
Cash Journal
Here’s an example request and response that journals $20 from one account to another.
Request body:
{
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"amount": "20",
"entry_type": "JNLC"
}
Response:
{
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"entry_type": "JNLC",
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"amount": 20.0,
"symbol": null,
"qty": null,
"description": null,
"transmitter_name": null,
"transmitter_account_number": null,
"transmitter_address": null,
"transmitter_financial_institution": null,
"transmitter_timestamp": null
}
Security Journal
This request journals one share of Apple Inc ($AAPL) from one account to another.
Request body:
{
"from_account": "3d0275ab-ecfb-4518-9382-2d2559207328",
"to_account": "0d46fe45-7dc4-4871-b54e-b99d186b62ea",
"entry_type": "JNLS",
"symbol": "AAPL",
"qty": 1
}
Response:
{
"from_account": "3d0275ab-ecfb-4518-9382-2d2559207328",
"entry_type": "JNLS",
"to_account": "0d46fe45-7dc4-4871-b54e-b99d186b62ea",
"amount": null,
"symbol": "AAPL",
"qty": 1.0,
"description": null,
"transmitter_name": null,
"transmitter_account_number": null,
"transmitter_address": null,
"transmitter_financial_institution": null,
"transmitter_timestamp": null
}
Batch Journal
This request sends varying amounts of cash from one account to many.
Body:
{
"entry_type": "JNLC",
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"entries": [
{
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"amount": "10"
},
{
"to_account": "ae85a93b-3509-47a0-bcec-57f55a5d33a2",
"amount": "15"
},
{
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"amount": "20"
}
]
}
Response:
[
{
"id": "cd4b74f3-cf2b-48e2-bfff-b601483fa34d",
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"entry_type": "JNLC",
"status": "queued",
"net_amount": 10.0,
"symbol": "",
"qty": null,
"price": 0.0,
"description": "",
"settle_date": null,
"system_date": null,
"transmitter_name": null,
"transmitter_account_number": null,
"transmitter_address": null,
"transmitter_financial_institution": null,
"transmitter_timestamp": null,
"error_message": ""
},
{
"id": "fc9ef04f-dc0d-43f2-a457-f5ad1f2cbdfe",
"to_account": "ae85a93b-3509-47a0-bcec-57f55a5d33a2",
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"entry_type": "JNLC",
"status": "queued",
"net_amount": 15.0,
"symbol": "",
"qty": null,
"price": 0.0,
"description": "",
"settle_date": null,
"system_date": null,
"transmitter_name": null,
"transmitter_account_number": null,
"transmitter_address": null,
"transmitter_financial_institution": null,
"transmitter_timestamp": null,
"error_message": ""
},
{
"id": "489f45e0-4921-4ed9-adbe-6f0286c1f8d3",
"to_account": "b13b2282-c0a9-4b7d-b312-d7c267bb8a03",
"from_account": "f365fcda-804f-4e3d-8741-8e2e20ff3264",
"entry_type": "JNLC",
"status": "queued",
"net_amount": 20.0,
"symbol": "",
"qty": null,
"price": 0.0,
"description": "",
"settle_date": null,
"system_date": null,
"transmitter_name": null,
"transmitter_account_number": null,
"transmitter_address": null,
"transmitter_financial_institution": null,
"transmitter_timestamp": null,
"error_message": ""
}
]
Conclusion
Today we’ve covered what journals are, how the Journals API can be implemented, how to validate requests with optional fields, and some examples of how one could communicate with the implemented endpoints. Journaling is a crucial part of any brokerage and now you have the tools to integrate it into your backend if you haven’t yet. While you wait for the next part of this series, check out Getting Started with Local Currency Trading API to localize your app.
Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member FINRA/SIPC, a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.
Please note that this article is for informational purposes only. The example above is for illustrative purposes only. Actual crypto prices may vary depending on the market price at that particular time. Alpaca Crypto LLC does not recommend any specific cryptocurrencies.
Cryptocurrency is highly speculative in nature, involves a high degree of risks, such as volatile market price swings, market manipulation, flash crashes, and cybersecurity risks. Cryptocurrency is not regulated or is lightly regulated in most countries. Cryptocurrency trading can lead to large, immediate and permanent loss of financial value. You should have appropriate knowledge and experience before engaging in cryptocurrency trading. For additional information please click here.
Cryptocurrency services are made available by Alpaca Crypto LLC ("Alpaca Crypto"), a FinCEN registered money services business (NMLS # 2160858), and a wholly-owned subsidiary of AlpacaDB, Inc. Alpaca Crypto is not a member of SIPC or FINRA. Cryptocurrencies are not stocks and your cryptocurrency investments are not protected by either FDIC or SIPC. Please see the Disclosure Library for more information.
This is not an offer, solicitation of an offer, or advice to buy or sell cryptocurrencies, or open a cryptocurrency account in any jurisdiction where Alpaca Crypto is not registered or licensed, as applicable.