From 2bc58fa1477a7cdbab75e5c0c3b85b017f802f4c Mon Sep 17 00:00:00 2001 From: dmoss18 Date: Mon, 9 May 2022 13:26:30 -0500 Subject: [PATCH] Update README --- README.md | 57 +++++---- components/subscribed-symbol.tsx | 30 +++++ lib/crypto-util.ts | 2 + lib/date-helper.ts | 48 ++++++++ lib/dx-util.ts | 52 ++++++++ lib/option-util.ts | 129 ++++++++++++++++++++ lib/quote-streamer.ts | 45 +++++++ lib/security.ts | 187 +++++++++++++++++++++++++++++ package.json | 6 +- pages/api/hello.ts | 13 -- pages/index.tsx | 99 +++++++-------- styles/Home.module.css | 109 +---------------- styles/SubscribedSymbol.module.css | 12 ++ styles/globals.css | 8 +- yarn.lock | 33 +++++ 15 files changed, 625 insertions(+), 205 deletions(-) create mode 100644 components/subscribed-symbol.tsx create mode 100644 lib/crypto-util.ts create mode 100644 lib/date-helper.ts create mode 100644 lib/dx-util.ts create mode 100644 lib/option-util.ts create mode 100644 lib/quote-streamer.ts create mode 100644 lib/security.ts delete mode 100644 pages/api/hello.ts create mode 100644 styles/SubscribedSymbol.module.css diff --git a/README.md b/README.md index c87e042..534c0e0 100644 --- a/README.md +++ b/README.md @@ -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. + - AAPL   + - FB     + - BRK/A  +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. \ No newline at end of file diff --git a/components/subscribed-symbol.tsx b/components/subscribed-symbol.tsx new file mode 100644 index 0000000..e75f9e5 --- /dev/null +++ b/components/subscribed-symbol.tsx @@ -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 ( +
+
{props.symbol}
+
+
Bid: {bidPrice}
+
Ask: {askPrice}
+ +
+
+ ) +} diff --git a/lib/crypto-util.ts b/lib/crypto-util.ts new file mode 100644 index 0000000..d5bd253 --- /dev/null +++ b/lib/crypto-util.ts @@ -0,0 +1,2 @@ +export const DX_CRYPTO_SUFFIX = ':CXTALP' +export const CRYPTO_SYMBOL_REGEX = new RegExp(`^[A-Z]+/USD((${DX_CRYPTO_SUFFIX})?)$`) \ No newline at end of file diff --git a/lib/date-helper.ts b/lib/date-helper.ts new file mode 100644 index 0000000..2c764a3 --- /dev/null +++ b/lib/date-helper.ts @@ -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) + } +} diff --git a/lib/dx-util.ts b/lib/dx-util.ts new file mode 100644 index 0000000..3665230 --- /dev/null +++ b/lib/dx-util.ts @@ -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}` +} \ No newline at end of file diff --git a/lib/option-util.ts b/lib/option-util.ts new file mode 100644 index 0000000..8c4c3d8 --- /dev/null +++ b/lib/option-util.ts @@ -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' + ]) +) \ No newline at end of file diff --git a/lib/quote-streamer.ts b/lib/quote-streamer.ts new file mode 100644 index 0000000..0655678 --- /dev/null +++ b/lib/quote-streamer.ts @@ -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) \ No newline at end of file diff --git a/lib/security.ts b/lib/security.ts new file mode 100644 index 0000000..3385dfc --- /dev/null +++ b/lib/security.ts @@ -0,0 +1,187 @@ +import _ from 'lodash' +import DateHelper, { MONTHS } from './date-helper' +import { CRYPTO_SYMBOL_REGEX } from './crypto-util' +import { + OptionType, + OptionChainType, + toOptionChainType, + toOptionType, + AM_SETTLED_ROOT_SYMBOLS, + NORMALIZED_UNDERLYING_SYMBOLS, + OCC_SYMBOL_REGEX, + OCC_SYMBOL_GROUPS, + STRIKE_PRICE_FACTOR +} from './option-util' + +export enum AssetType { + Equity = 'Equity', + EquityOption = 'Equity Option', + Index = 'Index', + Cryptocurrency = 'Cryptocurrency', + Unknown = 'Unknown' +} + +export abstract class Security { + // prettier-ignore + constructor( + readonly symbol: string, + readonly type: AssetType, + readonly rootSymbol: string + ) { } + + abstract get underlying(): Security + + get isEquityOption() { + return this.type === AssetType.EquityOption + } + + get isEquityLike() { + return this.type === AssetType.Equity || + this.type === AssetType.Index + } + + get isCryptocurrency() { + return this.type === AssetType.Cryptocurrency + } +} + +export class EquitySecurity extends Security { + // prettier-ignore + constructor( + readonly symbol: string, + ) { + super(symbol, AssetType.Equity, symbol) + } + + get underlying(): Security { + return this + } +} + +export class CryptoSecurity extends Security { + // prettier-ignore + constructor( + readonly symbol: string + ) { + super(symbol, AssetType.Cryptocurrency, symbol) + } + + get underlying(): Security { + return this + } +} + +export abstract class OptionSecurity extends Security { + constructor( + symbol: string, + assetType: AssetType, + rootSymbol: string, + readonly callOrPut: OptionType, + readonly strikePrice: number, + readonly expirationDate: Date + ) { + super(symbol, assetType, rootSymbol) + } + + get isCall(): boolean { + return this.callOrPut === OptionType.Call + } + + get isPut(): boolean { + return this.callOrPut === OptionType.Put + } +} + +export class EquityOptionSecurity extends OptionSecurity { + constructor( + symbol: string, + rootSymbol: string, + readonly underlying: Security, + readonly optionChainType: OptionChainType, + readonly callOrPut: OptionType, + readonly strikePrice: number, + readonly expirationDate: Date + ) { + super(symbol, AssetType.EquityOption, rootSymbol, callOrPut, strikePrice, expirationDate) + } +} + +function toEquityUnderlyingSecurty(symbol: string) { + const normalizedSymbol = NORMALIZED_UNDERLYING_SYMBOLS.get(symbol) + return new EquitySecurity(normalizedSymbol === undefined ? symbol : normalizedSymbol) +} + +function toExpirationDate(year: string, monthRaw: string, day: string): DateHelper { + const month: MONTHS = parseInt(monthRaw, 10) as MONTHS + const yearFormatted = `20${year}` + return new DateHelper().setYearMonthDay(parseInt(yearFormatted, 10), month, parseInt(day, 10)) +} + +function adjustEquityOptionExpirationDate(rootSymbol: string, dateHelper: DateHelper): Date { + if (AM_SETTLED_ROOT_SYMBOLS.has(rootSymbol)) { + return dateHelper.toStartOfTradingTime().toDate() + } else { + return dateHelper.toEndOfTradingTime().toDate() + } +} + +export function parseEquityOption(symbol: string): EquityOptionSecurity { + const matches = OCC_SYMBOL_REGEX.exec(symbol) + if (matches === null || matches.length !== OCC_SYMBOL_GROUPS) { + throw new Error(`Invalid OCC Option symbol: ${symbol}`) + } + + // tslint:disable:no-magic-numbers + const rootSymbol = matches[1] + const optionChainTypeStr = matches[2] + const year = matches[3] + const month = matches[4] + const day = matches[5] + const callOrPutStr = matches[6] + const strikeStr = matches[7] + // tslint:enable:no-magic-numbers + + const underlyingSecurity = toEquityUnderlyingSecurty(rootSymbol) + const optionChainType = toOptionChainType(optionChainTypeStr) + const parsedExpirationDate = toExpirationDate(year, month, day) + const expirationDate = adjustEquityOptionExpirationDate(rootSymbol, parsedExpirationDate) + const optionType = toOptionType(callOrPutStr) + const strikePrice = parseFloat(strikeStr) / STRIKE_PRICE_FACTOR + + return new EquityOptionSecurity(symbol, rootSymbol, underlyingSecurity, optionChainType, optionType, strikePrice, expirationDate) +} + +export function parseSecurity(rawSymbol: string, assetType?: string): Security | null { + const symbol = rawSymbol.toUpperCase() + // tslint:disable-next-line:strict-boolean-expressions + if (assetType) { + switch (assetType) { + case AssetType.Equity: + case AssetType.Index: + return new EquitySecurity(symbol) + case AssetType.EquityOption: + return parseEquityOption(symbol) + case AssetType.Cryptocurrency: + return new CryptoSecurity(symbol) + default: + // Assume Equity + return new EquitySecurity(symbol) + } + } else { + const length = symbol.length + if (length === 0) { + return null + } + + if (OCC_SYMBOL_REGEX.test(symbol)) { + return parseEquityOption(symbol) + } + + if (CRYPTO_SYMBOL_REGEX.test(symbol)) { + return new CryptoSecurity(symbol) + } + + // Assume it is an equity + return new EquitySecurity(symbol) + } +} diff --git a/package.json b/package.json index 7c227e0..4ac31cb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "tw-dxfeed-demo", + "name": "tastyworks-api-js-demo", "version": "0.1.0", "private": true, "scripts": { @@ -9,6 +9,10 @@ "lint": "next lint" }, "dependencies": { + "@dxfeed/api": "^1.1.0", + "@types/lodash": "^4.14.182", + "dayjs": "^1.11.1", + "lodash": "^4.17.21", "next": "12.1.5", "react": "18.0.0", "react-dom": "18.0.0" diff --git a/pages/api/hello.ts b/pages/api/hello.ts deleted file mode 100644 index f8bcc7e..0000000 --- a/pages/api/hello.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' - -type Data = { - name: string -} - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/pages/index.tsx b/pages/index.tsx index 86b5b3b..89e0c74 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,70 +1,59 @@ import type { NextPage } from 'next' -import Head from 'next/head' -import Image from 'next/image' import styles from '../styles/Home.module.css' +import { streamer } from '../lib/quote-streamer' +import { useEffect, useState } from 'react' +import SubscribedSymbol from '../components/subscribed-symbol' +import _ from 'lodash' + +const twSymbols = [ + 'AAPL', + 'SPY 220617C00438000' +] const Home: NextPage = () => { - return ( -
- - Create Next App - - - + const [symbols, setSymbols] = useState(twSymbols) + const [loading, setLoading] = useState(true) + const [symbolText, setSymbolText] = useState('') -
-

- Welcome to Next.js! -

+ useEffect(() => { + streamer.connect() + setLoading(false) + return () => streamer.disconnect() + }, []); + + const addSymbol = () => { + const symbol = symbolText.trim().toUpperCase() + if (symbols.includes(symbol)) { + return + } -

- Get started by editing{' '} - pages/index.tsx -

+ setSymbols([...symbols, symbol]) + } -
- -

Documentation →

-

Find in-depth information about Next.js features and API.

-
+ const removeSymbol = (symbol: string) => { + setSymbols(_.without(symbols, symbol)) + } - -

Learn →

-

Learn about Next.js in an interactive course with quizzes!

-
+ const handleChange = (event: any) => { + setSymbolText(event.target.value); + } - -

Examples →

-

Discover and deploy boilerplate example Next.js projects.

-
+ return ( +
+
+

+ DxFeed Quotes Demo +

- -

Deploy →

-

- Instantly deploy your Next.js site to a public URL with Vercel. -

-
+
+ +
-
- + {!loading && symbols.map(twSymbol => ( + + ))} +
) } diff --git a/styles/Home.module.css b/styles/Home.module.css index 32a57d5..1f1236e 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -4,113 +4,10 @@ .main { min-height: 100vh; - padding: 4rem 0; + padding: 1rem 0; flex: 1; display: flex; flex-direction: column; - justify-content: center; + justify-content: start; align-items: center; -} - -.footer { - display: flex; - flex: 1; - padding: 2rem 0; - border-top: 1px solid #eaeaea; - justify-content: center; - align-items: center; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - margin: 4rem 0; - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; -} - -.grid { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - max-width: 800px; -} - -.card { - margin: 1rem; - padding: 1.5rem; - text-align: left; - color: inherit; - text-decoration: none; - border: 1px solid #eaeaea; - border-radius: 10px; - transition: color 0.15s ease, border-color 0.15s ease; - max-width: 300px; -} - -.card:hover, -.card:focus, -.card:active { - color: #0070f3; - border-color: #0070f3; -} - -.card h2 { - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.card p { - margin: 0; - font-size: 1.25rem; - line-height: 1.5; -} - -.logo { - height: 1em; - margin-left: 0.5rem; -} - -@media (max-width: 600px) { - .grid { - width: 100%; - flex-direction: column; - } -} +} \ No newline at end of file diff --git a/styles/SubscribedSymbol.module.css b/styles/SubscribedSymbol.module.css new file mode 100644 index 0000000..5cb2e58 --- /dev/null +++ b/styles/SubscribedSymbol.module.css @@ -0,0 +1,12 @@ +.main { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 100%; + margin-top: 1em; +} + +.rightColumn { + width: 150px; + margin-left: 1em; +} \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index e5e2dcc..b5bf08e 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,16 +1,14 @@ html, body { padding: 0; - margin: 0; + margin-left: auto; + margin-right: auto; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + width: 800px; } a { color: inherit; text-decoration: none; } - -* { - box-sizing: border-box; -} diff --git a/yarn.lock b/yarn.lock index 92f5e0e..c1c6e3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,14 @@ dependencies: regenerator-runtime "^0.13.4" +"@dxfeed/api@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@dxfeed/api/-/api-1.1.0.tgz#67ad82c74935648ca790517bb8fb40b12e91f7ce" + integrity sha512-U8agkpCaVXCQF6DZab0pXou8jQnOcN9ApriRnpwib3Jp0AQOzO8F8047yr/iCvTij9/XovcQyRUyWQaVudS1pw== + dependencies: + "@types/cometd" "^4.0.7" + cometd "^5.0.1" + "@eslint/eslintrc@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" @@ -144,11 +152,21 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64" integrity sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw== +"@types/cometd@^4.0.7": + version "4.0.11" + resolved "https://registry.yarnpkg.com/@types/cometd/-/cometd-4.0.11.tgz#d56b1b380845822710c4cc16a324d7afbde99bdf" + integrity sha512-bn27WenWkEyCH1ynpZZdSA6gnYW30bLWVUIu+EluxEaFNDPor6EnVJtTCVDX/wzUdriaXRcIYjOTnIP1fEuGeQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@^4.14.182": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + "@types/node@17.0.27": version "17.0.27" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.27.tgz#f4df3981ae8268c066e8f49995639f855469081e" @@ -378,6 +396,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +cometd@^5.0.1: + version "5.0.11" + resolved "https://registry.yarnpkg.com/cometd/-/cometd-5.0.11.tgz#88a195eaa5636f617cfa855539a75e7c4d002549" + integrity sha512-56rQ6P3gg/8Tib3nSPxF7xBlLXcBm6fvp5F/cH4GX7HNif12eR/bApPyUcUL1kRI5QaC8O6BoSBmq8qDXZtWpw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -407,6 +430,11 @@ damerau-levenshtein@^1.0.7: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +dayjs@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.1.tgz#90b33a3dda3417258d48ad2771b415def6545eb0" + integrity sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== + debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1136,6 +1164,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"