Fixed Stock Market UI issues. Added warnings for price movements

This commit is contained in:
danielyxie
2019-04-27 17:43:38 -07:00
committed by danielyxie
parent d7fb335815
commit 67632ced09
10 changed files with 315 additions and 96 deletions

View File

@ -7,13 +7,27 @@
p {
font-size: $defaultFontSize * 0.8125;
}
a {
font-size: $defaultFontSize * 0.875;
}
h2 {
}
.stock-market-info-and-purchases {
> h2 {
display: block;
margin-top: 10px;
margin-left: 10px;
}
> p {
display: block;
margin-left: 10px;
width: 70%;
}
> a, > button {
margin: 10px;
}
}
@ -27,19 +41,10 @@
}
}
#stock-market-container p {
padding: 6px;
margin: 6px;
width: 70%;
}
#stock-market-container a {
margin: 10px;
}
#stock-market-watchlist-filter {
margin: 5px 5px 5px 10px;
padding: 4px;
width: 50%;
margin-left: 10px;
}
.stock-market-input {
@ -51,13 +56,25 @@
color: var(--my-font-color);
}
.stock-market-price-movement-warning {
border: 1px solid white;
color: red;
margin: 2px;
padding: 2px;
}
.stock-market-position-text {
color: #fff;
display: inline-block;
display: block;
p {
color: #fff;
display: block;
display: inline-block;
margin: 4px;
}
h3 {
margin: 4px;
}
}

View File

@ -1,14 +1,12 @@
import {
Order,
OrderTypes,
PositionTypes
} from "./Order";
import { Order } from "./Order";
import { Stock } from "./Stock";
import {
getStockMarket4SDataCost,
getStockMarket4STixApiCost
} from "./StockMarketCosts";
import { InitStockMetadata } from "./data/InitStockMetadata";
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
import { StockSymbols } from "./data/StockSymbols";
import { StockMarketRoot } from "./ui/Root";
@ -23,7 +21,7 @@ import { dialogBoxCreate } from "../../utils/DialogBox";
import { Reviver } from "../../utils/JSONReviver";
import React from "react";
import ReactDOm from "react-dom";
import ReactDOM from "react-dom";
export function placeOrder(stock, shares, price, type, position, workerScript=null) {
var tixApi = (workerScript instanceof WorkerScript);
@ -60,7 +58,7 @@ export function cancelOrder(params, workerScript=null) {
if (StockMarket["Orders"] == null) {return false;}
if (params.order && params.order instanceof Order) {
var order = params.order;
//An 'Order' object is passed in
// An 'Order' object is passed in
var stockOrders = StockMarket["Orders"][order.stock.symbol];
for (var i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
@ -72,7 +70,7 @@ export function cancelOrder(params, workerScript=null) {
return false;
} else if (params.stock && params.shares && params.price && params.type &&
params.pos && params.stock instanceof Stock) {
//Order properties are passed in. Need to look for the order
// Order properties are passed in. Need to look for the order
var stockOrders = StockMarket["Orders"][params.stock.symbol];
var orderTxt = params.stock.symbol + " - " + params.shares + " @ " +
numeralWrapper.format(params.price, '$0.000a');
@ -124,7 +122,7 @@ function executeOrder(order) {
break;
}
if (res) {
//Remove order from order book
// Remove order from order book
for (var i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
stockOrders.splice(i, 1);
@ -509,14 +507,12 @@ export function processStockPrices(numCycles=1) {
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Long);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Short);
displayStockMarketContent();
} else {
stock.price /= (1 + av);
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Short);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Short);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Long);
displayStockMarketContent();
}
var otlkMagChange = stock.otlkMag * av;
@ -533,9 +529,10 @@ export function processStockPrices(numCycles=1) {
stock.otlkMag *= -1;
stock.b = !stock.b;
}
}
}
displayStockMarketContent();
}
//Checks and triggers any orders for the specified stock

View File

