Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, { HTMLProps } from 'react';
- import { ClipboardEvent, Component, ComponentClass, FunctionComponent } from 'react';
- import { connect } from 'react-redux';
- import { styled } from 'styletron-react';
- import { withTheme } from '@benzinga/themetron';
- import DatePicker from 'antd/lib/date-picker';
- import { RangePickerValue } from 'antd/lib/date-picker/interface';
- import Radio from 'antd/lib/radio';
- import { RadioChangeEvent } from 'antd/lib/radio/interface';
- import Select from 'antd/lib/select';
- import Slider from 'antd/lib/slider';
- import invariant from 'invariant';
- import moment from 'moment-timezone';
- import { addIndex, contains, filter, find, isEmpty, isNil, join, map, pipe, prop, propEq, reduce } from 'ramda';
- import { moversUpdateParameters } from '../../../../actions/moversActions';
- import { sagaScreenerCheckAutoRefresh, sagaScreenerRequestMovers } from '../../../../actions/sagaTriggers';
- import {
- ChevronDownIcon,
- ChevronRightIcon,
- DebugIcon,
- TrendingDownIcon,
- TrendingUpIcon
- } from '../../../../assets/icons/';
- import { MarketSession } from '../../../../entities/marketEntity';
- import { RootState } from '../../../../entities/rootEntity';
- import { WidgetId } from '../../../../entities/widgetEntity';
- import { MoversWidgetParameters, MoversWidgetTransient } from '../../../../entities/widgetEntity/screenerWidgetEntity';
- import { selectDebugMode } from '../../../../selectors/appSelectors';
- import { selectWidgetById } from '../../../../selectors/widgetSelectors';
- import { Column, ColumnStyles, Div, Inline, InlineStyles, RowStyles } from '../../../../styles/StyledHOCs';
- import { MECSSectorCode, NBSP } from '../../../../utils/global';
- import {
- createMoversURL,
- intraSessionIntervals,
- marketCapMarks,
- moversSectorOptions,
- moversTimeFormat,
- outOfSessionIntervals,
- priceMarks,
- } from '../../../../utils/moversUtils';
- import NoResults from '../../../search/NoResults';
- import Spinner from '../../../ui/Spinner';
- import MoversGrid from './MoversGrid';
- export interface OwnProps {
- widgetId: WidgetId;
- withQuoteSubscription(props: string[]): Component<any>; // TODO: type more accurately
- }
- export interface ReduxState {
- debugMode: boolean;
- parameters: MoversWidgetParameters;
- transient: MoversWidgetTransient;
- }
- export interface DispatchableActions {
- moversUpdateParameters: typeof moversUpdateParameters;
- sagaScreenerCheckAutoRefresh: typeof sagaScreenerCheckAutoRefresh;
- sagaScreenerRequestMovers: typeof sagaScreenerRequestMovers,
- }
- type Props = Readonly<OwnProps & ReduxState & DispatchableActions>;
- const reduceIndexed: <TResult, T>
- (func: (acc: TResult, property: T, index: number) => TResult, acc: TResult, values: T[]) =>
- TResult = addIndex(reduce);
- const { Button: RadioButton, Group: RadioGroup } = Radio;
- const { Option } = Select;
- const { RangePicker } = DatePicker;
- const relativeTimeFormat = {
- lastDay: '[Yesterday]',
- lastWeek: 'dddd, MMMM Do YYYY',
- nextDay: '[Tomorrow]',
- nextWeek: 'dddd, MMMM Do YYYY',
- sameDay: '[Today]',
- sameElse: 'dddd, MMMM Do YYYY',
- };
- const Parameter = styled('div', {
- ...RowStyles,
- alignItems: 'center',
- flexWrap: 'wrap',
- padding: '0.5em 1.5em',
- });
- const ParameterLabel = styled('div', {
- ...RowStyles,
- alignItems: 'center',
- display: 'inline-flex',
- margin: '0.5em 0',
- minWidth: '100px',
- whiteSpace: 'nowrap',
- });
- const DatePickerContainer = styled('div', {
- ...InlineStyles,
- paddingTop: '0.25em',
- });
- const SliderContainer = styled('div', {
- ...InlineStyles,
- flexGrow: 1,
- height: '30px',
- marginTop: '-20px',
- maxWidth: '300px',
- width: '100%',
- });
- const MoversContainer = styled('div', {
- ...ColumnStyles,
- flex: 1,
- overflowX: 'hidden',
- overflowY: 'auto',
- });
- const WidgetBody = styled('div', {
- ...ColumnStyles,
- flex: 1,
- overflow: 'auto',
- });
- const StyledSelect = styled(Select, {
- minWidth: '225px',
- });
- interface ParametersHeaderProps {
- $expanded: boolean;
- onClick(): void;
- }
- const ParametersHeader = withTheme<ParametersHeaderProps, HTMLProps<HTMLDivElement>>((props, theme) => ({
- ...InlineStyles,
- backgroundColor: props.$expanded ? theme.colors.brandMuted : 'transparent',
- color: props.$expanded ? theme.colors.brandForeground : theme.colors.foregroundInactive,
- cursor: 'pointer',
- fill: theme.colors.foregroundInactive,
- padding: '0.5em',
- }))(Div);
- interface MoversParametersProps {
- $expanded: boolean;
- }
- const MoversParameters = withTheme<MoversParametersProps, HTMLProps<HTMLDivElement>>((props, theme) => ({
- borderBottom: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
- borderLeft: `2px solid ${theme.colors.brandMuted}`,
- borderRight: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
- borderTop: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
- flex: '0 0 auto',
- }))(Column);
- const Emphasis = withTheme((_, theme) => ({
- color: theme.colors.foregroundActive,
- whiteSpace: 'nowrap',
- }))(Inline);
- const DatesContainerLabel = withTheme((_, theme) => ({
- color: theme.colors.foregroundInactive,
- fontSize: '0.825em',
- marginTop: '0.25em',
- }))(Div);
- const GreenText = withTheme((_, theme) => ({
- color: theme.colors.statistic.positive,
- fill: theme.colors.statistic.positive,
- }))(Inline);
- const RedText = withTheme((_, theme) => ({
- color: theme.colors.statistic.negative,
- fill: theme.colors.statistic.negative,
- }))(Inline);
- const StyledTrendingUpIcon = withTheme((_, theme) => ({
- fill: theme.colors.statistic.positive,
- }))(TrendingUpIcon);
- const StyledTrendingDownIcon = withTheme((_, theme) => ({
- fill: theme.colors.statistic.negative,
- }))(TrendingDownIcon);
- const chevronIconEnhancer = withTheme((_, theme) => ({
- fill: theme.colors.brandForeground,
- marginRight: '0.25em',
- }));
- const StyledChevronDownIcon = chevronIconEnhancer(ChevronDownIcon);
- const StyledChevronRightIcon = chevronIconEnhancer(ChevronRightIcon);
- const StyledSpinner = styled(Spinner, {
- display: 'inline-flex',
- fontSize: '90%',
- marginLeft: '0.5em',
- });
- const marketCapTuple: ['minimumMarketCap', 'maximumMarketCap'] = ['minimumMarketCap', 'maximumMarketCap'];
- const priceTuple: ['minimumPrice', 'maximumPrice'] = ['minimumPrice', 'maximumPrice'];
- class Movers extends Component<Props> {
- componentDidMount() {
- const { sagaScreenerCheckAutoRefresh } = this.props;
- this.loadMoversData();
- sagaScreenerCheckAutoRefresh();
- }
- componentWillUnmount() {
- const { sagaScreenerCheckAutoRefresh } = this.props;
- sagaScreenerCheckAutoRefresh();
- }
- loadMoversData = () => {
- const {
- sagaScreenerRequestMovers,
- widgetId,
- } = this.props;
- sagaScreenerRequestMovers(widgetId);
- };
- updateParameters = (parameter: Partial<MoversWidgetParameters>) => () => {
- const {
- moversUpdateParameters,
- parameters: {
- interval,
- session,
- },
- sagaScreenerCheckAutoRefresh,
- widgetId,
- } = this.props;
- let parameters: Partial<MoversWidgetParameters> = {
- ...parameter,
- };
- const updatedSession = Boolean(parameters.session);
- if (updatedSession && session === 'REGULAR') {
- const isIntervalOutOfSession = !find(propEq('id', interval), outOfSessionIntervals);
- if (isIntervalOutOfSession) {
- parameters = {
- ...parameters,
- interval: 'session',
- };
- }
- }
- moversUpdateParameters(widgetId, parameters);
- // If updating autorefresh property, check if the autorefreshing saga should start/stop
- if (!isNil(parameters.autoRefresh)) {
- sagaScreenerCheckAutoRefresh();
- }
- // Dont load movers data if turning autoRefresh off or expanding/minimising filtersExpanded
- const dontLoadMoversData = parameters.autoRefresh === false || !isNil(parameters.filtersExpanded);
- if (dontLoadMoversData) {
- return;
- }
- this.loadMoversData();
- };
- onPaste = (nextDate: 'fromDate' | 'toDate') => (event: ClipboardEvent<HTMLElement>) => {
- event.preventDefault();
- const clipData = event.clipboardData.getData('text');
- const momentDate = moment(clipData).format('MM/DD/YYYY');
- if (momentDate === 'Invalid date') {
- // eslint-disable-next-line
- return;
- }
- };
- onDayPickerInputChange = (nextDate: 'fromDate' | 'toDate') => (date, { disabled }) => {
- if (disabled || !date) {
- return;
- }
- const dateToMoment = moment(date);
- this.updateParameters({
- interval: 'custom',
- [nextDate]: dateToMoment.format(moversTimeFormat),
- })();
- };
- renderCollapsedFilterMenu() {
- const {
- parameters: {
- gainersLosersDir,
- interval,
- session,
- fromDate,
- toDate,
- },
- transient: {
- loading,
- },
- } = this.props;
- const emphasize = (input: string | JSX.Element | null) => (
- <Emphasis>{input}</Emphasis>
- );
- const gainersBlurb = <GreenText><StyledTrendingUpIcon /> Gainers</GreenText>;
- const losersBlurb = <RedText><StyledTrendingDownIcon /> Losers</RedText>;
- const screenerType = {
- both: <span>{gainersBlurb}{' & '}{losersBlurb}</span>,
- gainers: gainersBlurb,
- losers: losersBlurb,
- }[gainersLosersDir];
- const sessionType = {
- AFTER_HOURS: <span>of the {emphasize(MarketSession.afterHours)} session</span>,
- PRE_MARKET: <span>of the {emphasize(MarketSession.preMarket)}</span>,
- REGULAR: <span>of the {emphasize(MarketSession.regular)} session</span>,
- }[session];
- // TODO it would be great to statically type the keys of this object against MoversInterval once typescript 2.7 drops
- const dateRange = {
- '-15m': <span>over the last {emphasize('15 minutes')}</span>,
- '-180d': <span>over the last {emphasize('6 months')}</span>,
- '-1w': <span>over the last {emphasize('week')}</span>,
- '-30d': <span>over the last {emphasize('month')}</span>,
- '-30m': <span>over the last {emphasize('30 minutes')}</span>,
- '-60m': <span>over the last {emphasize('hour')}</span>,
- '-90d': <span>over last {emphasize('3 months')}</span>,
- YTD: <span>over the {emphasize('Year to Date')}</span>,
- custom: (
- <span>
- from {emphasize(moment(fromDate || '').format('MM/DD/YYYY'))}
- {NBSP}to {emphasize(moment(toDate || '').format('MM/DD/YYYY'))}
- </span>
- ),
- session: <span>over this {emphasize('session')}</span>,
- }[interval];
- return (
- <span>
- : {screenerType}
- {' | '}{emphasize(this.getSectorsLabel())}
- {' | '}{dateRange}{NBSP}{sessionType}
- {loading && <StyledSpinner />}
- </span>
- );
- }
- getSectorsLabel() {
- const { sectors } = this.props.parameters;
- let sectorLabel: string | null = null;
- const matchedSectors = filter(
- option => contains(option.id, sectors),
- moversSectorOptions,
- );
- if (!isEmpty(matchedSectors)) {
- sectorLabel = pipe(
- map(prop('name')),
- join(', '),
- )(matchedSectors);
- }
- return sectorLabel;
- }
- // takes a tuple of keys, which are then updated by the rc-slider's onAfterChange callback
- handleRangeChange =
- <T extends [keyof MoversWidgetParameters, keyof MoversWidgetParameters], K extends [number, number]>
- (properties: T) => (values: K) => {
- // tslint:disable-next-line:max-line-length
- const parameters: Partial<MoversWidgetParameters> = reduceIndexed<Partial<MoversWidgetParameters>, keyof MoversWidgetParameters>(
- (accumulator: Partial<MoversWidgetParameters>, property: keyof MoversWidgetParameters, index: number) => {
- accumulator[property] = values[index];
- return accumulator;
- },
- {},
- properties,
- );
- this.updateParameters(parameters)();
- };
- handleGainersLosersChange = (event: RadioChangeEvent) => {
- const { target: { value } } = event;
- invariant(
- value === 'gainers' ||
- value === 'losers' ||
- value === 'both',
- 'Invalid value for gainers/losers radio group',
- );
- const gainersLosersDir = value as 'gainers' | 'losers' | 'both';
- this.updateParameters({ gainersLosersDir })();
- };
- handleSessionChange = (event: RadioChangeEvent) => {
- const { target: { value } } = event;
- invariant(
- value === 'PRE_MARKET' ||
- value === 'REGULAR' ||
- value === 'AFTER_HOURS',
- 'Invalid value for session type radio group',
- );
- const session = value as 'PRE_MARKET' | 'REGULAR' | 'AFTER_HOURS';
- this.updateParameters({ session })();
- };
- handleIntervalChange = (event: RadioChangeEvent) => {
- this.updateParameters({ interval: event.target.value })();
- };
- handleAutoRefreshChange = (event: RadioChangeEvent) => {
- const autoRefresh = Boolean(event.target.value);
- this.updateParameters({ autoRefresh })();
- };
- handleRangePickerChange = (date: RangePickerValue) => {
- const [fromDate, toDate] = date;
- if (fromDate && toDate) {
- this.updateParameters({
- fromDate: fromDate.format(moversTimeFormat),
- toDate: toDate.format(moversTimeFormat),
- })();
- }
- };
- handleSectorFiltersChange = (sectors: MECSSectorCode[]) => {
- this.updateParameters({ sectors })();
- };
- renderFullMenu() {
- const {
- debugMode,
- parameters: {
- autoRefresh,
- fromDate,
- gainersLosersDir,
- interval,
- maximumMarketCap,
- maximumPrice,
- minimumMarketCap,
- minimumPrice,
- sectors,
- session,
- toDate,
- },
- } = this.props;
- const fromMoment = moment.tz(fromDate, 'America/New_York');
- const toMoment = moment.tz(toDate, 'America/New_York');
- let debugZone;
- const gainersLosersFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Movers">Movers</ParameterLabel>
- <RadioGroup
- defaultValue={gainersLosersDir}
- onChange={this.handleGainersLosersChange}
- size="small"
- >
- <RadioButton value="both">Gainers & Losers</RadioButton>
- <RadioButton value="gainers">Gainers</RadioButton>
- <RadioButton value="losers">Losers</RadioButton>
- </RadioGroup>
- </Parameter>
- );
- const sessionFilters = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Session">Session</ParameterLabel>
- <RadioGroup
- defaultValue={session}
- onChange={this.handleSessionChange}
- size="small"
- >
- <RadioButton value="PRE_MARKET">{MarketSession.preMarket}</RadioButton>
- <RadioButton value="REGULAR">{MarketSession.regular}</RadioButton>
- <RadioButton value="AFTER_HOURS">{MarketSession.afterHours}</RadioButton>
- </RadioGroup>
- </Parameter>
- );
- const renderCustomDateInputs = () => {
- const shouldShowCustomDateInputs = interval === 'custom';
- const presentDateTime = (theMoment: moment.Moment, relativeTime?: boolean) => (
- <span>
- {relativeTime ? theMoment.calendar(moment(), relativeTimeFormat) : theMoment.format('MMMM Do YYYY')}
- {NBSP}at{NBSP}
- <Emphasis>{theMoment.format('h:mma')}</Emphasis>
- </span>
- );
- if (!shouldShowCustomDateInputs) {
- return (
- <DatesContainerLabel>
- comparing {presentDateTime(fromMoment, true)} to {presentDateTime(toMoment, true)}
- </DatesContainerLabel>
- );
- }
- const disabledDates = current => current > moment().endOf('day');
- return (
- <DatePickerContainer>
- <RangePicker
- disabledDate={disabledDates}
- format={'MM/DD/YYYY'}
- onChange={this.handleRangePickerChange}
- size="small"
- value={[moment(fromMoment, 'MM/DD/YYYY'), moment(toMoment, 'MM/DD/YYYY')]}
- />
- <DatesContainerLabel>
- {presentDateTime(fromMoment)} to {presentDateTime(toMoment)}
- </DatesContainerLabel>
- </DatePickerContainer>
- );
- };
- const period = {
- AFTER_HOURS: outOfSessionIntervals,
- PRE_MARKET: outOfSessionIntervals,
- REGULAR: intraSessionIntervals,
- }[session];
- const intervalFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Period">Period</ParameterLabel>
- <Column>
- <RadioGroup
- onChange={this.handleIntervalChange}
- size="small"
- value={interval}
- >
- {map(
- ({ id, label }) => <RadioButton key={id} value={id}>{label}</RadioButton>,
- period,
- )}
- </RadioGroup>
- {fromDate && renderCustomDateInputs()}
- </Column>
- </Parameter>
- );
- const sectorsFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Sectors">Sectors</ParameterLabel>
- <StyledSelect
- allowClear
- filterOption
- mode="multiple"
- notFoundContent="Sector Not Found"
- onChange={this.handleSectorFiltersChange}
- optionFilterProp="children"
- placeholder="All Sectors"
- value={sectors}
- >
- {map(
- ({ name, id }) => <Option key={id}>{name}</Option>,
- moversSectorOptions,
- )}
- </StyledSelect>
- </Parameter>
- );
- const marketCapFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_MarketCap">Market Cap ($)</ParameterLabel>
- <SliderContainer>
- <Slider
- defaultValue={[minimumMarketCap, maximumMarketCap]}
- dots
- marks={marketCapMarks}
- max={5}
- min={0}
- onAfterChange={this.handleRangeChange(marketCapTuple)}
- range
- step={1}
- tipFormatter={null}
- />
- </SliderContainer>
- </Parameter>
- );
- const priceFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Price">Price ($)</ParameterLabel>
- <SliderContainer>
- <Slider
- defaultValue={[minimumPrice, maximumPrice]}
- dots
- marks={priceMarks}
- max={6}
- min={0}
- onAfterChange={this.handleRangeChange(priceTuple)}
- range
- step={1}
- tipFormatter={null}
- />
- </SliderContainer>
- </Parameter>
- );
- const autoRefreshFilter = (
- <Parameter>
- <ParameterLabel className="TUTORIAL_ScreenerFilters_Refresh">Refresh (1 min)</ParameterLabel>
- <RadioGroup
- defaultValue={autoRefresh}
- onChange={this.handleAutoRefreshChange}
- size="small"
- >
- <RadioButton value>Auto Refresh</RadioButton>
- <RadioButton value={false}>Freeze</RadioButton>
- </RadioGroup>
- </Parameter>
- );
- if (debugMode) {
- const url = createMoversURL(this.props.parameters);
- debugZone = (
- <Parameter>
- <ParameterLabel>Debug <DebugIcon /></ParameterLabel>
- <a href={url} target="_blank" rel="noopener noreferrer">{url}</a>
- </Parameter>
- );
- }
- return (
- <Column>
- {gainersLosersFilter}
- {sessionFilters}
- {intervalFilter}
- {sectorsFilter}
- {marketCapFilter}
- {priceFilter}
- {autoRefreshFilter}
- {debugZone}
- </Column>
- );
- }
- renderContent = () => {
- const {
- transient: {
- error,
- instruments,
- loading,
- },
- withQuoteSubscription,
- } = this.props;
- if (error) {
- return <NoResults hideSearchTips message={error} />;
- }
- // on first load, instruments will be null in the absence of an error.
- if (loading && (isEmpty(instruments) || isNil(instruments))) {
- return <Spinner />;
- }
- return (
- <MoversGrid
- rowData={instruments}
- withQuoteSubscription={withQuoteSubscription}
- />
- );
- };
- render() {
- const {
- parameters: {
- filtersExpanded,
- },
- } = this.props;
- const header = (
- <ParametersHeader
- className="TUTORIAL_Screener-FilterMenu"
- onClick={this.updateParameters({ filtersExpanded: !filtersExpanded })}
- $expanded={filtersExpanded}
- >
- {filtersExpanded ? <StyledChevronDownIcon /> : <StyledChevronRightIcon />}
- Applied Filters{!filtersExpanded && this.renderCollapsedFilterMenu()}
- </ParametersHeader>
- );
- return (
- <MoversContainer>
- <MoversParameters $expanded={filtersExpanded}>
- {header}
- {filtersExpanded && this.renderFullMenu()}
- </MoversParameters>
- <WidgetBody>
- {this.renderContent()}
- </WidgetBody>
- </MoversContainer>
- );
- }
- }
- const mapStateToProps = (state: RootState, ownProps: Props): ReduxState => {
- const { data: { parameters, transient } } = selectWidgetById(state, ownProps);
- return {
- debugMode: selectDebugMode(state),
- parameters,
- transient,
- };
- };
- const mapDispatchToProps = {
- moversUpdateParameters,
- sagaScreenerCheckAutoRefresh,
- sagaScreenerRequestMovers,
- };
- export type ExternalProps = Pick<OwnProps & ReduxState & DispatchableActions, 'widgetId' | 'withQuoteSubscription'>;
- const MoversConnect: ComponentClass<ExternalProps> & {
- WrappedComponent: ComponentClass<OwnProps & ReduxState & DispatchableActions> |
- FunctionComponent<OwnProps & ReduxState & DispatchableActions>;
- } = connect<ReduxState, DispatchableActions, OwnProps, RootState>(mapStateToProps, mapDispatchToProps)(Movers);
- export default MoversConnect;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement