currency-market
A synchronous implementation of a limit order based currency market
Installation
npm install currency-market
Usage
The currency-market
package is intended to be used by a system of components that need to be synchronized. Those components are
- A number of front ends that generate operations and allow the state of the market to be queried
- An operation hub that accepts the operations and ensures they are submitted to order matching engines in a reproducible order
- A number of identical order matching engines that process the operations and calculate the new state of the market for each one
- A delta hub that receives the market state deltas and distributes them to the front ends
On intialization
- Engines will be constructed (perhaps from a previous engine state) and when requested will send the state to the delta hub
var Engine = require('currency-market').Engine;var Amount = require('currency-market').Amount; var COMMISSION_RATE = new Amount('0.001');var COMMISSION_REFERENCE = '0.1%'; var engine = new Engine({ commission: { account: 'commission' calculate: function(params) { return { amount: params.amount.multiply(COMMISSION_RATE), reference: COMMISSION_REFERENCE }; } }, json: previousEngineState }); ... sendStateToDeltaHub(JSON.stringify(engine));
- The delta hub will request the state from an engine and forward that to the front ends when they start up
var State = require('currency-market').State; getStateFromEngine(function(receivedEngineState){ var state = new State({ commission: { account: 'commission' }, json: receivedEngineState });}); ... sendStateToFrontEnd(JSON.stringify(state));
- Front ends will request the state and then construct a state from the JSON state they receive from the delta hub
var State = require('currency-market').State; getStateFromDeltaHub(function(receivedState){ var state = new State({ commission: { account: 'commission' }, json: receivedState });});
Then the life cycle of an operation is as follows
- A front end constructs an operation and sends it to an operation hub
var Operation = require('currency-market').Operation;var Amount = require('currency-market').Amount; var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', deposit: { currency: 'EUR', amount: new Amount '500' }}); sendToOperationHub(JSON.stringify(operation));
- The operation hub receives the operation, accepts it with a seqence number and timestamp and forwards it to the engine instances
var Operation = require('currency-market').Operation; var operation = new Operation({ json: receivedJSON }); operation.accept({ sequence: 654852, timestamp: 1371737390976}); sendToEngines(JSON.stringify(operation));
- The engines receive the operation, process it and send market state deltas to the delta hub
var Operation = require('currency-market').Operation; var operation = new Operation({ json: receivedJSON }); delta = engine.apply(operation); sendToDeltaHub(JSON.stringify(delta));
- The delta hub receives the deltas, processes the first of each one it receives to update its own state and sends that delta on to the front ends
var Delta = require('currency-market').Delta; var delta = new Delta({ json: receivedJSON }); state.apply(delta); sendToFrontEnds(JSON.stringify(delta));
- The front ends receive the deltas and apply them to their own state so that they can respond to queries with the new information
var Delta = require('currency-market').Delta; var delta = new Delta({ json: receivedJSON }); state.apply(delta); var funds = state.getAccount('Peter').getBalance('EUR').funds
API
All functions complete synchronously and throw errors if they fail.
Amount
Amount
handles large numerical arithmetic accurately (unlike the built in Javascript number implementation). It is used for all amount and price values and is provided as a utility for applications to apply the same arimthmetic functionality in their own contexts.
Amount
instances are immutable.
Divisions are carried out to an arbitrary precision of 25 decimal points.
var Amount = require('currency-market').Amount;// Always initialise from a string representation of a numbervar amount1000 = new Amount('1000');var amount200 = new Amount('200');// multiply 2 amountsvar amount200000 = amount1000.multiply(amount200);// add 2 amountsvar amount1200 = amount1000.add(amount200);// subtract 2 amountsvar amount800 = amount1000.subtract(amount200);// divide 2 amountsvar amountPoint2 = amount200.divide(amount1000);// Return the string representation of an amountvar str1000 = amount1000.toString();// Compare 2 valuesamount1000.compareTo(amount200) > 0;amount200.compareTo(amount1000) < 0;amount200.compareTo(amount200) == 0;// 2 Identity constants are definedvar zero = Amount.ZERO;var one = Amount.ONE;
Engine
var Engine = Engine;
Engine
instances accept operations and return deltas that can be applied to simplified State
instances.
Constructor
// Define a commission rate of 0.5%var COMMISSION_RATE = '0.005'; // instantiate an enginevar engine = // Optionally specify how commission should be applied to credits resulting from trades. // If this is not specified then no commission will be charged commission: // The ID of the account to receive the commission account: 'commission' // The callback to use for calculating the commission amount to subtract from a credit // resulting from a trade { // A timestamp for the trade being executed var timestamp = paramstimestamp; // The ID of the account that is being credited var account = paramsaccount; // The currency of the credited amount var currency = paramscurrency; // The amount that is being credited as an Amount instance var amount = paramsamount; // Return an object containing the amount of commission to deduct as an Amount // instance and a reference for the commission rate/type being charged // // Note that it's best to avoid divisions when calculating commissions so as // to avoid rounding errors. Also, as the reference is intended be transmitted along // with market deltas, it should be possible to convert it losslessly to and from JSON return amount: amount reference: COMMISSION_RATE + '%' ; } ;
apply
method
The apply
method applies operations and returns the resulting deltas.
Operations and deltas can be converted losslessly to and from JSON for transmission.
If an operation fails for any reason (eg. not enough funds) then an error will be thrown.
Only operations that have been accepted using the Operation.accept
method can be applied to an Engine
instance.
try { var delta = engine.apply(new Operation({ // Operation parameters ... }));} catch(error) { // Possible errors will include invalid parameters or insufficient funds to complete the operation ...}
JSON.stringify
Engines can be converted to and from JSON
var json = JSON.stringify(engine);var engine = new Engine({ commission: { account: 'commission', calculate: function(params) { return { amount: amount.multiply(COMMISSION_RATE), reference: COMMISSION_RATE + '%' }; } }, json: json}); // OR var json = JSON.stringify(engine);var engine = new Engine({ commission: { account: 'commission', calculate: function(params) { return { amount: amount.multiply(COMMISSION_RATE), reference: COMMISSION_RATE + '%' }; } }, exported: JSON.parse(json)});
Delta
var Delta = Delta;
Delta
instances are returned by Engine
instances after successfully applying operations. They can be converted to JSON, transmitted, reconstructed from JSON and applied to State
instances.
All deltas have the following properties
// The delta sequence number. These will be generated consecutively by the engine// for successful operations. As such they will not be synchronized with operation sequence// numbers due to the possibility of operations throwing errorsvar sequence = delta.sequence; // The operation instance as supplied to the `apply` methodvar operation = delta.operation; // Additional state change information in a format specific to the operation typevar result = delta.result;
JSON.stringify
Deltas can be converted to and from JSON
var json = JSON.stringify(delta);var delta = new Delta({ json: json}); // OR var json = JSON.stringify(delta);var delta = new Delta({ exported: JSON.parse(json)});
Operation
var Operation = Operation;
Operation
instances are submitted to Engine
instances to apply operations. They can be converted to JSON, transmitted and reconstructed from JSON
All operations follow this pattern
var operation = new Operation({ // Application specified reference that is returned untouched with the operation details included in the delta. // Care should be taken to ensure that this too can be converted to and from JSON reference: '550e8400-e29b-41d4-a716-446655440000', // The ID of the account submitting the operation account: 'Peter', // The operation details, the name of this property will determine the type of the operation // and what additional fields need to be supplied operationType: { // Operation parameters ... }});
accept
method
Operations must be accepted before they can be applied to an Engine
instance to ensure they are associated with a sequence number and a timestamp
operation.accept({ // The operation sequence number. These must be consecutive for consecutive operations sequence: 123456, // The timestamp for the operation as a Unix time since epoch in milliseconds timestamp: 1371737390976});
deposit
operation
Deposit funds into an account
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // deposit 1000 Euros to account ID 'Peter' deposit: { currency: 'EUR', amount: new Amount('1000') }}); // On successful application the `delta.result` will have the following fields // The new level of funds in the deposited currency as an `Amount` instancevar funds = delta.result.funds
withdraw
operation
Withdraw funds from an account
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // withdraw 1000 Euros from account ID 'Peter' withdraw: { currency: 'EUR', amount: new Amount('1000') }}); // On successful application the `delta.result` will have the following fields // The new level of funds in the deposited currency as an `Amount` instancevar funds = delta.result.funds
submit
operation
Submit orders to the market. Both bid and offer orders can be submitted and follow this pattern
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // Place a bid order for 10 BTC offering 100 Euros per BTC submit: { // order parameters ... }}); // On successful application the `delta.result` will have the following fields // The new level of locked funds in the order's offer currencyvar lockedFunds = delta.result.lockedFunds // Note that only one of `nextHigherOrderSequence` or `trades` will be set // If the order is not at the top of the order book then the sequence number// of the next order above it is returned. This is a hint to optimize the// insertion of the order into a `State` instancevar nextHigherOrderSequence = delta.result.nextHigherOrderSequence; // If the order was inserted at the top of the order book then an array of trades// will be returned. This array will still be set, but will be empty, if no actual // trades were made//// Note that the price at which any trade was executed will be given by the bid or// offer price associated with the `right` order and that the volume traded in each// currency is most easily referenced by the debit amounts associated with the `left`// and `right` accounts in their respective order's offer currenciesvar trades = delta.result.trades; // `left` gives the changes to be applied to the order that was submitted and the // account that submitted it var left = trades[0].left; // Only one of `left` or `right` will have a remainder and this // signals the amount of the order that has not yet been executed. // When no remainder is specified it signals that the order was // completely executed. It is possible that neither `left` nor `right` // will have a remainder if they completely satisfy each other var remainder = left.remainder; // The remaining bidAmount on the order var bidAmount = remainder.bidAmount; // The remaining offerAmount on the order var offerAmount = remainder.offerAmount; // The transaction fields signal by how much the account balances have changed // and how much commission was applied var transaction = left.transaction; // The changes applied to the balance being debited var debit = transaction.debit; // The amount of the order's offer currency debited from the account var amount = debit.amount; // The new level of funds in the debited currency var funds = debit.funds; // The new level of locked funds in the debited currency var lockedFunds = debit.lockedFunds; // The changes applied to the balances being credited var credit = transaction.credit; // The amount of the order's bid currency credited to the account var amount = credit.amount; // The new level of funds in the credited currency var funds = credit.funds; // If the engine was instantiated with commission then the commission // field will be set var commission = credit.commission; // The amount of the order's bid currency credited to the commission account var amount = commission.amount; // The new level of funds in the order's bid currency in the commission account var funds = commission.funds; // The reference associated with the commission calculation var reference = commission.reference; // `right` gives the changes to be applied to the order that was matched and the // account that submitted it. This order will always be the order that is currently // at the top of the opposing order book to that which the submitted order was added. // The fields that can be set are the same as for `left` var right = trades[0].right;
Bid orders
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // Place a bid for 10 BTC offering 100 Euros per BTC submit: { bidCurrency: 'BTC', offerCurrency: 'EUR', bidPrice: new Amount('100'), bidAmount: new Amount('10') }});
Offer orders
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // Place an offer of 1000 EUR bidding 0.01 BTC per EUR submit: { bidCurrency: 'BTC', offerCurrency: 'EUR', offerPrice: new Amount('0.01'), offerAmount: new Amount('1000') }});
cancel
operation
Orders can be cancelled using the cancel operation.
var operation = new Operation({ reference: '550e8400-e29b-41d4-a716-446655440000', account: 'Peter', // Cancel the order submitted by acount ID 'Peter' with operation sequence 615368 cancel: { sequence: 615368 }}); // On successful application the `delta.result` will have the following fields // The new level of locked funds in the order's offer currencyvar lockedFunds = delta.result.lockedFunds;
JSON.stringify
Operations can be converted to and from JSON
var json = JSON.stringify(operation);var operation = new Operation({ json: json}); // OR var json = JSON.stringify(operation);var operation = new Operation({ exported: JSON.parse(json)});
State
var State = State;
State
instances provide simplified access to a market state. They do not contain the logic for matching orders but do accept Engine
generated deltas to keep them synchronized with Engine
instances
Constructor
// instantiate a statevar state = commission: // Note that the commission acount name should match the commission // acount name from the engine to which this state will be synchronised account: 'commission'; // instantiate a state from JSON stringified enginevar state = commission: account: 'commission' json: JSON;
apply
method
The apply
method is used to apply deltas generated by Engine
instances. The delta will be applied synchronously and errors may be thrown
try { state.apply(delta);} catch(error) { // possible errors include out of sequence deltas ...}
JSON.stringify
States can be converted to and from JSON
var json = JSON.stringify(state);var state = new State({ json: json}); // OR var json = JSON.stringify(state);var state = new State({ exported: JSON.parse(json)});
getBook
method
The getBook
method gives access to the order books keyed by bid and offer currency. Each book is an Array
of orders sorted as they will be matched for execution
var book = state.getBook({ bidCurrency: 'BTC', offerCurrency: 'EUR'}); // Get the top of the order bookvar order = book[0]; // All orders have the following fields//// The sequence numbervar sequence = order.sequence;// The timestamp in milliseconds since epochvar timestamp = order.timestamp;// The account ID associated with the ordervar account = order.account;// The offer currencyvar offerCurrency = order.offerCurrency;// The bid currencyvar bidCurrency = order.bidCurrency; // Bid orders have the following additional fields // The bid price as an `Amount` instancevar bidPrice = order.bidPrice;// The bid amount as an `Amount` instancevar bidAmount = order.bidAmount; // Offer orders have the following additional fields // The offer price as an `Amount` instancevar offerPrice = order.offerPrice;// The offer amount as an `Amount` instancevar offerAmount = order.offerAmount;
getAccount
method
The getAccount
method gives access to the accounts as instances of Account
by account ID
var account = state.getAccount('Peter');
Account
The Account
class gives access to the properties of an account
orders
property
This is a collection of active orders keyed by sequence number (NB. it is an Object
and not an Array
)
var order = account.orders[5];
The orders are the same instances as those in the books retrieved with the getBook
method
getBalance
method
The getBalance
method gives access to the balances of funds as instances of Balance
associated with an account, keyed by currency
var balance = account.getBalance('EUR');
Balance
The Balance
class gives access to the levels of funds and locked funds (when orders are outstanding) as Amount
instances
var funds = balance.funds;var lockedFunds = balance.lockedFunds;
Roadmap
- When applying a delta to a state we should get back a flag to say whether the delta was applied so that we know if the delta was old or not
- The state should emit an event when a deposit is recorded so that it can also be recorded in more permanent storage
- The state should record the last N deposits for each account so that these can be quickly queried without looking at permanent storage
- The state should emit an event when a withdrawal is recorded so that it can also be recorded in more permanent storage
- The state should record the last N withdrawals for each account so that these can be quickly queried without looking at permanent storage
- The state should emit an event when a trade is recorded so that it can also be recorded in more permanent storage
- The state should record the last N trades for each account so that these can be quickly queried without looking at permanent storage
- Setting commission for an account/balance/globally should be an operation so that changes can be reflected in the history
- Commission types should be predefined and parameterized
- The commission account should be fixed?
- Instant orders
- Market orders
- zero priced offers that are rejected if they cannot be completely filled by the market
- if a zero price is used then any remainder cannot be left on the book as it may cause a division by zero
- partial fills could be executed as long as the remainder is instantly cancelled
- zero priced offers that are rejected if they cannot be completely filled by the market
- Fill or Kill limit orders?
- will be tougher (less efficient) than market orders as an average price will have to be calculated?
- Market orders
- Pluggable rounding policies
- Amount factory required?
- currently we only round down debits and credits so as not to debit more funds than available
- current rounding is done to an arbitrary scale of 25
- Separate transaction IDs and sequence IDs
- Use sequence numbers instead of transaction IDs so the engine knows that it hasn't missed anything?
- Use both sequence numbers and transaction IDs?
- transaction IDs provide replayability
- have to be unique forever (uuid?)
- sequence numbers provide integrity checking
- may get unweildy if required to be unique forever and could loop instead
- where are sequence numbers assigned?
- This implies a state somewhere and a centralised component (bottleneck?)
- transaction IDs provide replayability
- Protection against attacks?
- entering orders that satisfy each other
- entering tiny orders
- should this be in another layer?
Contributing
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality.
Run tests with
$ npm test
Run performance tests with
$ npm run-script perf
License
Copyright © 2013 Peter Halliday
Licensed under the MIT license.