@ -13,6 +13,10 @@ import { CONSTANTS } from "../Constants";
export function getBuyTransactionCost(stock: Stock, shares: number, posType: PositionTypes): number | null {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return null; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
// If the number of shares doesn't trigger a price movement, its a simple calculation
@ -58,6 +62,10 @@ export function getBuyTransactionCost(stock: Stock, shares: number, posType: Pos
export function getSellTransactionGain(stock: Stock, shares: number, posType: PositionTypes): number | null {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return null; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
// If the number of shares doesn't trigger a price mvoement, its a simple calculation

View File

@ -39,6 +39,16 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
this.purchase4SMarketDataTixApiAccess = this.purchase4SMarketDataTixApiAccess.bind(this);
}
shouldComponentUpdate(nextProps: IProps) {
// This only need to rerender if the player has purchased something new
if (this.props.p.hasWseAccount !== nextProps.p.hasWseAccount) { return true; }
if (this.props.p.hasTixApiAccess !== nextProps.p.hasTixApiAccess) { return true; }
if (this.props.p.has4SData !== nextProps.p.has4SData) { return true; }
if (this.props.p.has4SDataTixApi !== nextProps.p.has4SDataTixApi) { return true; }
return false;
}
handleClick4SMarketDataHelpTip() {
dialogBoxCreate(
"Access to the 4S Market Data feed will display two additional pieces " +
@ -179,15 +189,19 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
render() {
const documentationLink = "https://bitburner.readthedocs.io/en/latest/basicgameplay/stockmarket.html";
return (
<div>
<p>Welcome to the World Stock Exchange (WSE)!</p><br /><br />
<div className={"stock-market-info-and-purchases"}>
<p>Welcome to the World Stock Exchange (WSE)!</p>
<button className={"std-button"}>
<a href={documentationLink} target={"_blank"}>
Investopedia
</a>
</button>
<br />
<p>
To begin trading, you must first purchase an account.
To begin trading, you must first purchase an account:
</p>
{this.renderPurchaseWseAccountButton()}
<a className={"std-button"} href={documentationLink} target={"_blank"}>
Investopedia
</a>
<h2>Trade Information eXchange (TIX) API</h2>
<p>
TIX, short for Trade Information eXchange, is the communications protocol
@ -208,7 +222,7 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
<p>
Commission Fees: Every transaction you make has
a {numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} commission fee.
</p>
</p><br />
<p>
WARNING: When you reset after installing Augmentations, the Stock
Market is reset. You will retain your WSE Account, access to the

View File

@ -10,12 +10,18 @@ import { StockTickerTxButton } from "./StockTickerTxButton";
import { Order } from "../Order";
import { Stock } from "../Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
} from "../StockMarketHelpers";
import { OrderTypes } from "../data/OrderTypes";
import { PositionTypes } from "../data/PositionTypes";
import { CONSTANTS } from "../../Constants";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Accordion } from "../../ui/React/Accordion";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import {
@ -63,8 +69,11 @@ export class StockTicker extends React.Component<IProps, IState> {
qty: "",
}
this.getBuyTransactionCostText = this.getBuyTransactionCostText.bind(this);
this.getSellTransactionCostText = this.getSellTransactionCostText.bind(this);
this.handleBuyButtonClick = this.handleBuyButtonClick.bind(this);
this.handleBuyMaxButtonClick = this.handleBuyMaxButtonClick.bind(this);
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.handleOrderTypeChange = this.handleOrderTypeChange.bind(this);
this.handlePositionTypeChange = this.handlePositionTypeChange.bind(this);
this.handleQuantityChange = this.handleQuantityChange.bind(this);
@ -96,8 +105,46 @@ export class StockTicker extends React.Component<IProps, IState> {
yesNoTxtInpBoxCreate(popupTxt);
}
getBuyTransactionCostText(): string {
const stock = this.props.stock;
const qty: number = this.getQuantity();
if (isNaN(qty)) { return ""; }
const cost = getBuyTransactionCost(this.props.stock, qty, this.state.position);
if (cost == null) { return ""; }
let costTxt = `Purchasing ${numeralWrapper.formatBigNumber(qty)} shares will cost ${numeralWrapper.formatMoney(cost)}. `;
const causesMovement = qty > stock.shareTxUntilMovement;
if (causesMovement) {
costTxt += `WARNING: Purchasing this many shares will influence the stock price`;
}
return costTxt;
}
getQuantity(): number {
return Math.round(parseFloat(this.state.qty));
}
getSellTransactionCostText(): string {
const stock = this.props.stock;
const qty: number = this.getQuantity();
if (isNaN(qty)) { return ""; }
const cost = getSellTransactionGain(this.props.stock, qty, this.state.position);
if (cost == null) { return ""; }
let costTxt = `Selling ${numeralWrapper.formatBigNumber(qty)} shares will result in a gain of ${numeralWrapper.formatMoney(cost)}. `;
const causesMovement = qty > stock.shareTxUntilMovement;
if (causesMovement) {
costTxt += `WARNING: Selling this many shares will influence the stock price`;
}
return costTxt;
}
handleBuyButtonClick() {
const shares = parseInt(this.state.qty);
const shares = this.getQuantity();
if (isNaN(shares)) {
dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`);
return;
@ -178,6 +225,18 @@ export class StockTicker extends React.Component<IProps, IState> {
}
}
handleHeaderClick(e: React.MouseEvent<HTMLButtonElement>) {
const elem = e.currentTarget;
elem.classList.toggle("active");
const panel: HTMLElement = elem.nextElementSibling as HTMLElement;
if (panel!.style.display === "block") {
panel!.style.display = "none";
} else {
panel.style.display = "block";
}
}
handleOrderTypeChange(e: React.ChangeEvent<HTMLSelectElement>) {
const val = e.target.value;
@ -224,7 +283,7 @@ export class StockTicker extends React.Component<IProps, IState> {
}
handleSellButtonClick() {
const shares = parseInt(this.state.qty);
const shares = this.getQuantity();
if (isNaN(shares)) {
dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`);
return;
@ -294,49 +353,68 @@ export class StockTicker extends React.Component<IProps, IState> {
}
render() {
// Determine if the player's intended transaction will cause a price movement
let causesMovement: boolean = false;
const qty = this.getQuantity();
if (!isNaN(qty)) {
causesMovement = qty > this.props.stock.shareTxUntilMovement;
}
return (
<li>
<button className={"accordion-header"}>
<StockTickerHeaderText p={this.props.p} stock={this.props.stock} />
</button>
<div className={"accordion-panel"}>
<input
className={"stock-market-input"}
onChange={this.handleQuantityChange}
placeholder={"Quantity (Shares)"}
value={this.state.qty}
/>
<select className={"stock-market-input dropdown"} onChange={this.handlePositionTypeChange} value={this.state.position}>
<option value={"Long"}>Long</option>
{
this.hasShortAccess() &&
<option value={"Short"}>Short</option>
}
</select>
<select className={"stock-market-input dropdown"} onChange={this.handleOrderTypeChange} value={this.state.orderType}>
<option value={SelectorOrderType.Market}>{SelectorOrderType.Market}</option>
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Limit}>{SelectorOrderType.Limit}</option>
}
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Stop}>{SelectorOrderType.Stop}</option>
}
</select>
<Accordion
headerContent={
<StockTickerHeaderText p={this.props.p} stock={this.props.stock} />
}
panelContent={
<div>
<input
className={"stock-market-input"}
onChange={this.handleQuantityChange}
placeholder={"Quantity (Shares)"}
value={this.state.qty}
/>
<select className={"stock-market-input dropdown"} onChange={this.handlePositionTypeChange} value={this.state.position}>
<option value={"Long"}>Long</option>
{
this.hasShortAccess() &&
<option value={"Short"}>Short</option>
}
</select>
<select className={"stock-market-input dropdown"} onChange={this.handleOrderTypeChange} value={this.state.orderType}>
<option value={SelectorOrderType.Market}>{SelectorOrderType.Market}</option>
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Limit}>{SelectorOrderType.Limit}</option>
}
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Stop}>{SelectorOrderType.Stop}</option>
}
</select>
<StockTickerTxButton onClick={this.handleBuyButtonClick} text={"Buy"} />
<StockTickerTxButton onClick={this.handleSellButtonClick} text={"Sell"} />
<StockTickerTxButton onClick={this.handleBuyMaxButtonClick} text={"Buy MAX"} />
<StockTickerTxButton onClick={this.handleSellAllButtonClick} text={"Sell ALL"} />
<StockTickerPositionText p={this.props.p} stock={this.props.stock} />
<StockTickerOrderList
cancelOrder={this.props.cancelOrder}
orders={this.props.orders}
p={this.props.p}
stock={this.props.stock}
/>
</div>
<StockTickerTxButton onClick={this.handleBuyButtonClick} text={"Buy"} tooltip={this.getBuyTransactionCostText()} />
<StockTickerTxButton onClick={this.handleSellButtonClick} text={"Sell"} tooltip={this.getSellTransactionCostText()} />
<StockTickerTxButton onClick={this.handleBuyMaxButtonClick} text={"Buy MAX"} />
<StockTickerTxButton onClick={this.handleSellAllButtonClick} text={"Sell ALL"} />
{
causesMovement &&
<p className="stock-market-price-movement-warning">
WARNING: Buying/Selling {numeralWrapper.formatBigNumber(qty)} shares will affect
the stock's price. This applies during the transaction itself as well. See Investopedia
for more details.
</p>
}
<StockTickerPositionText p={this.props.p} stock={this.props.stock} />
<StockTickerOrderList
cancelOrder={this.props.cancelOrder}
orders={this.props.orders}
p={this.props.p}
stock={this.props.stock}
/>
</div>
}
/>
</li>
)
}

