diff --git a/src-trader-dashboard/actions/fxDataActions.js b/src-trader-dashboard/actions/fxDataActions.js new file mode 100644 index 0000000..4023207 --- /dev/null +++ b/src-trader-dashboard/actions/fxDataActions.js @@ -0,0 +1,16 @@ +import cloneDeep from "lodash/cloneDeep"; + +export function fxDataUpdated(fxData) { + return { + type: 'FX_DATA_CHANGED', + fxData: cloneDeep(fxData) + }; +} + +export function fxTopMoversUpdated(fxTopMovers) { + return { + type: 'FX_TOP_MOVERS_CHANGED', + fxTopMovers: cloneDeep(fxTopMovers) + }; +} + diff --git a/src-trader-dashboard/actions/fxDataUpdated.js b/src-trader-dashboard/actions/fxDataUpdated.js deleted file mode 100644 index 6f38f48..0000000 --- a/src-trader-dashboard/actions/fxDataUpdated.js +++ /dev/null @@ -1,9 +0,0 @@ -import cloneDeep from "lodash/cloneDeep"; - -export default function fxDataUpdatedAction(fxData) { - return { - type: 'FX_DATA_CHANGED', - fxData: cloneDeep(fxData) - }; -} - diff --git a/src-trader-dashboard/components/FxPanel.jsx b/src-trader-dashboard/components/FxPanel.jsx new file mode 100644 index 0000000..db3582b --- /dev/null +++ b/src-trader-dashboard/components/FxPanel.jsx @@ -0,0 +1,26 @@ +import React, {Component} from "react"; + +import FxDataService from "../services/FxDataService.jsx"; +import FxQuoteMatrix from "./FxQuoteMatrix.jsx"; +import TopMoversGrid from "./TopMoversGrid.jsx"; + +export default class extends Component { + constructor(props) { + super(props); + + this.fxDataService = new FxDataService(); + } + + render() { + return ( +
+
+ +
+
+ +
+
+ ); + } +}; diff --git a/src-trader-dashboard/components/FxQuoteMatrix.jsx b/src-trader-dashboard/components/FxQuoteMatrix.jsx index 6637b4e..74fd33c 100644 --- a/src-trader-dashboard/components/FxQuoteMatrix.jsx +++ b/src-trader-dashboard/components/FxQuoteMatrix.jsx @@ -5,18 +5,13 @@ import {AgGridReact} from "ag-grid-react"; import assign from "lodash/assign"; import uniq from "lodash/uniq"; -import cloneDeep from "lodash/cloneDeep"; - -import FxDataService from "../services/FxDataService.jsx"; class FxQuoteMatrix extends Component { constructor(props) { super(props); - this.fxDataService = new FxDataService(); - this.state = { - columnDefs: this.fxDataService.getFxMatrixHeaderNames() + columnDefs: this.props.fxDataService.getFxMatrixHeaderNames() }; // grid events @@ -28,48 +23,43 @@ class FxQuoteMatrix extends Component { this.columnApi = params.columnApi; } + componentDidMount() { + this.gridApi.setRowData(this.props.rowData); + } + componentWillReceiveProps(nextProps) { - if (!this.gridApi) { - return; - } + const newRowData = nextProps.rowData; - if (!this.props.rowData || - this.props.rowData.length === 0) { - this.gridApi.setRowData(nextProps.rowData); - } else { - const newRowData = nextProps.rowData; + const updatedNodes = []; + const updatedCols = []; - const updatedNodes = []; - const updatedCols = []; + for (let i = 0; i < newRowData.length; i++) { + // note that for this use case we assume the existing and new row data have the same + // row and column order + let node = this.gridApi.getModel().getRow(i); + let newRow = newRowData[i]; - for (let i = 0; i < newRowData.length; i++) { - // note that for this use case we assume the existing and new row data have the same - // row and column order - let node = this.gridApi.getModel().getRow(i); - let newRow = newRowData[i]; + const {data} = node; + let updated = false; + for (const def of this.state.columnDefs) { + if (data[def.field] !== newRow[def.field]) { + updatedCols.push(def.field); - const {data} = node; - let updated = false; - for (const def of this.state.columnDefs) { - if (data[def.field] !== newRow[def.field]) { - updatedCols.push(def.field); - - updated = true; - } - } - if(updated) { - assign(data, newRow); - updatedNodes.push(node); + updated = true; } } - - this.gridApi.refreshCells(updatedNodes, uniq(updatedCols)); + if(updated) { + assign(data, newRow); + updatedNodes.push(node); + } } + + this.gridApi.refreshCells(updatedNodes, uniq(updatedCols)); } render() { return ( -
- +
); diff --git a/src-trader-dashboard/components/TopMoversGrid.jsx b/src-trader-dashboard/components/TopMoversGrid.jsx new file mode 100644 index 0000000..61873fa --- /dev/null +++ b/src-trader-dashboard/components/TopMoversGrid.jsx @@ -0,0 +1,105 @@ +import React, {Component} from "react"; +import {connect} from "react-redux"; + +import {AgGridReact} from "ag-grid-react"; + +class TopMoversGrid extends Component { + constructor(props) { + super(props); + + this.state = { + columnDefs: [ + { + field: 'symbol', + headerName: 'Symbol' + }, + { + field: 'last', + headerName: 'Last', + headerClass: 'align-right', + cellRenderer: 'animateShowChange', + cellClass: 'align-right' + }, + { + field: 'net', + headerName: 'Net', + headerClass: 'align-right', + cellRenderer: 'animateShowChange', + cellClass: 'align-right' + }, + { + field: 'pct_net_change', + headerName: '% NC', + headerClass: 'align-right', + cellRenderer: 'animateShowChange', + cellClass: 'align-right' + }, + ] + }; + + // grid events + this.onGridReady = this.onGridReady.bind(this); + } + + onGridReady(params) { + this.gridApi = params.api; + this.columnApi = params.columnApi; + } + + componentDidMount() { + this.gridApi.setRowData(this.props.rowData); + this.gridApi.sizeColumnsToFit(); + } + + componentWillReceiveProps(nextProps) { + let newRowData = nextProps.rowData; + let model = this.gridApi.getModel(); + + // remove nodes not in new data set + let nodesToRemove = []; + for (let rowIndex = 0; rowIndex < model.getRowCount(); rowIndex++) { + let rowNode = model.getRow(rowIndex); + if (rowNode.data.symbol !== newRowData[rowIndex].symbol) { + nodesToRemove.push(rowNode); + } + } + this.gridApi.removeItems(nodesToRemove); + + // add new items in set + for (let rowIndex = 0; rowIndex < newRowData.length; rowIndex++) { + let model = this.gridApi.getModel(); + let rowNode = model.getRow(rowIndex); + + if (!rowNode || + rowNode.data.symbol !== newRowData[rowIndex].symbol) { + this.gridApi.insertItemsAtIndex(rowIndex, [newRowData[rowIndex]]) + } + } + } + + render() { + return ( +
+ + +
+ ); + } +} + +export default connect( + (state) => { + return { + rowData: state.fxTopMovers + } + } +)(TopMoversGrid); \ No newline at end of file diff --git a/src-trader-dashboard/reducers/fxData.js b/src-trader-dashboard/reducers/fxData.js deleted file mode 100644 index e0a9b1f..0000000 --- a/src-trader-dashboard/reducers/fxData.js +++ /dev/null @@ -1,10 +0,0 @@ -export default (state = {fxData: []}, action) => { - switch (action.type) { - case 'FX_DATA_CHANGED': - return { - fxData: action.fxData.slice(0) - }; - default: - return state; - } -}; \ No newline at end of file diff --git a/src-trader-dashboard/reducers/fxDataReducer.jsx b/src-trader-dashboard/reducers/fxDataReducer.jsx new file mode 100644 index 0000000..3dfb7f2 --- /dev/null +++ b/src-trader-dashboard/reducers/fxDataReducer.jsx @@ -0,0 +1,16 @@ +export default (state = {fxData: [], fxTopMovers: []}, action) => { + switch (action.type) { + case 'FX_DATA_CHANGED': + return { + ...state, + fxData: action.fxData.slice(0), + }; + case 'FX_TOP_MOVERS_CHANGED': + return { + ...state, + fxTopMovers: action.fxTopMovers.slice(0), + }; + default: + return action.state; + } +}; \ No newline at end of file diff --git a/src-trader-dashboard/services/FxDataService.jsx b/src-trader-dashboard/services/FxDataService.jsx index af21292..f5beb5f 100644 --- a/src-trader-dashboard/services/FxDataService.jsx +++ b/src-trader-dashboard/services/FxDataService.jsx @@ -1,7 +1,8 @@ import sampleSize from "lodash/sampleSize"; +import cloneDeep from "lodash/cloneDeep"; import StoreService from "./StoreService"; -import fxDataUpdated from "../actions/fxDataUpdated"; +import {fxDataUpdated, fxTopMoversUpdated} from "../actions/fxDataActions"; import HorizontalBarComponent from "../components/renderers/HorizontalBarComponent.jsx"; @@ -9,14 +10,32 @@ export default class { constructor() { this.store = StoreService.STORE; - this.fxData = this.createFxMatrixSnapshot(); + this.createFxMatrixSnapshot(); + this.calculateTopMovers(); this.store.dispatch(fxDataUpdated(this.fxData)); + this.store.dispatch(fxTopMoversUpdated(this.fxTopMovers)); setInterval(() => { this.applyDeltasToFxData(); + this.store.dispatch(fxDataUpdated(this.fxData)); - }, 1500) + }, 1500); + + setInterval(() => { + this.calculateTopMovers(); + + this.store.dispatch(fxTopMoversUpdated(this.fxTopMovers)); + }, 2500); + } + + calculateTopMovers() { + let fxData = cloneDeep(this.fxData); + fxData.sort((a, b) => { + return Math.abs(b.pct_net_change) - Math.abs(a.pct_net_change); + }); + + this.fxTopMovers = fxData.slice(0, 20); } applyDeltasToFxData() { @@ -34,7 +53,7 @@ export default class { row['pct_net_change'] = multiplier * (this.random(30, 100) / 100).toFixed(2); for (let symbolToUpdate of fxSymbolsToUpdate) { - if(row[symbolToUpdate] === null) { + if (row[symbolToUpdate] === null) { continue; } @@ -42,7 +61,6 @@ export default class { row[symbolToUpdate] = (multiplier * Math.random()).toFixed(2); } } - return this.fxData; } random(min, max) { @@ -75,7 +93,8 @@ export default class { rowData.push(row); } - return rowData; + + this.fxData = rowData; } } @@ -169,251 +188,3 @@ const FX_CURRENCY_MATRIX = [ ["CHFJPY", "0.60", "0.96", "0.22", "0.52", "0.63", "0.24", "0.76", "0.91", "0.80", "-0.14", "0.59", "-0.01", "0.60", "-0.08", "0.63", "0.39", "0.30", "-0.71", "0.18", "0.94", "-0.30", "0.28", "0.87", "0.92", "-0.19", "0.18", "-0.40", "0.44", "0.88", "0.52", "0.50", "0.44", "0.84", "0.24", "0.09", "0.15", "0.10", "0.30", "-0.05", "0.79", null, "0.99"], ["CHFCAD", "0.93", "0.24", "0.05", "0.40", "0.75", "0.12", "0.90", "0.56", "0.46", "0.62", "0.71", "0.01", "0.04", "0.02", "0.49", "0.51", "-0.94", "0.93", "0.80", "-0.40", "0.79", "0.82", "-0.04", "0.38", "-0.24", "-0.17", "0.78", "-0.42", "0.92", "-0.83", "-0.01", "0.54", "0.73", "0.52", "0.95", "0.44", "0.90", "0.81", "0.42", "0.26", "0.36", null] ]; - -// for (i = 0; i < MAJOR_CURRENCIES.length; i++) { -// for (j = 0; j < MAJOR_CURRENCIES.length; j++) { -// if (i === j) { -// continue; -// } -// console.log(`${MAJOR_CURRENCIES[i] + MAJOR_CURRENCIES[j]}`); -// } -// } - -// let rows = []; -// let row = ['Symbol']; -// -// rows = []; -// for (j = 0; j < FX_CURRENCY_CODE.length; j++) { -// row.push(FX_CURRENCY_CODE[j]); -// } -// rows.push(row); -// -// for (i = 0; i < FX_CURRENCY_CODE.length; i++) { -// let row = []; -// row.push(FX_CURRENCY_CODE[i]); -// -// for (j = 0; j < FX_CURRENCY_CODE.length; j++) { -// if (i === j) { -// row.push(null); -// } else { -// var mutliplier = ((Math.random() * 10) > 8) ? -1 : 1; -// row.push((mutliplier * Math.random()).toFixed(2)) -// // row.push(`${FX_CURRENCY_CODE[j]} value`) -// } -// } -// -// rows.push(row); -// } -// -// JSON.stringify(rows); -// -// const FX_CURRENCY_CODE = [ -// 'USDGBP', -// 'USDEUR', -// 'USDAED', -// 'USDJPY', -// 'USDCAD', -// 'USDCHF', -// 'GBPUSD', -// 'GBPEUR', -// 'GBPAED', -// 'GBPJPY', -// 'GBPCAD', -// 'GBPCHF', -// 'EURUSD', -// 'EURGBP', -// 'EURAED', -// 'EURJPY', -// 'EURCAD', -// 'EURCHF', -// 'AEDUSD', -// 'AEDGBP', -// 'AEDEUR', -// 'AEDJPY', -// 'AEDCAD', -// 'AEDCHF', -// 'JPYUSD', -// 'JPYGBP', -// 'JPYEUR', -// 'JPYAED', -// 'JPYCAD', -// 'JPYCHF', -// 'CADUSD', -// 'CADGBP', -// 'CADEUR', -// 'CADAED', -// 'CADJPY', -// 'CADCHF', -// 'CHFUSD', -// 'CHFGBP', -// 'CHFEUR', -// 'CHFAED', -// 'CHFJPY', -// 'CHFCAD' -// ]; -// -// const MAJOR_CURRENCIES = [ -// 'USD', -// 'GBP', -// 'EUR', -// 'AED', -// 'JPY', -// 'CAD', -// 'CHF']; -// -// const SECONDARY_CURRENCIES = [ -// 'AFN', -// 'ALL', -// 'AMD', -// 'ANG', -// 'AOA', -// 'ARS', -// 'AUD', -// 'AWG', -// 'AZN', -// 'BAM', -// 'BBD', -// 'BDT', -// 'BGN', -// 'BHD', -// 'BIF', -// 'BMD', -// 'BND', -// 'BOB', -// 'BRL', -// 'BSD', -// 'BTN', -// 'BWP', -// 'BYN', -// 'BZD', -// 'CDF', -// 'CLP', -// 'CNY', -// 'COP', -// 'CRC', -// 'CUC', -// 'CUP', -// 'CVE', -// 'CZK', -// 'DJF', -// 'DKK', -// 'DOP', -// 'DZD', -// 'EGP', -// 'ERN', -// 'ETB', -// 'FJD', -// 'FKP', -// 'GEL', -// 'GGP', -// 'GHS', -// 'GIP', -// 'GMD', -// 'GNF', -// 'GTQ', -// 'GYD', -// 'HKD', -// 'HNL', -// 'HRK', -// 'HTG', -// 'HUF', -// 'IDR', -// 'ILS', -// 'IMP', -// 'INR', -// 'IQD', -// 'IRR', -// 'ISK', -// 'JEP', -// 'JMD', -// 'JOD', -// 'KES', -// 'KGS', -// 'KHR', -// 'KMF', -// 'KPW', -// 'KRW', -// 'KWD', -// 'KYD', -// 'KZT', -// 'LAK', -// 'LBP', -// 'LKR', -// 'LRD', -// 'LSL', -// 'LYD', -// 'MAD', -// 'MDL', -// 'MGA', -// 'MKD', -// 'MMK', -// 'MNT', -// 'MOP', -// 'MRO', -// 'MUR', -// 'MVR', -// 'MWK', -// 'MXN', -// 'MYR', -// 'MZN', -// 'NAD', -// 'NGN', -// 'NIO', -// 'NOK', -// 'NPR', -// 'NZD', -// 'OMR', -// 'PAB', -// 'PEN', -// 'PGK', -// 'PHP', -// 'PKR', -// 'PLN', -// 'PYG', -// 'QAR', -// 'RON', -// 'RSD', -// 'RUB', -// 'RWF', -// 'SAR', -// 'SBD', -// 'SCR', -// 'SDG', -// 'SEK', -// 'SGD', -// 'SHP', -// 'SLL', -// 'SOS', -// 'SPL', -// 'SRD', -// 'STD', -// 'SVC', -// 'SYP', -// 'SZL', -// 'THB', -// 'TJS', -// 'TMT', -// 'TND', -// 'TOP', -// 'TRY', -// 'TTD', -// 'TVD', -// 'TWD', -// 'TZS', -// 'UAH', -// 'UGX', -// 'UYU', -// 'UZS', -// 'VEF', -// 'VND', -// 'VUV', -// 'WST', -// 'XAF', -// 'XCD', -// 'XDR', -// 'XOF', -// 'XPF', -// 'YER', -// 'ZMW', -// 'ZWD', -// 'ZAR']; diff --git a/src-trader-dashboard/services/StoreService.js b/src-trader-dashboard/services/StoreService.js index 909981d..a18d310 100644 --- a/src-trader-dashboard/services/StoreService.js +++ b/src-trader-dashboard/services/StoreService.js @@ -1,6 +1,6 @@ import {createStore} from "redux"; -import fxData from "../reducers/fxData"; +import fxData from "../reducers/fxDataReducer.jsx"; class StoreService { }