Brokerage
Functionality
The Brokerage app allows a user to invest in the stock market and monitor the performance of the portfolio. Security is of utmost importance. The portfolio can be browsed while offline. Some non-critical data (e.g., stock watchlist) may be shared in read-only mode with other apps. There is no sharing between user accounts.
Key Behaviors
- Portfolio and transaction data is available for offline access.
- The brokerage firm’s server takes real-world financial actions on behalf of the client and acts as a bridge between the brokerage firm’s backend systems (and databases) and the user’s objects in the Vanadium store.
- The server is the source of truth and the maintainer of the portfolio and the history of transactions. The client app cannot modify the portfolio directly (only display it), it can only submit new transactions for the server to validate, accept or reject, and act upon. The client may also try to cancel a transaction if it is still pending, but the server determines the outcome, accepting or rejecting the cancellation request.
- Transactions can be initiated while offline but they only get acted upon when the server learns about it, directly from the initiating client, or indirectly from other peer clients that relayed the transaction to the server.
- The object’s VOM values of critical objects are encrypted in the store using a shared secret per user. Store queries are thus limited to object keys.
- A transaction object goes through state mutations enforced by the app and the server:
- Client creates a transaction in “open” state.
- Client moves a transaction to “submitted” state to trigger server action on it.
- Server moves a transaction to “accepted” state to indicate it is now official.
- Server moves a transaction to “done” (purchase or sale completed), “rejected”, or “cancelled” states.
- Client may move a transaction to “cancel” state while still in “submitted” or “accepted” states.
- The server resolves race conditions in state transitions (e.g. cancel or done) and the server is the source of truth. This means business logic is needed at the server for conflict resolution, the default policies are not sufficient.
- Portfolio updates are transactional across objects and across collections: add shares, remove cash, and update the status of the financial transaction (remove it from the “Transactions” collection, insert a historical record in the “Portfolio” under the “Completed-Transactions” directory.
- Transactions are ordered by their visibility at the server, the local creation time at the client is only informational.
- Security prices are updated by the server (e.g. periodically while the market is open for that security).
- The watchlist is a shared read-only copy of the updated stock prices, created locally by the app in a separate directory, based on the security prices received from the server. The app may update this copy at a slower frequency than the server’s updates of the security prices.
- User confusion when issuing dependent transactions from different devices, some online and some offline, is their own problem and may result in transactions being rejected (e.g. lack of funds). The state of the portfolio at the server is always valid and the time ordering of transactions at the server is compatible with the portfolio state.
- All critical objects are only accessible by the
/ and / principals. - Non-critical objects used for cross-app sharing are not encrypted and have read-only access for a selected list of
/ principals chosen by the user. For this to work the shared object schemas have to be known by these apps and modified in future versions in cross-app backward-compatible manner. It might be easier for app developers to skip that and rely on APIs for cross-app data sharing. - For extra safety, when a transaction reaches a terminal state (done, rejected, cancelled) the server makes it read-only.
Schema
Data Types
// The IDs are store keys.
type SecurityID string
type BatchID string
type TxID string
type TxState int
type TxType int
const Cash = SecurityID(“Cash”)
const NoBatch = BatchID(“”)
const (
Open TxState = iota
Submitted
Accepted
Done
Rejected
Cancel
Cancelled
)
const
Buy TxType = iota
Sell
Dividend
ShortTermCapGain
LongTermCapGain
Interest
// other complicated transaction types...
)
type AssetBatch struct {
ID BatchID
Security SecurityID
NumShares int32
Price float32
Date int64
}
type Transaction struct {
ID TxID
State TxState
Type TxType
Security SecurityID
NumShares int32
FromBatch BatchID // when selling
DateOpened int64
DateAccepted int64
DateClosed int64 // done, rejected, cancelled
}
type SecurityMeta struct {
Security SecurityID
Name string
// other quasi-static info...
}
type SecurityPrice struct {
Price float32
Date int64
}
type WatchlistEntry struct {
Security SecurityID
Name string
Price float32
Date int64
}
Organization
Collection <deckId>
Brokerage/
<user>/ // database
Portfolio/ // collection (user: R, server: RW)
Assets/
<security-ID>/
<batch-ID> // type AssetBatch
Completed-Transactions/
<tx-ID> // type Transaction
Securities/
<security-ID>/
metadata // type SecurityMeta
price // type SecurityPrice
Transactions/ // collection (user: RW, server: RW)
<tx-ID> // type Transaction
Shared/ // collection (user: RW)
Watchlist/
<security-ID> // type WatchlistEntry
There is one app database (“Brokerage/
- “Portfolio”: joined by all the user’s devices (read-only) and the brokerage firm’s server (read-write). It also contains completed transactions, a read-only historical record.
- “Transactions:” joined by all the user’s devices (read-write) and the brokerage firm’s server (read-write). These are the inflight transactions created by the user and not yet finalized by the server.
- “Shared”: joined by the user’s devices (read-write) and is locally managed by the app without involving the firm’s server.
In the “Transactions” collection, the user writes or updates objects in these cases:
- Create a transaction.
- Cancel a transaction.
Some transaction types cannot be entered by the user, they exist for the server to report automatically created transactions (e.g. dividends).
During app-to-app synchronization, the peers accept each other’s data based on the default last-timestamp-wins policy.
Conflicts
During app-to-server synchronization, the server code handles its own conflict resolution, after decrypting the objects, applying business rules, and validating changes transactionally with the firm’s databases. It rejects invalid transactions, decides whether a “cancel” arrived before the transaction was done, and updates the objects under the “Assets” directory. The server also updates the objects under the “Securities” directory, filling the security metadata if needed, and updating the security prices.