-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
625 additions
and
205 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,41 @@ | ||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||
## Tastyworks Symbology | ||
This is a demo javascript app to demonstrate how to parse Tastyworks equity and equity option symbols and open a websocket connection with our quote provider. | ||
|
||
## Getting Started | ||
### Equities | ||
Equity symbols contain only alphanumeric characters (A-Z, 0-9) with an occasional `/`. A few examples: | ||
`AAPL` | ||
`BRK/A` | ||
|
||
First, run the development server: | ||
### Equity Options | ||
Tastyworks uses the same conventions as the OCC for equity option symbols. You can read more about OCC symbology [here](https://en.wikipedia.org/wiki/Option_symbol). | ||
|
||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
``` | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
|
||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. | ||
|
||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. | ||
In short, there are 4 pieces that make up an equity option symbol: | ||
1. Root symbol - 6 alphanumeric digits with whitespace padding. | ||
- <code>AAPL </code> | ||
- <code>FB </code> | ||
- <code>BRK/A </code> | ||
2. Expiration date - 6 numeric digits with format `yymmdd`. | ||
3. Option type - `P` or `C` | ||
4. Strike price - 8 numeric digits front-padded with 0s. No decimals. Multiply strike by 1000. | ||
- 64.0 strike: `00064000` | ||
- 1050.55 strike: `01050550` | ||
- 0.50 strike: `00000500` | ||
|
||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | ||
Example equity option symbols: | ||
AAPL June 17, 2022 150 Put: `AAPL 220617P00150000` | ||
SPY Nov 18, 2022 400 Call: `SPY 221118C00400000` | ||
SPX May 20, 2022 4025 Call: `SPXW 220520C04025000` | ||
|
||
## Learn More | ||
## Running Locally | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
Replace the demo `token` and `wsUrl` in `quote-streamer.tsx` with a tastyworks account streamer token and url. | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
To run the development server: | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
``` | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | ||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import styles from '../styles/SubscribedSymbol.module.css' | ||
import { streamer } from '../lib/quote-streamer' | ||
import { useEffect, useState } from 'react' | ||
import _ from 'lodash' | ||
|
||
export default function SubscribedSymbol(props: any) { | ||
const [bidPrice, setBidPrice] = useState(NaN) | ||
const [askPrice, setAskPrice] = useState(NaN) | ||
|
||
const handleEvent = (event: any) => { | ||
setBidPrice(event.bidPrice) | ||
setAskPrice(event.askPrice) | ||
} | ||
|
||
useEffect(() => { | ||
const unsubscribe = streamer.subscribe(props.symbol, handleEvent) | ||
return unsubscribe | ||
}, []); | ||
|
||
return ( | ||
<div className={styles.main}> | ||
<div>{props.symbol}</div> | ||
<div className={styles.rightColumn}> | ||
<div>Bid: {bidPrice}</div> | ||
<div>Ask: {askPrice}</div> | ||
<button onClick={() => props.onRemove(props.symbol)}>Remove</button> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const DX_CRYPTO_SUFFIX = ':CXTALP' | ||
export const CRYPTO_SYMBOL_REGEX = new RegExp(`^[A-Z]+/USD((${DX_CRYPTO_SUFFIX})?)$`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// tslint:disable | ||
import dayjs from 'dayjs' | ||
import utc from 'dayjs/plugin/utc' | ||
import timezone from 'dayjs/plugin/timezone' | ||
dayjs.extend(utc) | ||
dayjs.extend(timezone) | ||
|
||
export type MONTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ||
|
||
export const NEW_YORK_ZONE = 'America/New_York' | ||
|
||
const TRADING_END_HOUR_NEW_YORK = 16 | ||
const TRADING_END_MINUTES_NEW_YORK = 0 | ||
const TRADING_START_HOUR_NEW_YORK = 9 | ||
const TRADING_START_MINUTES_NEW_YORK = 30 | ||
|
||
export default class DateHelper { | ||
private dateTime: dayjs.Dayjs | ||
|
||
constructor(date = new Date()) { | ||
this.dateTime = dayjs.tz(date, NEW_YORK_ZONE) | ||
} | ||
|
||
toDate(): Date { | ||
return this.dateTime.toDate() | ||
} | ||
|
||
setYearMonthDay(year: number, month: MONTHS, day: number): this { | ||
this.dateTime = this.dateTime.set('year', year).set('month', month - 1).set('date', day) | ||
return this | ||
} | ||
|
||
setTime(hours: number, minute: number, second = 0, millisecond = 0): this { | ||
this.dateTime = this.dateTime.set('hour', hours) | ||
.set('minute', minute) | ||
.set('second', second) | ||
.set('millisecond', millisecond) | ||
return this | ||
} | ||
|
||
toStartOfTradingTime(): this { | ||
return this.setTime(TRADING_START_HOUR_NEW_YORK, TRADING_START_MINUTES_NEW_YORK) | ||
} | ||
|
||
toEndOfTradingTime(): this { | ||
return this.setTime(TRADING_END_HOUR_NEW_YORK, TRADING_END_MINUTES_NEW_YORK) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import dayjs from 'dayjs' | ||
import { AssetType, Security, EquityOptionSecurity } from './security' | ||
import { CRYPTO_SYMBOL_REGEX } from './crypto-util' | ||
|
||
const DATE_FORMAT = 'YYMMDD' | ||
const DX_CRYPTO_SUFFIX = ":CXTALP"; | ||
|
||
export function toDxSymbol(security: Security): string { | ||
switch (security.type) { | ||
case AssetType.Equity: | ||
case AssetType.Index: | ||
return toDxEquitySymbol(security) | ||
case AssetType.EquityOption: | ||
return toDxOptionSymbol(security) | ||
case AssetType.Cryptocurrency: | ||
return toDxCryptoSymbol(security) | ||
default: | ||
// Assume Equity | ||
return toDxEquitySymbol(security) | ||
} | ||
} | ||
|
||
export function toDxEquitySymbol(security: Security): string { | ||
return security.underlying.symbol | ||
} | ||
|
||
export function toDxOptionSymbol(security: Security): string { | ||
if (!security.isEquityOption) { | ||
throw new Error(`${security.symbol} is not an option security`) | ||
} | ||
|
||
const optionSecurity = security as EquityOptionSecurity | ||
|
||
const expirationStr = dayjs(optionSecurity.expirationDate).tz('America/New_York').format(DATE_FORMAT) | ||
const strikeStr = optionSecurity.strikePrice.toString() | ||
return `.${optionSecurity.rootSymbol}${optionSecurity.optionChainType}${expirationStr}${optionSecurity.callOrPut[0]}${strikeStr}` | ||
} | ||
|
||
function isDxCryptoSymbol(symbol: string) { | ||
return CRYPTO_SYMBOL_REGEX.test(symbol) && symbol.endsWith(DX_CRYPTO_SUFFIX) | ||
} | ||
|
||
export function toDxCryptoSymbol(security: Security) { | ||
if (!security.isCryptocurrency) { | ||
return security.symbol | ||
} | ||
|
||
if (isDxCryptoSymbol(security.symbol)) { | ||
return security.symbol | ||
} | ||
return `${security.symbol}${DX_CRYPTO_SUFFIX}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
export enum OptionChainType { | ||
STANDARD = '', | ||
NS1 = '1', | ||
NS2 = '2', | ||
NS3 = '3', | ||
NS4 = '4', | ||
NS5 = '5', | ||
NS6 = '6', | ||
MINI7 = '7', | ||
MINI8 = '8', | ||
MINI9 = '9' | ||
} | ||
|
||
export const OPTION_CHAIN_TYPE_VALUES = Object.values(OptionChainType) as string[] | ||
|
||
export function toOptionChainType(raw: string): OptionChainType { | ||
if (OPTION_CHAIN_TYPE_VALUES.indexOf(raw) === -1 || raw === 'Standard') { | ||
return OptionChainType.STANDARD | ||
} | ||
|
||
return raw as OptionChainType | ||
} | ||
|
||
export enum OptionType { | ||
Call = 'Call', | ||
Put = 'Put' | ||
} | ||
|
||
export function toOptionType(raw: string): OptionType { | ||
if (raw[0].toLowerCase() === 'c') { | ||
return OptionType.Call | ||
} else { | ||
return OptionType.Put | ||
} | ||
} | ||
|
||
export const STRIKE_PRICE_FACTOR = 1000 | ||
|
||
export const OCC_SYMBOL_GROUPS = 8 | ||
export const OCC_SYMBOL_REGEX: RegExp = /^([A-Z]{1,5})(\d?)[ ]{0,5}(\d{2})(\d{2})(\d{2})([CP])(\d{8})$/ | ||
|
||
export const NORMALIZED_UNDERLYING_SYMBOLS = Object.freeze( | ||
new Map([ | ||
['SPXW', 'SPX'], // SPX Weeklys have a different underlying | ||
['SPXQ', 'SPX'], // SPX Quarterlys have a different underlying | ||
['RUTW', 'RUT'], // RUT Weeklys | ||
['NDXP', 'NDX'], // NDX PM Weeklys | ||
['BFA', 'BF/A'], | ||
['BFB', 'BF/B'], | ||
['BRKB', 'BRK/B'], | ||
['CBSA', 'CBS/A'], | ||
['EBRB', 'EBR/B'], | ||
['FCEA', 'FCE/A'], | ||
['HEIA', 'HEI/A'], | ||
['JWA', 'JW/A'], | ||
['LGFA', 'LGF/A'], | ||
['NYLDA', 'NYLD/A'], | ||
['MOGA', 'MOG/A'], | ||
['PBRA', 'PBR/A'], | ||
['RDSA', 'RDS/A'], | ||
['RDSB', 'RDS/B'], | ||
['VALEP', 'VALE/P'] | ||
]) | ||
) | ||
|
||
export const AM_SETTLED_ROOT_SYMBOLS = Object.freeze( | ||
new Set([ | ||
'2DJX', | ||
'2NDX', | ||
'2OSX', | ||
'2RUT', | ||
'2SPX', | ||
'2VIX', | ||
'AUM', | ||
'AUX', | ||
'BJE', | ||
'BKX', | ||
'BPX', | ||
'BRB', | ||
'BSZ', | ||
'BVZ', | ||
'BZJ', | ||
'CDD', | ||
'DJX', | ||
'EUI', | ||
'EUU', | ||
'FTEM', | ||
'FXTM', | ||
'GBP', | ||
'GESPY', | ||
'GVZ', | ||
'HGX', | ||
'JBV', | ||
'KBK', | ||
'MNX', | ||
'NDO', | ||
'NDX', | ||
'NZD', | ||
'OSX', | ||
'OVX', | ||
'PZO', | ||
'RLG', | ||
'RLV', | ||
'RMN', | ||
'RUI', | ||
'RUT', | ||
'RVX', | ||
'SFC', | ||
'SKA', | ||
'SOX', | ||
'SPX', | ||
'UKXM', | ||
'VIX', | ||
'VXEEM', | ||
'VXEWZ', | ||
'VXST', | ||
'XAL', | ||
'XDA', | ||
'XDB', | ||
'XDC', | ||
'XDE', | ||
'XDN', | ||
'XDS', | ||
'XDZ', | ||
'XNG', | ||
'XSPAM', | ||
'YUK' | ||
]) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import Feed, { EventType, IEvent } from '@dxfeed/api' | ||
import { toDxSymbol } from './dx-util' | ||
import { parseSecurity } from './security' | ||
import _ from 'lodash' | ||
|
||
export default class QuoteStreamer { | ||
private feed: Feed | null = null | ||
|
||
constructor(private readonly token: string, private readonly url: string) {} | ||
|
||
connect() { | ||
this.feed = new Feed() | ||
this.feed.setAuthToken(this.token) | ||
this.feed.connect(this.url) | ||
} | ||
|
||
disconnect() { | ||
if (!_.isNil(this.feed)) { | ||
this.feed.disconnect() | ||
} | ||
} | ||
|
||
subscribe(twSymbol: string, eventHandler: (event: IEvent) => void): () => void { | ||
if (_.isNil(this.feed)) { | ||
return _.noop | ||
} | ||
|
||
const security = parseSecurity(twSymbol) | ||
if (_.isNil(security)) { | ||
throw `Unable to parse ${twSymbol}` | ||
} | ||
|
||
const dxSymbol = toDxSymbol(security) | ||
return this.feed.subscribe( | ||
[EventType.Quote], | ||
[dxSymbol], | ||
eventHandler | ||
) | ||
} | ||
} | ||
|
||
const token = 'dGFzdHksbGl2ZSwsMTY1MDk5NTQzOCwxNjUwOTA5MDM4LFUwMDAwMDM3MTg0.w3OJ6sG0P93nTL4hPu7xwtb-cUuY8EgUdHKQuTU_kxw' | ||
const wsUrl = 'wss://tools.dxfeed.com/webservice/cometd' | ||
|
||
export const streamer = new QuoteStreamer(token, wsUrl) |
Oops, something went wrong.