Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
dmoss18 committed May 10, 2022
1 parent b66628c commit 2bc58fa
Show file tree
Hide file tree
Showing 15 changed files with 625 additions and 205 deletions.
57 changes: 32 additions & 25 deletions README.md
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&nbsp;&nbsp;</code>
- <code>FB&nbsp;&nbsp;&nbsp;&nbsp;</code>
- <code>BRK/A&nbsp;</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.
30 changes: 30 additions & 0 deletions components/subscribed-symbol.tsx
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>
)
}
2 changes: 2 additions & 0 deletions lib/crypto-util.ts
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})?)$`)
48 changes: 48 additions & 0 deletions lib/date-helper.ts
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)
}
}
52 changes: 52 additions & 0 deletions lib/dx-util.ts
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}`
}
129 changes: 129 additions & 0 deletions lib/option-util.ts
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'
])
)
45 changes: 45 additions & 0 deletions lib/quote-streamer.ts
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)
Loading

0 comments on commit 2bc58fa

Please sign in to comment.