Skip to content
This repository has been archived by the owner on May 29, 2021. It is now read-only.

Commit

Permalink
Cryptocurrency support for all APIs, including market buy/sell (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedsakr authored Jan 24, 2021
1 parent 4051855 commit 4124876
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 48 deletions.
6 changes: 5 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
src/
docs/
docs/
Contributing.md
webpack.config.js
.eslintrc.json
.babelrc
4 changes: 2 additions & 2 deletions dist.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/quotes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Attach a custom quote provider for the specified exchange.

**Note**: the `orders` module makes use of the `quotes` module for its orders.`marketBuy` and orders.`marketSell` APIs. A custom quote provider will be utilized in a market buy or sell if the security trades on the specified exchange.

**Caution**: You CANNOT configure a custom provider for cryptocurrencies at this moment. Wealthsimple Trade servers seem to not honour the limit price provided for cryptocurrencies and execute them at their best price.

[View examples](/docs/quotes/examples.js)

See also: [orders.`marketBuy`](/docs/orders/README.md#orders-marketBuy), [orders.`marketSell`](/docs/orders/README.md#orders-marketSell)
Expand Down
23 changes: 13 additions & 10 deletions docs/ticker.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# Ticker

Wealthsimple Trade supports securities from 5 exchanges:
* `NASDAQ`
* `NYSE`
* `TSX`
* `TSX-V`
* `NEO`

You will notice that some APIs require a ticker as a parameter. `wstrade-api` has incorporated a non-ambiguous way of identifying a security through its `symbol`, `exchange`, or `id`. This document will provide guidance on how you can specify the ticker argument for such APIs.

## Understanding exchange

An exchange is a platform that a security is traded on. Wealthsimple Trade supports:
* **Conventional securities** from 5 exchanges: `NASDAQ`, `NYSE`, `TSX`, `TSX-V`, and `NEO`
* **Cryptocurrencies** through undisclosed exchanges, but you **must** specify `CC` as the exchange when creating a cryptocurrency ticker within `wstrade-api`.

Here are the different ways you can construct a Ticker for `wstrade-api`

## 1. Ticker as string

You can simply specify the ticker argument as a string of the security `symbol` alone, or a composite of `symbol` and `exchange`, separated by a colon (`:`). Here are a few examples of valid tickers:
* `AAPL`
* `AAPL:NASDAQ`
* `SU:TSX`
* `BB`
* `BTC:CC`

The composite `symbol`:`exchange` format is useful for disambiguating which stock you are referring to. You will find that some distinct securities trade with the same ticker on different exchanges. As a result, it is recommended that you provide the `exchange` as much as you are able to.

Expand All @@ -24,8 +26,9 @@ The composite `symbol`:`exchange` format is useful for disambiguating which stoc
You can also provide the ticker argument as an object of `symbol` and `exchange`, or `id`. Here are a few examples of valid tickers as objects:
* `{ symbol: 'AAPL' }`
* `{ symbol: 'AAPL', exchange: 'NASDAQ' }`
* `{ symbol: 'SU', exchange: 'TSX' }`
* `{ symbol : 'BB' }`
* `{ id: 'sec-s-76a7155242e8477880cbb43269235cb6' }`
* `{ symbol: 'SU', exchange: 'TSX' }`
* `{ symbol : 'BB' }`
* `{ id: 'sec-s-76a7155242e8477880cbb43269235cb6' }`
* `{ symbol: 'BTC', exchange: 'CC' }`

The `id` shown in the last example is the internal unique security id that Wealthsimple Trade assigns to each security, and can be retrieved from the [data.`getSecurity`](/docs/data/README.md#data-getSecurity) API.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wstrade-api",
"version": "1.2.1",
"version": "1.3.0",
"description": "Wealthsimple Trade API Wrapper",
"main": "dist.js",
"scripts": {
Expand Down
46 changes: 39 additions & 7 deletions src/core/__test__/ticker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ test('Ticker as symbol only', () => {
expect(ticker.symbol).toBe('AAPL');
expect(ticker.exchange).toBe(undefined);
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('AAPL');
});

Expand All @@ -28,6 +29,7 @@ test('Ticker as symbol and (valid) exchange composite', () => {
expect(ticker.symbol).toBe('AAPL');
expect(ticker.exchange).toBe('NASDAQ');
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('AAPL:NASDAQ');
});

Expand All @@ -40,8 +42,9 @@ test('Ticker as empty object', () => {
test('Ticker as object with symbol only', () => {
const ticker = new Ticker({ symbol: 'AAPL' });
expect(ticker.symbol).toBe('AAPL');
expect(ticker.exchange).toBe(undefined);
expect(ticker.id).toBe(undefined);
expect(ticker.exchange).toBe(null);
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('AAPL');
});

Expand All @@ -50,7 +53,8 @@ test('Ticker as object with symbol and exchange', () => {
const ticker = new Ticker({ symbol: 'AAPL', exchange: 'NASDAQ' });
expect(ticker.symbol).toBe('AAPL');
expect(ticker.exchange).toBe('NASDAQ');
expect(ticker.id).toBe(undefined);
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('AAPL:NASDAQ');
});

Expand All @@ -62,16 +66,36 @@ test('Ticker with NEO exchange is internally mapped to full name', () => {
expect(ticker.symbol).toBe('CYBN');
expect(ticker.exchange).toBe('AEQUITAS NEO EXCHANGE');
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('CYBN:NEO');
});

test('Ticker with CC exchange is treated as cryptocurrency', () => {
const ticker = new Ticker('BTC:CC');
expect(ticker.symbol).toBe('BTC');
expect(ticker.exchange).toBe('CC');
expect(ticker.id).toBe(null);
expect(ticker.crypto).toBe(true);
expect(ticker.format()).toBe('BTC:CC');
});

test('Ticker with id starting with "sec-z" is treated as crypto', () => {
const ticker = new Ticker({ id: 'sec-z-eth-dc40261c82a191b11e53426aa25d91af' });
expect(ticker.symbol).toBe(null);
expect(ticker.exchange).toBe(null);
expect(ticker.id).toBe('sec-z-eth-dc40261c82a191b11e53426aa25d91af');
expect(ticker.crypto).toBe(true);
expect(ticker.format()).toBe('sec-z-eth-dc40261c82a191b11e53426aa25d91af');
});

// The user can specify the internal id of the security instead of
// a symbol.
test('Ticker as internal id', () => {
const ticker = new Ticker({ id: 'sec-s-76a7155242e8477880cbb43269235cb6' });
expect(ticker.symbol).toBe(undefined);
expect(ticker.exchange).toBe(undefined);
expect(ticker.symbol).toBe(null);
expect(ticker.exchange).toBe(null);
expect(ticker.id).toBe('sec-s-76a7155242e8477880cbb43269235cb6');
expect(ticker.crypto).toBe(false);
expect(ticker.format()).toBe('sec-s-76a7155242e8477880cbb43269235cb6');
});

Expand All @@ -85,15 +109,23 @@ test('Weak comparison between two tickers with same symbol but different exchang
});

// Varying symbols will return a false comparison
test('Weak comaprison between two tickers with different symbols', () => {
test('Weak comparison between two tickers with different symbols', () => {
const ticker1 = new Ticker('AAPL');
const ticker2 = new Ticker('SU');
expect(ticker1.weakEquals(ticker2)).toBe(false);
});

// Varying ids will return a false comparison
test('Weak comaprison between two tickers with different ids', () => {
test('Weak comparison between two tickers with different ids', () => {
const ticker1 = new Ticker({ id: 'sec-s-76a7155242e8477880cbb43269235cb6' });
const ticker2 = new Ticker({ id: 'sec-s-72a7155241e8479880cbb43269235cb6' });
expect(ticker1.weakEquals(ticker2)).toBe(false);
});

// Even though weakEquals does not consider exchange, it does differentiate
// between cryptocurrencies and conventional securities.
test('Weak comparison between conventional security and cryptocurrency', () => {
const ticker1 = new Ticker('ETH:CC');
const ticker2 = new Ticker('ETH');
expect(ticker1.weakEquals(ticker2)).toBe(false);
});
18 changes: 13 additions & 5 deletions src/core/ticker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Exchanges supported by Wealthsimple Trade
const exchanges = ['NASDAQ', 'NYSE', 'TSX', 'TSX-V', 'NEO'];
// 'CC' is not an exchange; it stands for Crypto currency and
// it allows us to distinguish it from conventional securities.
const exchanges = ['NASDAQ', 'NYSE', 'TSX', 'TSX-V', 'NEO', 'CC'];

/**
* Ticker provides a logical encapsulation for the allowed forms
Expand Down Expand Up @@ -33,6 +35,7 @@ class Ticker {
this.symbol = null;
this.exchange = null;
this.id = null;
this.crypto = false;

if (typeof (value) === 'string') {
// Empty tickers are not allowed
Expand All @@ -47,16 +50,21 @@ class Ticker {
throw new Error(`Invalid ticker '${value}'`);
}

this.symbol = value.symbol;
this.exchange = value.exchange;
this.id = value.id;
this.symbol = value.symbol || null;
this.exchange = value.exchange || null;
this.id = value.id || null;
}

// Guarantee that the exchange is valid if not null
if (this.exchange && !exchanges.includes(this.exchange)) {
throw new Error(`Invalid exchange '${this.exchange}'!`);
}

// Set the crypto property to true to treat this security as cryptocurrency
if (this.exchange === 'CC' || this.id?.startsWith('sec-z')) {
this.crypto = true;
}

// Wealthsimple Trade doesn't have a short exchange id ('NEO') for
// AEQUITAS NEO EXCHANGE for some reason...
// We have to map it to the full name for comparisons to work.
Expand Down Expand Up @@ -93,7 +101,7 @@ class Ticker {
return true;
}

if (this.symbol && this.symbol === other.symbol) {
if (this.symbol && this.symbol === other.symbol && this.crypto === other.crypto) {
return true;
}

Expand Down
4 changes: 3 additions & 1 deletion src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export default {
result = await handleRequest(endpoints.SECURITY, { ticker: ticker.symbol });
result = result.filter((security) => security.stock.symbol === ticker.symbol);

if (ticker.exchange) {
if (ticker.crypto) {
result = result.filter((security) => security.security_type === 'cryptocurrency');
} else if (ticker.exchange) {
result = result.filter((security) => security.stock.primary_exchange === ticker.exchange);
}

Expand Down
25 changes: 11 additions & 14 deletions src/orders/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import handleRequest from '../network/https';
import history from './history';
import data from '../data';
import quotes from '../quotes';
import Ticker from '../core/ticker';

const isCanadianSecurity = (exchange) => ['TSX', 'TSX-V'].includes(exchange);

Expand Down Expand Up @@ -40,7 +41,7 @@ export default {

return handleRequest(endpoints.PLACE_ORDER, {
security_id: details.id,
limit_price: await quotes.get(ticker),
limit_price: !(new Ticker(ticker).crypto) ? await quotes.get(ticker) : undefined,
quantity,
order_type: 'buy_quantity',
order_sub_type: 'market',
Expand Down Expand Up @@ -103,19 +104,15 @@ export default {
* @param {*} ticker The security symbol
* @param {*} quantity The number of securities to purchase
*/
marketSell: async (accountId, ticker, quantity) => {
const details = await data.getSecurity(ticker);

return handleRequest(endpoints.PLACE_ORDER, {
security_id: details.id,
market_value: await quotes.get(ticker),
quantity,
order_type: 'sell_quantity',
order_sub_type: 'market',
time_in_force: 'day',
account_id: accountId,
});
},
marketSell: async (accountId, ticker, quantity) => handleRequest(endpoints.PLACE_ORDER, {
security_id: (await data.getSecurity(ticker)).id,
market_value: !(new Ticker(ticker).crypto) ? await quotes.get(ticker) : undefined,
quantity,
order_type: 'sell_quantity',
order_sub_type: 'market',
time_in_force: 'day',
account_id: accountId,
}),

/**
* Limit sell a security through the Wealthsimple Trade application.
Expand Down
18 changes: 12 additions & 6 deletions src/quotes/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import trade from './default';
import data from '../data';
import Ticker from '../core/ticker';

export default {

Expand Down Expand Up @@ -30,23 +31,28 @@ export default {
* provider if a valid provider is registered for the exchange that the
* ticker trades on.
*
* @param {*} ticker The security to get a quote for.
* @param {*} security The security to get a quote for.
*/
async get(ticker) {
async get(security) {
let exchange = null;
const ticker = new Ticker(security);

// We need the exchange in the next step if the user has specified
// a custom provider for an exchange. So if the user hasn't provided
// it, we will have to ping Wealthsimple trade to get it.
if (ticker.exchange) {
// We need the exchange in the next step if the user has specified
// a custom provider for an exchange. So if the user hasn't provided
// it, we will have to ping Wealthsimple trade to get it.
exchange = ticker.exchange;
} else if (ticker.crypto && ticker.id) {
// If the id is only given but we know it's a crypto id,
// we will automatically set exchange to 'CC'.
exchange = 'CC';
} else if (Object.keys(this.providers).length > 0) {
const info = await data.getSecurity(ticker, false);
exchange = info.stock.primary_exchange;
}

// A custom provider will take precedence over the default source
if (this.providers[exchange]) {
if (exchange in this.providers) {
return this.providers[exchange].quote(ticker);
}
return this.defaultProvider.quote(ticker);
Expand Down

0 comments on commit 4124876

Please sign in to comment.