diff options
author | 9308233900 <reagan_hunt@brown.edu> | 2021-04-20 10:24:34 -0700 |
---|---|---|
committer | 9308233900 <reagan_hunt@brown.edu> | 2021-04-20 10:24:34 -0700 |
commit | 2e3243bb52b23571df529697d841f883846a8954 (patch) | |
tree | 315eda2621ddc65d96472e2fc29548356d25425b | |
parent | 564295d2ac6b40e349a1cbc3e3bd329989e9ec82 (diff) | |
parent | 4411ae1564d716e5aa063e4c47302ffc907a078a (diff) |
Merge branch 'master' of https://github.com/cs0320-2021/term-project-cohwille-jmccaul3-mfoiani-rhunt2master
37 files changed, 1115 insertions, 260 deletions
diff --git a/data/mock_tradeClarks.sqlite3 b/data/mock_tradeClarks.sqlite3 Binary files differdeleted file mode 100644 index 980c539..0000000 --- a/data/mock_tradeClarks.sqlite3 +++ /dev/null diff --git a/data/profit_testing.sqlite3 b/data/profit_testing.sqlite3 Binary files differnew file mode 100644 index 0000000..33dd2b8 --- /dev/null +++ b/data/profit_testing.sqlite3 diff --git a/data/trades.sqlite3 b/data/trades.sqlite3 Binary files differindex 878261a..a15d12e 100644 --- a/data/trades.sqlite3 +++ b/data/trades.sqlite3 diff --git a/react-frontend/public/assets/outline_cancel_white_18dp.png b/react-frontend/public/assets/outline_cancel_white_18dp.png Binary files differnew file mode 100644 index 0000000..b989e20 --- /dev/null +++ b/react-frontend/public/assets/outline_cancel_white_18dp.png diff --git a/react-frontend/public/assets/outline_close_white_18dp.png b/react-frontend/public/assets/outline_close_white_18dp.png Binary files differnew file mode 100644 index 0000000..65867f3 --- /dev/null +++ b/react-frontend/public/assets/outline_close_white_18dp.png diff --git a/react-frontend/public/assets/outline_minimize_white_18dp.png b/react-frontend/public/assets/outline_minimize_white_18dp.png Binary files differnew file mode 100644 index 0000000..a6a12d6 --- /dev/null +++ b/react-frontend/public/assets/outline_minimize_white_18dp.png diff --git a/react-frontend/public/assets/outline_search_white_18dp.png b/react-frontend/public/assets/outline_search_white_18dp.png Binary files differnew file mode 100644 index 0000000..52ffe5f --- /dev/null +++ b/react-frontend/public/assets/outline_search_white_18dp.png diff --git a/react-frontend/public/assets/outline_tune_white_18dp.png b/react-frontend/public/assets/outline_tune_white_18dp.png Binary files differnew file mode 100644 index 0000000..4168a63 --- /dev/null +++ b/react-frontend/public/assets/outline_tune_white_18dp.png diff --git a/react-frontend/public/round_arrow_back_white_18dp.png b/react-frontend/public/assets/round_arrow_back_white_18dp.png Binary files differindex bbaccda..bbaccda 100644 --- a/react-frontend/public/round_arrow_back_white_18dp.png +++ b/react-frontend/public/assets/round_arrow_back_white_18dp.png diff --git a/react-frontend/public/assets/round_expand_less_white_18dp.png b/react-frontend/public/assets/round_expand_less_white_18dp.png Binary files differnew file mode 100644 index 0000000..a64f430 --- /dev/null +++ b/react-frontend/public/assets/round_expand_less_white_18dp.png diff --git a/react-frontend/public/assets/round_expand_more_white_18dp.png b/react-frontend/public/assets/round_expand_more_white_18dp.png Binary files differnew file mode 100644 index 0000000..f8c7213 --- /dev/null +++ b/react-frontend/public/assets/round_expand_more_white_18dp.png diff --git a/react-frontend/public/link-icon.svg b/react-frontend/public/link-icon.svg new file mode 100644 index 0000000..926cbcb --- /dev/null +++ b/react-frontend/public/link-icon.svg @@ -0,0 +1 @@ +<?xml version="1.0" ?><svg fill="none" height="24" stroke="lightgreen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" x2="21" y1="14" y2="3"/></svg>
\ No newline at end of file diff --git a/react-frontend/src/App.js b/react-frontend/src/App.js index 2cef235..1461c5a 100644 --- a/react-frontend/src/App.js +++ b/react-frontend/src/App.js @@ -12,18 +12,17 @@ import mainlogo from './images/mainlogo.png'; import './css/Landing.css'; function App() { - const [startApp, setStartApp] = useState(false); - - const startModal = () => { - document.getElementById("main-modal").style.display = 'block'; - setStartApp(false); - } - const exitModal = () => { - document.getElementById("main-modal").style.display = 'none'; - setStartApp(true); - } - - + const [startApp, setStartApp] = useState(false); + + const startModal = () => { + document.getElementById("main-modal").style.display = 'block'; + setStartApp(false); + } + const exitModal = () => { + document.getElementById("main-modal").style.display = 'none'; + setStartApp(true); + } + return ( <> diff --git a/react-frontend/src/components/EdgeInfo.js b/react-frontend/src/components/EdgeInfo.js new file mode 100644 index 0000000..566c70a --- /dev/null +++ b/react-frontend/src/components/EdgeInfo.js @@ -0,0 +1,59 @@ + +import '../css/UserCheckin.css'; +import { useEffect, useState } from "react"; + +/** + * Componenet for checkins. Has a toggle to show more info. + * @param {Object} props The props of the component. + * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info. + */ +function EdgeInfo(props) { + /* // State - toggled + const [stockList, setStockList] = useState([]); + + const stockInfo = stockList.map((stock) => + <li>{stock}</li> + ); + + + const getEdgeInfo = (fromID, toID) => { + fetch("http://localhost:4567/edge-data", { + method: "POST", + body: JSON.stringify({ + followerID: fromID, + leaderID: toID, + }), + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin" + }) + .then(res => res.json()) + .then(data => { + console.log(data); + setStockList(data); + }) + .catch(err => console.log(err)); + } + + useEffect(() => getEdgeInfo(), [props.selectedFollowerID, props.selectedLeaderID]); + */ + + + return ( + <div> + + </div> + /*<div className='Chosen-user' hidden={!props.showEdgeInfo}> + <h3> + <span onClick={() => props.setShowEdgeInfo(false)}> + <img className="Img-btn" src="assets/round_arrow_back_white_18dp.png" alt="image" /> + </span> + </h3> + <div className = 'edge-info'> + {stockInfo} + </div> + </div>*/); +} + +export default EdgeInfo;
\ No newline at end of file diff --git a/react-frontend/src/components/Hub.js b/react-frontend/src/components/Hub.js index 8a3ac1c..1906684 100644 --- a/react-frontend/src/components/Hub.js +++ b/react-frontend/src/components/Hub.js @@ -10,12 +10,25 @@ import '../css/UserCheckin.css'; * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info. */ function Hub(props) { - // State - toggled + const LEN_NAME = 15; + + const [isHover, setIsHover] = useState(false); + + const formatName = name => { + if (name.length >= LEN_NAME) { + return props.name.substring(0, LEN_NAME - 3) + '...'; + } + return props.name; + } return ( <li className='Checkin'> <div className="Img-flex"> - <span className="Clickable-name" onClick= {() => console.log(props.id)}>{props.name}</span> + <span + className="Clickable-name" + onMouseOver = {() => setIsHover(true)} + onMouseLeave = {() => setIsHover(false)} + onClick = {() => props.setSelectedId(props.id)}>{isHover || props.searching ? props.name : formatName(props.name)}</span> <span>{props.value.toFixed(3)}</span> </div> </li>); diff --git a/react-frontend/src/components/HubList.js b/react-frontend/src/components/HubList.js index 0df3020..383d570 100644 --- a/react-frontend/src/components/HubList.js +++ b/react-frontend/src/components/HubList.js @@ -1,63 +1,64 @@ // React and component imports import { useEffect, useState } from "react"; import Hub from "./Hub.js"; -import InvestorInfo from "./InvestorInfo.js"; +import uuid from 'react-uuid'; // CSS import -import '../css/UserCheckin.css'; +import "../css/UserCheckin.css"; /** * Component that build the checkin list and displays checkin info. - * @returns {import('react').HtmlHTMLAttributes} A div with the hubs + * @returns {import('react').HtmlHTMLAttributes} A div with the hubs * in a vertical layout. */ function HubList(props) { - const [hubItems, setHubItems] = useState([]); - const [isSelected, setIsSelected] = useState(false); - const [name, setName] = useState(''); - + const [displayedItems, setDisplayedItems] = useState([]); + /** - * Loads new the checkins into the current cache/map of hubs. + * Method that determines whehter the Hub should be showed. + * @returns {Boolean} True if to be shown, false if not. */ - const updateHubItems = () => { - // sort and create the elemnts - let hubs = []; - //const sorted = props.data.sort((a, b) => b.suspicionScore - a.suspicionScore); - props.data.forEach(hub => hubs.push( - <Hub key={hub.id} id={hub.id} name={hub.name} value={hub.suspicionScore} setSelected={props.setSelected}></Hub> - )); - - setHubItems(hubs); + const toInclude = holder => { + // TODO: add number search or differentiate between it + // TODO: add sus score range.... + if (!holder) { + return false; + }; + + // const matchingId = holder.id.toString().includes(queryString.toLowerCase()); + //console.log(props.queryString.toLowerCase(), props.data.length); + const matchingName = holder.name.toLowerCase().includes(props.queryString.toLowerCase()); + return matchingName; } - const getName = () => { - props.data.forEach(hub => { - if (hub.id == props.selected) { - setName(hub.name); - } - }) - setName(''); + const toHubElement = hub => + <Hub + id={hub.id} + name={hub.name} + value={hub.suspicionScore} + setSelectedId={props.setSelectedId} + searching={props.searching} + ></Hub>; + + /** + * Filters the items to be shown, then created the iteams and sets the state with the items. + */ + const filterItems = () => { + if (!props.queryString) { + // don't need to show all unless searching + return setDisplayedItems(props.data.slice(0,600).map(hub => toHubElement(hub))) + } + + const criteria = props.data.filter(holder => toInclude(holder)); + setDisplayedItems(criteria.map(hub => toHubElement(hub))); } - - // React hook that updates when the hubs are recalculated - useEffect(() => updateHubItems(), [props.data]); - - //React hook to show data for an investor - useEffect(() => { - setIsSelected(true) - getName(); - }, [props.selected]); - - return ( - <div className="User-checkin"> - <div className="Checkins"> - <h2>Suspicion Ranks</h2> - <ul className='Checkin-list'>{hubItems}</ul> - </div> - <InvestorInfo personId={props.selected} isSelected={isSelected} name={name} dates={props.dates}></InvestorInfo> - </div> - ); + /** + * Hook to update the items on change of the search string or update of data. + */ + useEffect(() => filterItems(), [props.queryString, props.data, props.searching]); + + return <ul className='Checkin-list'>{displayedItems}</ul>; } -export default HubList;
\ No newline at end of file +export default HubList; diff --git a/react-frontend/src/components/HubWidget.js b/react-frontend/src/components/HubWidget.js new file mode 100644 index 0000000..f0434d7 --- /dev/null +++ b/react-frontend/src/components/HubWidget.js @@ -0,0 +1,84 @@ +// React and component imports +import { useEffect, useState, useRef } from "react"; +import Hub from "./Hub.js"; +import InvestorInfo from "./InvestorInfo.js"; + +// CSS import +import '../css/UserCheckin.css'; +import HubList from "./HubList.js"; + +/** + * Component that build the checkin list and displays checkin info. + * @returns {import('react').HtmlHTMLAttributes} A div with the hubs + * in a vertical layout. + */ +function HubWidget(props) { + // States for selected person + const [followers, setFollowers] = useState([]); + const [selectedName, setSelectedName] = useState([]); + + const [searching, setSearching] = useState(false); + const textInput = useRef(); + + const [showSelectedInfo, setShowSelectedInfo] = useState(false); + + const updateSelected = () => { + props.data.forEach(holder => { + if (holder.id === props.selectedId) { + console.log(holder); + setFollowers(holder.followers); + setSelectedName(holder.name); + } + }); + } + + // Don't need to fetch if repeat id... + const updateSelectedId = id => { + setShowSelectedInfo(true); + props.setSelectedId(id); + } + + // Hook to update and show the info when a user is clicked on ... + useEffect(() => { + updateSelected(); + }, [props.selectedId]); + // On init, don't show... + useEffect(() => setShowSelectedInfo(false), []); + + useEffect(() => { + if (searching) { + return; + } + textInput.current.value = ""; + setQueryString(""); + }, [searching]) + + const [queryString, setQueryString] = useState(""); + + return ( + <> + <div className="User-checkin"> + <div className="Checkins"> + <h2 className="Img-flex Title"> + <span hidden={searching}>Sus Ranks</span> + <input ref={textInput} hidden={!searching} type="search" onChange={e => setQueryString(e.target.value)} placeholder="Search by name"></input> + <img className="Img-btn" hidden={searching} onClick={() => setSearching(true)} src="assets/outline_search_white_18dp.png" alt="image"/> + <img className="Img-btn" hidden={!searching} onClick={() => setSearching(false)} src="assets/outline_cancel_white_18dp.png" alt="image"/> + </h2> + <HubList data={props.data} setSelectedId={updateSelectedId} queryString={queryString} searching={searching}></HubList> + </div> + <InvestorInfo + selectedId={props.selectedId} + setSelectedId={updateSelectedId} + dates={props.dates} + setShowSelectedInfo={setShowSelectedInfo} + showSelectedInfo={showSelectedInfo} + followers={followers} + name={selectedName} + ></InvestorInfo> + </div> + </> + ); +} + +export default HubWidget;
\ No newline at end of file diff --git a/react-frontend/src/components/InvestorInfo.js b/react-frontend/src/components/InvestorInfo.js index d368984..9e18299 100644 --- a/react-frontend/src/components/InvestorInfo.js +++ b/react-frontend/src/components/InvestorInfo.js @@ -2,7 +2,9 @@ import { useEffect, useState } from "react"; // CSS import -import '../css/UserCheckin.css'; +import "../css/UserCheckin.css"; +import "../css/InvesterInfo.css"; +import uuid from "react-uuid"; /** * Componenet for checkins. Has a toggle to show more info. @@ -10,57 +12,154 @@ import '../css/UserCheckin.css'; * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info. */ function InvestorInfo(props) { - const [info, setInfo] = useState({}); + const [info, setInfo] = useState({ + percentGain: 0, + moneyIn: 0, + moneyOut: 0, + percentSP500: 0, + holdings: [], + }); - const toEpochMilli = date => Date.parse(date); - const getInfo = () => { - console.log({ - person: props.name, - start: toEpochMilli(props.dates.start), - end: toEpochMilli(props.dates.end) - }); + const [showStocks, setShowStocks] = useState(false); + const [showFollowers, setShowFollowers] = useState(false); - if (props.name === "") { + const getInfo = () => { + if (props.selectedId === -1) { return; } - + + const toEpochMilli = (date) => Date.parse(date); + + console.log({ + selectedId: props.selectedId, + startTime: toEpochMilli(props.dates.start), + endTime: toEpochMilli(props.dates.end), + }); + fetch("http://localhost:4567/profit", { - method: "POST", - body: JSON.stringify({ - person: props.name, - start: toEpochMilli(props.dates.start), - end: toEpochMilli(props.dates.end) - }), - headers: { - "Content-Type": "application/json", - }, - credentials: "same-origin" + method: "POST", + body: JSON.stringify({ + selectedId: props.selectedId, + startTime: toEpochMilli(props.dates.start), + endTime: toEpochMilli(props.dates.end), + }), + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin", }) - .then(res => { - console.log(res); - res.json(); - }) - .then(data => { - console.log(data); - setInfo(data); - }) - .catch(err => console.log(err)); - } - /* + .then(res => res.json()) + .then(data => { + console.log(data); + setInfo(data); + props.setShowSelectedInfo(true); + }) + .catch(err => console.log(err)); + }; + + const stockTable = () => { + return ( + <> + <li class="Stock-row"> + <div className="tableHeader"> + <div className="symbol-row">Symbol</div> + <div className="gain-row">Realized gain</div> + <div className="gain-row">Unrealized gain</div> + </div> + </li> + {info.holdings.map((holding) => ( + <li class="Stock-row"> + <div className="symbol-row">{holding.ticker}</div> + <div className="gain-row">{holding.realizedGain.toFixed(3)}</div> + <div className="gain-row">{holding.unrealizedGain.toFixed(3)}</div> + </li> + ))} + </> + ); + }; - const coords = userCoords.map((coord, index) => - <li key={index}> - <span>{'('+coord[0].toFixed(6)}, {coord[1].toFixed(6)+')'}</span> - </li> - );*/ + const followerList = () => + props.followers.map((follower) => ( + <li key={uuid()} class="Clickable-name" onClick={() => props.setSelectedId(follower.id)}> + {follower.name} + </li> + )); - useEffect(() => getInfo(), [props.name, props.isSelected, props.personId]) + // Hook that updates when selected has changed + useEffect(() => getInfo(), [props.selectedId]); return ( - <div className="Chosen-user" hidden={props.isSelected}> - hi + <div className="Chosen-user" hidden={!props.showSelectedInfo}> + <h3> + <span onClick={() => props.setShowSelectedInfo(false)}> + <img className="Img-btn" src="assets/round_arrow_back_white_18dp.png" alt="image" /> + </span> + <span>CIK: {props.selectedId}</span> + </h3> + <h2 id="investerName"> + <a href={"https://sec.report/CIK/" + props.selectedId + "/Insider-Trades"} target="_blank"> + {props.name} + <img src="link-icon.svg" alt="image" /> + </a> + </h2> + <div id="top-bar"> + <div id="gain-number"> + <p className="bigNumber">${(info.moneyOut - info.moneyIn).toFixed(3)}</p> gained + </div> + + <div> + <p className="bigNumber">{info.percentGain.toFixed(3)}%</p> + compared to {info.percentSP500.toFixed(3)}% on SP500 + </div> + </div> + + <div> + <div className="Checkin" hidden={info.holdings.length === 0}> + <div className="Img-flex"> + <span className="tableHeader">View holdings</span> + <img + className="Img-btn" + hidden={showStocks} + onClick={() => setShowStocks((toggle) => !toggle)} + src="assets/round_expand_more_white_18dp.png" + alt="image" + /> + <img + className="Img-btn" + hidden={!showStocks} + onClick={() => setShowStocks((toggle) => !toggle)} + src="assets/round_expand_less_white_18dp.png" + alt="image" + /> + </div> + <ul hidden={!showStocks} class="Stock-table"> + {stockTable()} + </ul> + </div> + + <div className="Checkin" hidden={!followerList}> + <div className="Img-flex"> + <span className="tableHeader">View followers</span> + <img + className="Img-btn" + hidden={showFollowers} + onClick={() => setShowFollowers((toggle) => !toggle)} + src="assets/round_expand_more_white_18dp.png" + alt="image" + /> + <img + className="Img-btn" + hidden={!showFollowers} + onClick={() => setShowFollowers((toggle) => !toggle)} + src="assets/round_expand_less_white_18dp.png" + alt="image" + /> + </div> + <ul hidden={!showFollowers}>{followerList()}</ul> + </div> + </div> </div> ); } -export default InvestorInfo;
\ No newline at end of file +export default InvestorInfo; diff --git a/react-frontend/src/components/TimeSelector.js b/react-frontend/src/components/TimeSelector.js index 652a9ec..997494d 100644 --- a/react-frontend/src/components/TimeSelector.js +++ b/react-frontend/src/components/TimeSelector.js @@ -34,9 +34,8 @@ function TimeSelector(props) { <DateSelector side={"left"} name={"Start Date"} className="Coord-select-left" clickedFunc={setCurrent} changedFunc={setStartDate} disabled={current==='start' || props.isChanging} value={toValue(startDate)}></DateSelector> <div> - <h2>Adjust Timeframe</h2> <button className="Btn Route-btn" onClick={() => changeTimeframe()} - disabled={current!=="" || props.isChanging}>Change Timeframe</button> + disabled={current!=="" || props.isChanging}>Adjust Timeframe</button> </div> <DateSelector side={"right"} name={"End Date"} className="Coord-select-right" clickedFunc={setCurrent} changedFunc={setEndDate} disabled={current==='end' || props.isChanging} value={toValue(endDate)}></DateSelector> diff --git a/react-frontend/src/components/Visualization.js b/react-frontend/src/components/Visualization.js index 1975e86..d65d106 100644 --- a/react-frontend/src/components/Visualization.js +++ b/react-frontend/src/components/Visualization.js @@ -1,63 +1,114 @@ // JS module imports -import { useEffect, useRef, useState } from "react"; -import uuid from 'react-uuid'; +import { useMemo, useState } from "react"; import Graph from 'vis-react'; // CSS imports import '../css/Canvas.css'; +import EdgeInfo from "./EdgeInfo"; + /** - * This function renders and mantains thhe canvas. + * This function renders and mantains the canvas. * @param {Object} props The props for the canvas. * @returns {import("react").HtmlHTMLAttributes} The canvas to be retured. */ function Visualization(props) { + + + + const [key, setKey] = useState(0); + + const [graphState, setGraphState] = useState({ + nodes: [], + edges: [] + }); + const options = { + autoResize: true, edges: { color: "#ffffff" } }; + const events = { - select: () => event => props.setSelected(event.nodes[0]) - }; + selectNode: event => { + props.setSelectedId(event.nodes[0]) + } + } - const [graphState, setGraphState] = useState({ - nodes: [], - edges: [] - }); const getNodes = () => { let nodes = []; + const maxScore = props.data[0].suspicionScore; + const interval = maxScore / 4; + props.data.forEach(hub => { if (hub.followers) { + let colorVal = '#f6f7d4'; + const score = hub.suspicionScore; + + if(score > (maxScore - interval)){ + colorVal = '#d92027' + } + if(score <= (maxScore - interval) && score > (maxScore - interval*2)){ + colorVal = '#f37121' + } + if(score <= (maxScore - interval*2) && score > (maxScore - interval*3)){ + colorVal = '#fdca40' + } nodes.push({ id: hub.id, + autoResize: true, label: hub.name, - size: hub.suspicionScore + labelHighlightBold: true, + shape: "dot", + value: hub.suspicionScore*1000, + color: { + background: colorVal, + border: '#2b2e4a', + highlight:{ + background: '#29bb89', + border: '#fdca40' + } + }, + font: { + color: '#9fd8df', + size: 20, + } }); } }); return nodes; } + const getEdges = () => { let edges = [] props.data.forEach(hub => { hub.followers.forEach(follower => { edges.push({ - from: hub.id, - to: follower.id + from: follower.id, + to: hub.id, + dashes: false, + color:{ + opacity: 0.7, + highlight:'#fdca40', + }, }); }); }); return edges; } - // Hooks to update graph state - useEffect(() => setGraphState({nodes: getNodes(), edges: getEdges()}), [JSON.stringify(props.data)]); + // Hooks to update graph state when data changes + useMemo(() => { + setKey(key => key + 1); + setGraphState({nodes: getNodes(), edges: getEdges()}) + }, [JSON.stringify(props.data)]); + return ( <div className="Map-canvas"> <Graph - key={uuid()} + key={key} graph={graphState} options={options} events={events}> diff --git a/react-frontend/src/components/WatchDogs.js b/react-frontend/src/components/WatchDogs.js index d631ea9..f7a9e74 100644 --- a/react-frontend/src/components/WatchDogs.js +++ b/react-frontend/src/components/WatchDogs.js @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react'; import TimeSelector from './TimeSelector.js'; import Visualization from './Visualization.js'; -import HubList from './HubList.js'; +import HubWidget from './HubWidget.js'; import Loading from './Loading.js'; import Modal from './Modal.js'; import logo from './images/logo.png'; @@ -26,8 +26,8 @@ function WatchDogs() { }); // State for visualization data const [data, setData] = useState([]); - // State for selected person - const [selected, setSelected] = useState(-1); + // State for selectedId + const [selectedId, setSelectedId] = useState(-1); const toEpochMilli = date => Date.parse(date); const getGraphData = () => { @@ -49,9 +49,14 @@ function WatchDogs() { .then(res => res.json()) .then(data => { //TODO: optimize this - const sliced = data.holders.slice(0, 500); - console.log(sliced); - setData(sliced); + //const sliced = data.holders.slice(0, 500); + //console.log(sliced); + console.log(data); + if(data.holders.length === 0) { + alert("There is no data between those timeframes :("); + return; + } + setData(data.holders); setHasLoaded(true); }) .catch(err => console.log(err)); @@ -77,9 +82,9 @@ function WatchDogs() { <div className="Canvas-filler Canvas-filler-1"></div> <div className="Canvas-filler Canvas-filler-2"></div> <div className="Canvas-filler Canvas-filler-3"></div> - <HubList setHasLoaded={setHasLoaded} data={data} setSelected={setSelected} selected={selected} dates={dates}></HubList> + <HubWidget setHasLoaded={setHasLoaded} data={data} dates={dates} selectedId={selectedId} setSelectedId={setSelectedId}></HubWidget> <TimeSelector isChanging={isChanging} dates={dates} setDates={setDates}></TimeSelector> - <Visualization hasLoaded={hasLoaded} data={data} setSelected={setSelected}></Visualization> + <Visualization hasLoaded={hasLoaded} data={data.slice(0, 600)} setSelectedId={setSelectedId}></Visualization> </div> } </> diff --git a/react-frontend/src/components/images/person.svg b/react-frontend/src/components/images/person.svg new file mode 100644 index 0000000..6a93d8f --- /dev/null +++ b/react-frontend/src/components/images/person.svg @@ -0,0 +1 @@ +<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Person</title><path d='M332.64 64.58C313.18 43.57 286 32 256 32c-30.16 0-57.43 11.5-76.8 32.38-19.58 21.11-29.12 49.8-26.88 80.78C156.76 206.28 203.27 256 256 256s99.16-49.71 103.67-110.82c2.27-30.7-7.33-59.33-27.03-80.6zM432 480H80a31 31 0 01-24.2-11.13c-6.5-7.77-9.12-18.38-7.18-29.11C57.06 392.94 83.4 353.61 124.8 326c36.78-24.51 83.37-38 131.2-38s94.42 13.5 131.2 38c41.4 27.6 67.74 66.93 76.18 113.75 1.94 10.73-.68 21.34-7.18 29.11A31 31 0 01432 480z'/></svg>
\ No newline at end of file diff --git a/react-frontend/src/css/App.css b/react-frontend/src/css/App.css index e39eb3e..1b8de5f 100644 --- a/react-frontend/src/css/App.css +++ b/react-frontend/src/css/App.css @@ -7,6 +7,7 @@ grid-template-columns: max-content auto max-content max-content; background-color: #121212; + } .App-logo { diff --git a/react-frontend/src/css/CoordSelector.css b/react-frontend/src/css/CoordSelector.css index 881be08..e1fde99 100644 --- a/react-frontend/src/css/CoordSelector.css +++ b/react-frontend/src/css/CoordSelector.css @@ -49,4 +49,8 @@ width: 90%; } +.Flex-coord { + margin: auto; +} + diff --git a/react-frontend/src/css/EdgeInfo.css b/react-frontend/src/css/EdgeInfo.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/react-frontend/src/css/EdgeInfo.css diff --git a/react-frontend/src/css/InvesterInfo.css b/react-frontend/src/css/InvesterInfo.css new file mode 100644 index 0000000..c1bb137 --- /dev/null +++ b/react-frontend/src/css/InvesterInfo.css @@ -0,0 +1,62 @@ +#top-bar { + display: flex; + flex-direction: row; + justify-content: space-evenly; + margin-right: 10px; + margin-left: 10px; + border-bottom: solid 1px white; + margin-top: 0px; + padding-bottom: 20px; +} + +/* div { + border: solid white; +} */ + +/* p { + border: solid white; +} */ + +.bigNumber { + font-size: 16pt; + margin: 0; +} + +.Chosen-user { + background-color: #333333; + color: lightgreen; + width: 25vw; + border-radius: 10px; + overflow: scroll; +} + +.Stock-row { + display: flex; + justify-content: space-evenly; + color: white; + margin-bottom: 5px; +} + +.tableHeader { + display: flex; + flex-direction: row; + justify-content: left; + margin-right: 10px; + margin-left: 10px; + font-size: 120%; + padding: 5px; + color: lightgreen; +} + +.symbol-row { + margin-right: 20px; +} + +.gain-row { + margin-right: 20px; +} + +a { + color: lightgreen; + text-decoration: none; +} diff --git a/react-frontend/src/css/UserCheckin.css b/react-frontend/src/css/UserCheckin.css index 141cc01..e60b348 100644 --- a/react-frontend/src/css/UserCheckin.css +++ b/react-frontend/src/css/UserCheckin.css @@ -10,6 +10,7 @@ cursor: default; /* Transparent background */ background: rgba(0, 0, 0, 0); + overflow: scroll; } ul { @@ -20,7 +21,7 @@ ul { z-index: 10; background-color: #333333; border-radius: 20px; - margin: 5px; + margin: 5px 5px 5px 0; } .Coord-ex { @@ -30,23 +31,29 @@ ul { text-align: center; } +.Chosen-user > h3, .Checkins > h3 { + display: flex; + justify-content: space-between; + height: 5vh; + padding: 0 30px; +} + .Chosen-user > h2, .Checkins > h2 { display: flex; - justify-content: space-evenly; + justify-content: center; height: 5vh; - padding: 0 10px; + padding: 0 30px; } .Checkin-list { padding: 0 20px; - height: 86vh; - overflow-y: scroll; - cursor: default; + height: 85vh; + overflow: scroll; } .User-checkin-list { - height: 80vh; - overflow-y: scroll; + height: 75vh; + overflow: scroll; list-style-position: inside; padding: 0 20px; @@ -79,7 +86,7 @@ ul { .Img-btn { background-color: #424242; border-radius: 50%; - margin-right: 10px; + margin-left: auto; } .Img-btn:hover { @@ -91,4 +98,18 @@ ul { cursor: pointer; text-decoration: underline; color: lightgreen; +} + +/* CSS borrowed from W3 Schools */ +input[type=search] { + width: 80%; + padding: 12px 20px; + box-sizing: border-box; + font-size: 80%; + background-color: lightgoldenrodyellow; + border-radius: 2; +} + +.Title { + margin-top: 20px; }
\ No newline at end of file diff --git a/src/main/java/edu/brown/cs/student/term/DatabaseQuerier.java b/src/main/java/edu/brown/cs/student/term/DatabaseQuerier.java index 53c8cdc..2a9af65 100644 --- a/src/main/java/edu/brown/cs/student/term/DatabaseQuerier.java +++ b/src/main/java/edu/brown/cs/student/term/DatabaseQuerier.java @@ -125,16 +125,16 @@ public class DatabaseQuerier { return trades; } - public List<Trade> getAllTradesByHolder(String person, Date startDate, Date endDate) { + public List<Trade> getAllTradesByHolder(Integer holder_id, Date startDate, Date endDate) { LinkedList<Trade> trades = new LinkedList<>(); try { PreparedStatement prep; prep = - conn.prepareStatement("SELECT * FROM \'trades\' WHERE holder_name= ? " + conn.prepareStatement("SELECT * FROM \'trades\' WHERE holder_id = ?" + " AND trade_timestamp BETWEEN ? AND ?" + "order by trade_timestamp asc;"); - prep.setString(1, person); + prep.setInt(1, holder_id); prep.setDate(2, startDate); prep.setDate(3, endDate); ResultSet rs = prep.executeQuery(); diff --git a/src/main/java/edu/brown/cs/student/term/Main.java b/src/main/java/edu/brown/cs/student/term/Main.java index ee3bec1..37317c6 100644 --- a/src/main/java/edu/brown/cs/student/term/Main.java +++ b/src/main/java/edu/brown/cs/student/term/Main.java @@ -2,6 +2,7 @@ package edu.brown.cs.student.term; import com.google.common.collect.ImmutableMap; import edu.brown.cs.student.term.hub.Holder; +import edu.brown.cs.student.term.hub.LinkMapper; import edu.brown.cs.student.term.profit.ProfitCalculation; import edu.brown.cs.student.term.profit.StockHolding; import edu.brown.cs.student.term.hub.SuspicionRanker; @@ -78,12 +79,12 @@ public final class Main { setConnection.run(new String[] {"data/trades.sqlite3"}); } - if (!options.has("debug")) { + /*if (!options.has("debug")) { System.setErr(new PrintStream(new OutputStream() { public void write(int b) { } })); - } + }*/ HashMap<String, Command> commandHashMap = new HashMap<>(); @@ -137,6 +138,7 @@ public final class Main { Spark.post("/data", new SuspicionRankHandler()); Spark.post("/profit", new ProfitQueryHandler()); Spark.post("/trade-lookup", new TradeQueryHandler()); + Spark.post("/edge-data", new EdgeDataQueryHandler()); } /** @@ -178,24 +180,31 @@ public final class Main { @Override public Object handle(Request request, Response response) throws Exception { JSONObject req = new JSONObject(request.body()); - String person = req.getString("person"); + System.err.println("LOG: Call to /profit with " + req.toMap()); + Integer holder_id = req.getInt("selectedId"); Date startPeriod = new Date(req.getLong("startTime")); Date endPeriod = new Date(req.getLong("endTime")); ProfitCalculation profit = - new ProfitCalculation(DatabaseQuerier.getConn(), person, startPeriod, endPeriod); - List<StockHolding> holdings = profit.getHoldingsList(); - double gains = profit.calculateGains(); + new ProfitCalculation(DatabaseQuerier.getConn(), "", startPeriod, endPeriod); + List<StockHolding> holdings = profit.getHoldingsList(holder_id); + double gains = profit.calculateGainsSingle(holder_id); double sp500PercentGain = profit.compareToSP500(); + double percentGain = 100 * (gains / profit.getMoneyInput()); + if (profit.getMoneyInput() == 0) { + percentGain = 0; + } + Map<String, Object> res = new HashMap<>(); - res.put("person", person); + res.put("holder_id", holder_id); res.put("moneyIn", profit.getMoneyInput()); res.put("moneyOut", profit.getMoneyInput() + gains); res.put("holdings", holdings); - res.put("percentGain", 100 * gains / profit.getMoneyInput()); + res.put("percentGain", percentGain); res.put("SP500", (1 + sp500PercentGain) * profit.getMoneyInput()); res.put("percentSP500", 100 * sp500PercentGain); + System.err.println("LOG: Returning to GUI " + res); return GSON.toJson(res); } @@ -205,15 +214,26 @@ public final class Main { @Override public Object handle(Request request, Response response) throws Exception { JSONObject req = new JSONObject(request.body()); - String person = req.getString("person"); + Integer holder_id = req.getInt("selectedId"); Date startPeriod = new Date(req.getLong("startTime")); Date endPeriod = new Date(req.getLong("endTime")); DatabaseQuerier db = SetupCommand.getDq(); - List<Trade> trades = db.getAllTradesByHolder(person, startPeriod, endPeriod); + List<Trade> trades = db.getAllTradesByHolder(holder_id, startPeriod, endPeriod); return GSON.toJson(trades); + } + } + private static class EdgeDataQueryHandler implements Route { + @Override + public Object handle(Request request, Response response) throws Exception { + JSONObject req = new JSONObject(request.body()); + int leaderID = req.getInt("leaderID"); + int followerID = req.getInt("followerID"); + List<String> commonStocks = LinkMapper.getCommonTrades(leaderID, followerID); + System.out.println(commonStocks); + return GSON.toJson(commonStocks); } } diff --git a/src/main/java/edu/brown/cs/student/term/hub/LinkMapper.java b/src/main/java/edu/brown/cs/student/term/hub/LinkMapper.java index 31e2625..e749aff 100644 --- a/src/main/java/edu/brown/cs/student/term/hub/LinkMapper.java +++ b/src/main/java/edu/brown/cs/student/term/hub/LinkMapper.java @@ -11,8 +11,9 @@ public class LinkMapper { //TODO: Review what we actually need in here //not strictly necessary but may be nice to maintain - private List<List<Trade>> allTrades = new ArrayList<>(); + //private List<List<Trade>> allTrades = new ArrayList<>(); private Map<Holder, Set<Holder>> followerToLeaders = new HashMap<>(); + private static Map<Integer, Set<Trade>> holderIDToTrades = new HashMap<>(); private DatabaseQuerier databaseQuerier; public LinkMapper(DatabaseQuerier db){ @@ -55,6 +56,25 @@ public class LinkMapper { return followerToLeaders; } + public static List<String> getCommonTrades(int leaderID, int followerID){ + Set<Trade> leaderTrades = new HashSet<>(holderIDToTrades.get(leaderID)); + Set<Trade> followerTrades = new HashSet<>(holderIDToTrades.get(followerID)); + + leaderTrades.retainAll(followerTrades); + //TODO: Could retain WAY more info in here! + List<String> commonTrades = new ArrayList<>(); + for(Trade leaderTrade: leaderTrades){ + String buyType = ""; + if(leaderTrade.isBuy()){ + buyType = "Buy"; + } else{ + buyType = "Sell"; + } + commonTrades.add(buyType + ": " + leaderTrade.getStock()); + } + return commonTrades; + } + /** * Converts a single trade list into entries in the follower to leader map * @param tradeList - a list of trades for a single stock (either buy or sell) @@ -64,7 +84,15 @@ public class LinkMapper { //gets in order list of people for (Trade trade : tradeList) { - holderList.add(trade.getHolder()); + Holder currentHolder = trade.getHolder(); + holderList.add(currentHolder); + if(!holderIDToTrades.containsKey(currentHolder.getId())){ + Set<Trade> tradeSet = new HashSet<>(); + tradeSet.add(trade); + holderIDToTrades.put(currentHolder.getId(), tradeSet); + } else { + holderIDToTrades.get(currentHolder.getId()).add(trade); + } } //Set<Holder> followers = new HashSet<>(holderList); diff --git a/src/main/java/edu/brown/cs/student/term/hub/SuspicionRanker.java b/src/main/java/edu/brown/cs/student/term/hub/SuspicionRanker.java index 3283f5c..d37910e 100644 --- a/src/main/java/edu/brown/cs/student/term/hub/SuspicionRanker.java +++ b/src/main/java/edu/brown/cs/student/term/hub/SuspicionRanker.java @@ -16,8 +16,9 @@ public class SuspicionRanker { } private <K, V extends Comparable<V>> V getMaxOfMap(Map<K, V> map) { - Map.Entry<K, V> maxEntry = Collections.max(map.entrySet(), Map.Entry.comparingByValue()); - return maxEntry.getValue(); + //Map.Entry<K, V> maxEntry = Collections.max(map.entrySet(), Map.Entry.comparingByValue()); + Collection<V> values = map.values(); + return Collections.max(map.values()); } private <K, V extends Comparable<V>> V getMinOfMap(Map<K, V> map) { @@ -52,13 +53,12 @@ public class SuspicionRanker { HubSearch hub = new HubSearch(lm); Map<Holder, Double> holderToHubScore = hub.runHubSearch(start, end); - /* - ProfitCalculation pc = new ProfitCalculation(DatabaseQuerier.getConn(), "", new Date(start.toEpochMilli()), new Date(end.toEpochMilli())); Map<Integer, Double> profitMap = pc.getProfitMap(); + System.out.println(profitMap); //if the maps are empty, we abort because we have entirely incomplete data if(profitMap.isEmpty() || holderToHubScore.isEmpty()){ @@ -66,30 +66,34 @@ public class SuspicionRanker { } double profitMax = getMaxOfMap(profitMap); - /*if all of our values are negative, we need to flip sides so that the - * biggest loser doesn't end up being the most suspicious person*/ - /* + + //if all of our values are negative, we need to flip sides so that the + //biggest loser doesn't end up being the most suspicious person*/ if(profitMax <= 0) { profitMax = Math.abs(getMinOfMap(profitMap)); } - /*if both the min we found and max we found are 0, then we have - the special case where all the values are 0, in which case we - need to avoid dividing by 0*/ - /* + + //if both the min we found and max we found are 0, then we have + //the special case where all the values are 0, in which case we + //need to avoid dividing by 0 + if(profitMax == 0){ profitMax = 1; } - */ double hubMax = getMaxOfMap(holderToHubScore); for (Holder guy : holderToHubScore.keySet()) { - //double normalizedProfitScore = profitMap.get(guy.getId()) / profitMax; + double normalizedProfitScore = 0; + if (profitMap.containsKey(guy.getId())) { + normalizedProfitScore = profitMap.get(guy.getId()) / profitMax; + } double normalizedHubScore = holderToHubScore.get(guy) / hubMax; - double suspicionScore = normalizedHubScore; //* 0.6 + normalizedProfitScore * 0.4; + double suspicionScore = normalizedHubScore * 0.6 + normalizedProfitScore * 0.4; + guy.setSuspicionScore(suspicionScore); orderedSuspicion.add(guy); } diff --git a/src/main/java/edu/brown/cs/student/term/profit/ProfitCalculation.java b/src/main/java/edu/brown/cs/student/term/profit/ProfitCalculation.java index 15f31cc..4b19899 100644 --- a/src/main/java/edu/brown/cs/student/term/profit/ProfitCalculation.java +++ b/src/main/java/edu/brown/cs/student/term/profit/ProfitCalculation.java @@ -19,6 +19,11 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.LinkedList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.*; public class ProfitCalculation { @@ -71,6 +76,7 @@ public class ProfitCalculation { tablesFilled = false; } + /** * This method fills the maps of sell and buy orders with lists of oldest - new trades. */ @@ -78,25 +84,35 @@ public class ProfitCalculation { private String validateTicker(String ticker) { //this is cleaning some improperly formatted tickers ticker = ticker.replaceAll("[^a-zA-Z0-9]", "").toUpperCase(); - if(ticker.contains("[0-9]") || - ticker.length() > 5 || - ticker.length() < 2 || - ticker.contains("NONE")) { + if (ticker.contains("[0-9]") || + ticker.length() > 5 || + ticker.length() < 2 || + ticker.contains("NONE")) { return ""; } return ticker; } - private void organizeOrders() { + private void organizeOrders(Integer id) { //get a list of trades for a person to consider try { PreparedStatement prep; - prep = - conn.prepareStatement("SELECT * FROM \'trades\' WHERE holder_name= ? " - + " AND trade_timestamp BETWEEN ? AND ?" - + "order by trade_timestamp asc;"); - prep.setString(1, this.person); + if (id == -1) { + //search by name + prep = + conn.prepareStatement("SELECT * FROM \'trades\' WHERE holder_name= ? " + + " AND trade_timestamp BETWEEN ? AND ? " + + "order by trade_timestamp asc;"); + prep.setString(1, this.person); + } else { + //search by id + prep = + conn.prepareStatement("SELECT * FROM \'trades\' WHERE holder_id = ? " + + " AND trade_timestamp BETWEEN ? AND ? " + + "order by trade_timestamp asc;"); + prep.setInt(1, id); + } prep.setDate(2, startTime); prep.setDate(3, endTime); ResultSet rs = prep.executeQuery(); @@ -104,7 +120,7 @@ public class ProfitCalculation { while (rs.next()) { String ticker = rs.getString("stock_name"); ticker = validateTicker(ticker); - if(ticker.equals("")){ + if (ticker.equals("")) { continue; } int shares = rs.getInt("number_of_shares"); @@ -125,13 +141,12 @@ public class ProfitCalculation { } } else { //ignore sell orders for which we do not have buys for - if (buyHistoryMap.containsKey(ticker)) { - if (sellHistoryMap.containsKey(ticker)) { - sellHistoryMap.get(ticker).addLast(order); - } else { - sellHistoryMap.put(ticker, oneElement); - } + if (sellHistoryMap.containsKey(ticker)) { + sellHistoryMap.get(ticker).addLast(order); + } else { + sellHistoryMap.put(ticker, oneElement); } + } } @@ -150,39 +165,41 @@ public class ProfitCalculation { LinkedList<OrderTuple> sells = sellHistoryMap.get(ticker); LinkedList<OrderTuple> buys = buyHistoryMap.get(ticker); double realizedGain = 0; + if (sells != null && buys != null) { + //process each sell order (unless all buy orders are "drained" + for (OrderTuple sell : sells) { + //stop if buys are empty, stop if buy happened after sell + if (buys.isEmpty()) { + break; + } - //process each sell order (unless all buy orders are "drained" - for (OrderTuple sell : sells) { - //stop if buys are empty, stop if buy happened after sell - if (buys.isEmpty()) { - break; - } - - int sharesToSell = sell.getShares(); - - //sell off through list of buys - while (sharesToSell > 0 && !buys.isEmpty()) { - //dont sell from buys which didn't exist at the time. - if (sell.getDate().after(buys.getFirst().getDate())) { - OrderTuple buyBundle = buys.removeFirst(); - int sharesAtBundlePrice; - //the buy has more shares than we want to sell - if (buyBundle.getShares() > sharesToSell) { - sharesAtBundlePrice = sharesToSell; - sharesToSell = 0; - //add back the holdings that were not sold - buyBundle.setShares(buyBundle.getShares() - sharesAtBundlePrice); - buys.addFirst(buyBundle); + int sharesToSell = sell.getShares(); + + //sell off through list of buys + while (sharesToSell > 0 && !buys.isEmpty()) { + //dont sell from buys which didn't exist at the time. + if (sell.getDate().after(buys.getFirst().getDate()) + || sell.getDate().equals(buys.getFirst().getDate())) { + OrderTuple buyBundle = buys.removeFirst(); + int sharesAtBundlePrice; + //the buy has more shares than we want to sell + if (buyBundle.getShares() > sharesToSell) { + sharesAtBundlePrice = sharesToSell; + sharesToSell = 0; + //add back the holdings that were not sold + buyBundle.setShares(buyBundle.getShares() - sharesAtBundlePrice); + buys.addFirst(buyBundle); + } else { + sharesToSell -= buyBundle.getShares(); + sharesAtBundlePrice = buyBundle.getShares(); + } + realizedGain += sharesAtBundlePrice * (sell.getCost() - buyBundle.getCost()); } else { - sharesToSell -= buyBundle.getShares(); - sharesAtBundlePrice = buyBundle.getShares(); + break; } - realizedGain += sharesAtBundlePrice * (sell.getCost() - buyBundle.getCost()); - } else { - break; - } + } } } @@ -242,12 +259,16 @@ public class ProfitCalculation { if (currentStockPrices.containsKey(ticker)) { return currentStockPrices.get(ticker); } else { - String PRICE_URL = BASE_URL + "/last/stocks/" + ticker; - System.out.println("LOG: Making call to " + PRICE_URL + " in " + getClass()); + SimpleDateFormat localDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + String url = "https://data.alpaca.markets/v1/bars/" + + "day?" + + "symbols=" + ticker + + "&start=" + localDateFormat.format(startTime) + + "&end=" + localDateFormat.format(endTime); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(PRICE_URL)).setHeader("APCA-API-KEY-ID", API_KEY) + .uri(URI.create(url)).setHeader("APCA-API-KEY-ID", API_KEY) .setHeader("APCA-API-SECRET-KEY", SECRET_KEY) .build(); @@ -255,18 +276,15 @@ public class ProfitCalculation { try { response = client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); + } catch (Exception e) { + System.out.println("ERROR: error getting price for profit calculation"); } - - JSONObject object = new JSONObject(response.body()); + JSONArray object = new JSONObject(response.body()).getJSONArray(ticker); try { - double price = object.getJSONObject("last").getDouble("price"); - currentStockPrices.put(ticker, price); - return price; + double endPrice = object.getJSONObject(object.length() - 1).getDouble("c"); + currentStockPrices.put(ticker, endPrice); + return endPrice; } catch (JSONException e) { currentStockPrices.put(ticker, -1.0); return -1.0; @@ -276,29 +294,37 @@ public class ProfitCalculation { } - public double calculateGains() { + public double calculateGainsSingle(Integer id) { + if (!tablesFilled) { - organizeOrders(); + organizeOrders(id); getRealizedGains(); getUnrealizedGains(); tablesFilled = true; } - double realizedGains = 0; - double unrealizedGains = 0; + + + double gains = 0; for (double value : realizedGainsMap.values()) { - realizedGains += value; + gains += value; } - for (double value : unrealizedGainsMap.values()) { - unrealizedGains += value; + for (double value: unrealizedGainsMap.values()) { + gains += value; } - return unrealizedGains + realizedGains; + + return gains; } - public List<StockHolding> getHoldingsList() { + public List<StockHolding> getHoldingsList(Integer id) { + if (conn == null) { + System.out.println("ERROR: No database connection"); + return new LinkedList<>(); + } + if (!tablesFilled) { - organizeOrders(); + organizeOrders(id); getRealizedGains(); getUnrealizedGains(); tablesFilled = true; @@ -329,11 +355,12 @@ public class ProfitCalculation { * return percent change in SPY (SP 500) over the time period. */ public double compareToSP500() { + SimpleDateFormat localDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); String url = "https://data.alpaca.markets/v1/bars/" + "day?" + "symbols=SPY" - + "&start=" + startTime - + "&end=" + endTime; + + "&start=" + localDateFormat.format(startTime) + + "&end=" + localDateFormat.format(endTime); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() @@ -369,29 +396,199 @@ public class ProfitCalculation { */ public Map<Integer, Double> getProfitMap() { Map<Integer, Double> profitMap = new HashMap<>(); + if (conn == null) { + System.out.println("ERROR: no database connection"); + return profitMap; + } try { PreparedStatement prep; + long START = System.currentTimeMillis(); prep = - conn.prepareStatement("SELECT * from trades group by holder_name;"); + conn.prepareStatement( + "SELECT * From trades GROUP BY holder_id having max(is_buy) = 1;"); ResultSet rs = prep.executeQuery(); + + long QUERY = System.currentTimeMillis(); + //System.out.println((QUERY - START) + " query time"); + + //set of all people who have made both buy and sell orders + Set<Integer> people = new HashSet<>(); + while (rs.next()) { - int id = rs.getInt("holder_id"); - this.person = rs.getString("holder_name"); - resetClass(); - double gain = this.calculateGains(); - if (moneyInput == 0) { - profitMap.put(id, 0.0); - } else { - profitMap.put(id, gain / moneyInput); - } +// int id = rs.getInt("holder_id"); +// this.person = rs.getString("holder_name"); +// resetClass(); +// +// +// +// double gain = this.calculateGains(); +// if (moneyInput == 0) { +// profitMap.put(id, 0.0); +// } else { +// profitMap.put(id, gain / moneyInput); +// } + people.add(rs.getInt("holder_id")); } + + profitMap = calculateGains(people); + + long LOOP = System.currentTimeMillis(); + //System.out.println((LOOP - QUERY) + " loop"); + } catch (SQLException throwables) { System.out.println("ERROR: SQl error in profit calculation"); } return profitMap; } + private Map<Integer, Double> calculateGains(Set<Integer> people) { + Map<Integer, Double> gainsMap = new HashMap<>(); + + //map of stock to list of buy orders, first element in list is oldest + Map<Integer, Map<String, LinkedList<OrderTuple>>> sellMap = new HashMap<>(); + //map of stock to list of buy orders, first element in list is oldest + Map<Integer, Map<String, LinkedList<OrderTuple>>> buyMap = new HashMap<>(); + //money input + Map<Integer, Double> moneyInMap = new HashMap<>(); + + + try { + PreparedStatement prep; + prep = + conn.prepareStatement("SELECT * FROM \'trades\'" + + " WHERE NOT number_of_shares = 0 AND trade_timestamp BETWEEN ? AND ? " + + "order by trade_timestamp asc;"); + prep.setDate(1, startTime); + prep.setDate(2, endTime); + ResultSet rs = prep.executeQuery(); + while (rs.next()) { + if (people.contains(rs.getInt("holder_id"))) { + String ticker = rs.getString("stock_name"); + ticker = validateTicker(ticker); + if (ticker.equals("")) { + continue; + } + int shares = rs.getInt("number_of_shares"); + double price = rs.getDouble("share_price"); + int holder_id = rs.getInt("holder_id"); + if (!buyMap.containsKey(holder_id)) { + buyMap.put(holder_id, new HashMap<>()); + } + if (!sellMap.containsKey(holder_id)) { + sellMap.put(holder_id, new HashMap<>()); + } + + + OrderTuple order = new OrderTuple(shares, price, rs.getDate("trade_timestamp")); + + //one element list for first time ticker is seen. + LinkedList<OrderTuple> oneElement = new LinkedList<OrderTuple>(); + oneElement.addLast(order); + + //for buy orders, build up buy history + if (rs.getInt("is_buy") != 0) { + + if (moneyInMap.containsKey(holder_id)) { + moneyInMap.put(holder_id, moneyInMap.get(holder_id) + shares * price); + } else { + moneyInMap.put(holder_id, shares * price); + } + + + if (buyMap.get(holder_id).containsKey(ticker)) { + buyMap.get(holder_id).get(ticker).addLast(order); + } else { + buyMap.get(holder_id).put(ticker, oneElement); + } + } else { + //ignore sell orders for which we do not have buys for + if (sellMap.get(holder_id).containsKey(ticker)) { + sellMap.get(holder_id).get(ticker).addLast(order); + } else { + sellMap.get(holder_id).put(ticker, oneElement); + } + + } + } + } + } catch (SQLException e) { + System.out.println("ERROR: sql error getting trades"); + } + + + //part 2 doing math... + for (Integer person : people) { + this.buyHistoryMap = buyMap.get(person); + this.sellHistoryMap = sellMap.get(person); + if (sellHistoryMap == null) { + continue; + } + + for (String ticker : sellHistoryMap.keySet()) { + //use FIFO selling + LinkedList<OrderTuple> sells = sellHistoryMap.get(ticker); + LinkedList<OrderTuple> buys = buyHistoryMap.get(ticker); + double realizedGain = 0; + if (sells != null && buys != null) { + //process each sell order (unless all buy orders are "drained" + for (OrderTuple sell : sells) { + //stop if buys are empty, stop if buy happened after sell + if (buys.isEmpty()) { + break; + } + + int sharesToSell = sell.getShares(); + + //sell off through list of buys + while (sharesToSell > 0 && !buys.isEmpty()) { + //dont sell from buys which didn't exist at the time. + if (sell.getDate().after(buys.getFirst().getDate()) + || sell.getDate().equals(buys.getFirst().getDate())) { + OrderTuple buyBundle = buys.removeFirst(); + int sharesAtBundlePrice; + //the buy has more shares than we want to sell + if (buyBundle.getShares() > sharesToSell) { + sharesAtBundlePrice = sharesToSell; + sharesToSell = 0; + //add back the holdings that were not sold + buyBundle.setShares(buyBundle.getShares() - sharesAtBundlePrice); + buys.addFirst(buyBundle); + } else { + sharesToSell -= buyBundle.getShares(); + sharesAtBundlePrice = buyBundle.getShares(); + } + realizedGain += sharesAtBundlePrice * (sell.getCost() - buyBundle.getCost()); + } else { + break; + } + + + } + } + } + + if (gainsMap.containsKey(person)) { + gainsMap.put(person, gainsMap.get(person) + realizedGain); + } else { + gainsMap.put(person, realizedGain); + } + + } + + //percent gain + if (gainsMap.containsKey(person) && moneyInMap.containsKey(person)) { + gainsMap.put(person, gainsMap.get(person) / moneyInMap.get(person)); + } else { + gainsMap.put(person, 0.0); + } + + + } + + return gainsMap; + } + public double getMoneyInput() { return this.moneyInput; } @@ -406,15 +603,4 @@ public class ProfitCalculation { tablesFilled = false; } - public void setConnection(String filename) throws SQLException, ClassNotFoundException { - - // Initialize the database connection, turn foreign keys on - Class.forName("org.sqlite.JDBC"); - String urlToDB = "jdbc:sqlite:" + filename; - conn = DriverManager.getConnection(urlToDB); - - Statement stat = conn.createStatement(); - stat.executeUpdate("PRAGMA foreign_keys=ON;"); - } - } diff --git a/src/main/java/edu/brown/cs/student/term/profit/StockHolding.java b/src/main/java/edu/brown/cs/student/term/profit/StockHolding.java index f7924f2..dd57ce1 100644 --- a/src/main/java/edu/brown/cs/student/term/profit/StockHolding.java +++ b/src/main/java/edu/brown/cs/student/term/profit/StockHolding.java @@ -1,11 +1,23 @@ package edu.brown.cs.student.term.profit; +import java.util.Objects; + +/** + * class to map holding info for JSON. + */ public class StockHolding { private String ticker; private Double realizedGain; private Double unrealizedGain; private int shares; + /** + * constructor. + * @param ticker - stock. + * @param realizedGain realized gain. + * @param unrealizedGain unrealized gain. + * @param shares - number of shares + */ public StockHolding(String ticker, Double realizedGain, Double unrealizedGain, int shares) { this.ticker = ticker; this.realizedGain = realizedGain; @@ -13,11 +25,60 @@ public class StockHolding { this.shares = shares; } + /** + * getter method. + * @return realized gain. + */ public Double getRealizedGain() { return realizedGain; } + /** + * getter method. + * @return unrealized gain. + */ public Double getUnrealizedGain() { return unrealizedGain; } + + /** + * getter method for testing. + * @return shares. + */ + public int getShares() { + return shares; + } + + public String getTicker() { + return ticker; + } + + @Override + public String toString() { + return "StockHolding{" + + "ticker='" + ticker + '\'' + + ", realizedGain=" + realizedGain + + ", unrealizedGain=" + unrealizedGain + + ", shares=" + shares + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StockHolding that = (StockHolding) o; + return shares == that.shares && Objects.equals(ticker, that.ticker) && + Objects.equals(realizedGain, that.realizedGain) && + Objects.equals(unrealizedGain, that.unrealizedGain); + } + + @Override + public int hashCode() { + return Objects.hash(ticker, realizedGain, unrealizedGain, shares); + } }
\ No newline at end of file diff --git a/src/main/java/edu/brown/cs/student/term/repl/commands/LoadCommand.java b/src/main/java/edu/brown/cs/student/term/repl/commands/LoadCommand.java index 00ba3ad..6f8f610 100644 --- a/src/main/java/edu/brown/cs/student/term/repl/commands/LoadCommand.java +++ b/src/main/java/edu/brown/cs/student/term/repl/commands/LoadCommand.java @@ -125,7 +125,7 @@ public class LoadCommand implements Command { "text=form-type%3D4+and+(filing-date%3D" + filingDate + ")" + "&start=" + (100*counter++ + shift) + "&count=" + 100 + - "&first=2020" + + "&first=2021" + "&last=2021" + "&output=atom" : diff --git a/src/main/java/edu/brown/cs/student/term/trade/Trade.java b/src/main/java/edu/brown/cs/student/term/trade/Trade.java index 353de8d..df52a4f 100644 --- a/src/main/java/edu/brown/cs/student/term/trade/Trade.java +++ b/src/main/java/edu/brown/cs/student/term/trade/Trade.java @@ -2,6 +2,8 @@ package edu.brown.cs.student.term.trade; import edu.brown.cs.student.term.hub.Holder; +import java.util.Objects; + public class Trade { private int id; @@ -54,6 +56,26 @@ public class Trade { return price; } + /** + * This equals method differs from what may be expected, + * it considers trades "equal" if they have the same buy value and stock name + * because they are the same type of trade of the same stock in that case + * @param o - object to compare to + * @return true if equal by the considerations above + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Trade trade = (Trade) o; + return isBuy == trade.isBuy && stock.equals(trade.stock); + } + + @Override + public int hashCode() { + return Objects.hash(stock, isBuy); + } + @Override public String toString() { return "Trade{" + diff --git a/src/test/java/edu/brown/cs/student/ProfitCalculationTest.java b/src/test/java/edu/brown/cs/student/ProfitCalculationTest.java index 256afed..fcb0de4 100644 --- a/src/test/java/edu/brown/cs/student/ProfitCalculationTest.java +++ b/src/test/java/edu/brown/cs/student/ProfitCalculationTest.java @@ -12,6 +12,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.sql.Date; import java.time.Instant; @@ -35,7 +38,7 @@ public class ProfitCalculationTest { @Before public void setUp() { try { - db = new DatabaseQuerier("data/testing/test_trades.sqlite3"); + db = new DatabaseQuerier("data/profit_testing.sqlite3"); } catch (Exception e) { System.out.println("DBQuerier Test, couldn't connect to db???"); } @@ -47,16 +50,95 @@ public class ProfitCalculationTest { } @Test - public void testEmptyDB() { + public void testBasicTrades() { setUp(); ProfitCalculation profitCalculation = - new ProfitCalculation(DatabaseQuerier.getConn(), "CAKEBREAD STEVEN", new Date(1518010558000l), - new Date(1718010556000l)); - List<StockHolding> trade = profitCalculation.getHoldingsList(); - double gain = trade.get(0).getUnrealizedGain(); - assertEquals(294800.0, gain, .01); + new ProfitCalculation(DatabaseQuerier.getConn(), "Don", new Date(1518010558000l), + new Date(1618698807000l)); + //price of GME at end time is 154.69 + List<StockHolding> trade = profitCalculation.getHoldingsList(-1); + //buy with no sell + assertEquals(trade.get(0).getUnrealizedGain(), 3842.25, .25); + assertEquals(trade.get(0).getRealizedGain(), 0, .01); + + //just sell + profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "SELL", new Date(1518010558000l), + new Date(1618698807000l)); + trade = profitCalculation.getHoldingsList(-1); + assertTrue(trade.isEmpty()); + assertEquals(profitCalculation.calculateGainsSingle(-1), 0, 0.001); + + tearDown(); } + @Test + public void otherBuySellCases() { + setUp(); + //buy and sell at same timestamp + ProfitCalculation profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "concurrentBS", new Date(1518010558000l), + new Date(1715629591000l)); + + Map<Integer, Double> map = profitCalculation.getProfitMap(); + + assertEquals(map.get(100), 1, .01); + //buys at multiple prices + profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "mulitpleBuyPrices", + new Date(1518010558000l), + new Date(1715629591000l)); + + assertEquals(profitCalculation.getHoldingsList(-1).get(0).getRealizedGain(), 3750, 0.01); + assertEquals(profitCalculation.getMoneyInput(), 3750, .01); + + //left over holdings + profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "dontSellAll", + new Date(1518010558000l), + new Date(1715629591000l)); + + assertEquals(profitCalculation.getHoldingsList(-1).get(0).getShares(), 25, .01); + tearDown(); + } + + @Test + public void testAPICalls() { + ProfitCalculation profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "Don", new Date(1618234200000l), + new Date(1618703800000l)); + + //check sp500 calculation. 411.28 to 417.30 + assertEquals(profitCalculation.compareToSP500(), .01464, .001); + + tearDown(); + + } + + @Test + public void databaseAndConnectionIssues() { + //no database connection + ProfitCalculation profitCalculation = + new ProfitCalculation(null, "Don", new Date(1518010558000l), + new Date(1618698807000l)); + assertEquals(profitCalculation.getProfitMap(), new HashMap<>()); + + assertEquals(profitCalculation.getHoldingsList(-1), new LinkedList<>()); + + setUp(); + //invalid person + profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "1234", new Date(1518010558000l), + new Date(1618698807000l)); + assertEquals(profitCalculation.getHoldingsList(-1), new LinkedList<>()); + + + //invalid stock ticker + profitCalculation = + new ProfitCalculation(DatabaseQuerier.getConn(), "invalidTicker", new Date(1518010558000l), + new Date(1618698807000l)); + assertTrue(profitCalculation.getHoldingsList(-1).isEmpty()); + } }
\ No newline at end of file @@ -0,0 +1,52 @@ +[1mdiff --git a/data/mock_tradeClarks.sqlite3 b/data/mock_tradeClarks.sqlite3[m +[1mindex 980c539..e816ee8 100644[m +Binary files a/data/mock_tradeClarks.sqlite3 and b/data/mock_tradeClarks.sqlite3 differ +[1mdiff --git a/src/main/java/edu/brown/cs/student/term/Main.java b/src/main/java/edu/brown/cs/student/term/Main.java[m +[1mindex 31bf7a3..c4a7814 100644[m +[1m--- a/src/main/java/edu/brown/cs/student/term/Main.java[m +[1m+++ b/src/main/java/edu/brown/cs/student/term/Main.java[m +[36m@@ -47,12 +47,21 @@[m [mpublic final class Main {[m + //do a gui type thing[m + //runSparkServer((int) options.valueOf("port"));[m + }[m +[31m- [m +[31m- HashMap<String, Command> commandHashMap = new HashMap<>();[m +[31m- commandHashMap.put("setup", new SetupCommand());[m +[31m- /** add commands to map here! */[m +[31m- REPL repl = new REPL(commandHashMap);[m +[31m- repl.runREPL();[m +[32m+[m +[32m+[m[32m ProfitCalculation person = new ProfitCalculation(null, "Vincent", new Date(1515629591000L), new Date(1715507898000L));[m +[32m+[m[32m try {[m +[32m+[m[32m person.setConnection("./data/mock_tradeClarks.sqlite3");[m +[32m+[m[32m } catch(Exception e) {[m +[32m+[m[32m e.printStackTrace();[m +[32m+[m[32m }[m +[32m+[m +[32m+[m[32m person.calculateGains();[m +[32m+[m +[32m+[m[32m// HashMap<String, Command> commandHashMap = new HashMap<>();[m +[32m+[m[32m// commandHashMap.put("setup", new SetupCommand());[m +[32m+[m[32m// /** add commands to map here! */[m +[32m+[m[32m// REPL repl = new REPL(commandHashMap);[m +[32m+[m[32m// repl.runREPL();[m + }[m + [m + [m +[1mdiff --git a/src/main/java/edu/brown/cs/student/term/ProfitCalculation.java b/src/main/java/edu/brown/cs/student/term/ProfitCalculation.java[m +[1mindex aa1bc09..94f87f7 100644[m +[1m--- a/src/main/java/edu/brown/cs/student/term/ProfitCalculation.java[m +[1m+++ b/src/main/java/edu/brown/cs/student/term/ProfitCalculation.java[m +[36m@@ -265,10 +265,10 @@[m [mpublic class ProfitCalculation {[m + double totalGains = unrealizedGains + realizedGains;[m + [m + System.out.println("Money In: " + moneyInput);[m +[32m+[m[32m System.out.println("SP500 on money In: " + (moneyInput * compareToSP500()));[m + System.out.println("Money Out: " + (moneyInput + totalGains));[m +[31m- System.out.println("NASDAQ on money In: " + (moneyInput * compareToSP500()));[m + System.out.println([m +[31m- "Total: " + totalGains + "| unrealized: " + unrealizedGains + " | realized: " +[m +[32m+[m[32m "Total gain: " + totalGains + "| unrealized: " + unrealizedGains + " | realized: " +[m + realizedGains);[m + }[m + [m |