View File

@ -37,18 +37,16 @@ export class StockTickerPositionText extends React.Component<IProps, any> {
Shares in the long position will increase in value if the price
of the corresponding stock increases
</span>
</h3>
</h3><br />
<p>
Shares: {numeralWrapper.format(stock.playerShares, "0,0")}
</p>
</p><br />
<p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgPx)}
(Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgPx)} (Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p><br />
<p>
Profit: {numeralWrapper.formatMoney(gains)}
({numeralWrapper.formatPercentage(percentageGains)})
</p>
Profit: {numeralWrapper.formatMoney(gains)} ({numeralWrapper.formatPercentage(percentageGains)})
</p><br />
</div>
)
}
@ -71,18 +69,16 @@ export class StockTickerPositionText extends React.Component<IProps, any> {
Shares in the short position will increase in value if the
price of the corresponding stock decreases
</span>
</h3>
</h3><br />
<p>
Shares: {numeralWrapper.format(stock.playerShortShares, "0,0")}
</p>
</p><br />
<p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgShortPx)}
(Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgShortPx)} (Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p><br />
<p>
Profit: {numeralWrapper.formatMoney(gains)}
({numeralWrapper.formatPercentage(percentageGains)})
</p>
Profit: {numeralWrapper.formatMoney(gains)} ({numeralWrapper.formatPercentage(percentageGains)})
</p><br />
</div>
)
} else {
@ -96,15 +92,16 @@ export class StockTickerPositionText extends React.Component<IProps, any> {
return (
<div className={"stock-market-position-text"}>
<p style={blockStyleMarkup}>
Max Shares: ${numeralWrapper.formatMoney(stock.maxShares)}
Max Shares: {numeralWrapper.formatBigNumber(stock.maxShares)}
</p>
<p className={"tooltip"} >
Ask Price: {numeralWrapper.formatMoney(stock.getAskPrice())}
<span className={"tooltiptext"}>
See Investopedia for details on what this is
</span>
</p>
</p><br />
<p className={"tooltip"} >
Bid Price: {numeralWrapper.formatMoney(stock.getBidPrice())}
<span className={"tooltiptext"}>
See Investopedia for details on what this is
</span>

View File

@ -7,12 +7,35 @@ import * as React from "react";
type IProps = {
onClick: () => void;
text: string;
tooltip?: string;
}
type IInnerHTMLMarkup = {
__html: string;
}
export function StockTickerTxButton(props: IProps): React.ReactElement {
let className = "stock-market-input std-button";
const hasTooltip = (typeof props.tooltip === "string" && props.tooltip !== "");
if (hasTooltip) {
className += " tooltip";
}
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: props.tooltip!
}
}
return (
<button className={"stock-market-input std-button"} onClick={props.onClick}>
<button className={className} onClick={props.onClick}>
{props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

View File

@ -75,6 +75,10 @@ export class StockTickers extends React.Component<IProps, IState> {
this.setState({
watchlistSymbols: sanitizedWatchlist.split(","),
});
} else {
this.setState({
watchlistSymbols: [],
});
}
}
@ -91,8 +95,9 @@ export class StockTickers extends React.Component<IProps, IState> {
for (const stockMarketProp in this.props.stockMarket) {
const val = this.props.stockMarket[stockMarketProp];
if (val instanceof Stock) {
// Skip if there's a filter and the stock isnt in that filter
if (this.state.watchlistSymbols.length > 0 && !this.state.watchlistSymbols.includes(val.symbol)) {
continue; // Not in watchlist
continue;
}
let orders = this.props.stockMarket.Orders[val.symbol];
@ -100,11 +105,19 @@ export class StockTickers extends React.Component<IProps, IState> {
orders = [];
}
// Skip if we're in portfolio mode and the player doesnt own this or have any active orders
if (this.state.tickerDisplayMode === TickerDisplayMode.Portfolio) {
if (val.playerShares === 0 && val.playerShortShares === 0 && orders.length === 0) {
continue;
}
}
tickers.push(
<StockTicker
buyStockLong={this.props.buyStockLong}
buyStockShort={this.props.buyStockShort}
cancelOrder={this.props.cancelOrder}
key={val.symbol}
orders={orders}
p={this.props.p}
placeOrder={this.props.placeOrder}

View File

@ -82,7 +82,6 @@ import {
} from "./Server/SpecialServerIps";
import {
StockMarket,
StockSymbols,
SymbolToStockMap,
initSymbolToStockMap,
stockMarketCycle,

View File

@ -0,0 +1,73 @@
/**
* React component to create an accordion element
*/
import * as React from "react";
type IProps = {
headerContent: React.ReactElement;
panelContent: React.ReactElement;
}
type IState = {
panelOpened: boolean;
}
export class Accordion extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.state = {
panelOpened: false,
}
}
handleHeaderClick(e: React.MouseEvent<HTMLButtonElement>) {
const elem = e.currentTarget;
elem.classList.toggle("active");
const panel: HTMLElement = elem.nextElementSibling as HTMLElement;
if (panel!.style.display === "block") {
panel!.style.display = "none";
this.setState({
panelOpened: false,
});
} else {
panel!.style.display = "block";
this.setState({
panelOpened: true,
});
}
}
render() {
return (
<div>
<button className={"accordion-header"} onClick={this.handleHeaderClick}>
{this.props.headerContent}
</button>
<AccordionPanel opened={this.state.panelOpened} panelContent={this.props.panelContent} />
</div>
)
}
}
type IPanelProps = {
opened: boolean;
panelContent: React.ReactElement;
}
class AccordionPanel extends React.Component<IPanelProps, any> {
shouldComponentUpdate(nextProps: IPanelProps) {
return this.props.opened || nextProps.opened;
}
render() {
return (
<div className={"accordion-panel"}>
{this.props.panelContent}
</div>
)
}
}