mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 23:08:36 +02:00
343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
/**
|
|
* React Component for a single stock ticker in the Stock Market UI
|
|
*/
|
|
import React, { useState } from "react";
|
|
|
|
import { StockTickerHeaderText } from "./StockTickerHeaderText";
|
|
import { StockTickerOrderList } from "./StockTickerOrderList";
|
|
import { StockTickerPositionText } from "./StockTickerPositionText";
|
|
import { StockTickerTxButton } from "./StockTickerTxButton";
|
|
import { PlaceOrderModal } from "./PlaceOrderModal";
|
|
|
|
import { Order } from "../Order";
|
|
import { Stock } from "../Stock";
|
|
import { getBuyTransactionCost, getSellTransactionGain, calculateBuyMaxAmount } from "../StockMarketHelpers";
|
|
import { OrderTypes } from "../data/OrderTypes";
|
|
import { PositionTypes } from "../data/PositionTypes";
|
|
|
|
import { IPlayer } from "../../PersonObjects/IPlayer";
|
|
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
|
|
import { numeralWrapper } from "../../ui/numeralFormat";
|
|
import { Money } from "../../ui/React/Money";
|
|
|
|
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
|
import Box from "@mui/material/Box";
|
|
import TextField from "@mui/material/TextField";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
|
|
|
import ListItemButton from "@mui/material/ListItemButton";
|
|
import ListItemText from "@mui/material/ListItemText";
|
|
import Paper from "@mui/material/Paper";
|
|
import Collapse from "@mui/material/Collapse";
|
|
import ExpandMore from "@mui/icons-material/ExpandMore";
|
|
import ExpandLess from "@mui/icons-material/ExpandLess";
|
|
|
|
enum SelectorOrderType {
|
|
Market = "Market Order",
|
|
Limit = "Limit Order",
|
|
Stop = "Stop Order",
|
|
}
|
|
|
|
type txFn = (stock: Stock, shares: number) => boolean;
|
|
type placeOrderFn = (
|
|
stock: Stock,
|
|
shares: number,
|
|
price: number,
|
|
ordType: OrderTypes,
|
|
posType: PositionTypes,
|
|
) => boolean;
|
|
|
|
type IProps = {
|
|
buyStockLong: txFn;
|
|
buyStockShort: txFn;
|
|
cancelOrder: (params: any) => void;
|
|
orders: Order[];
|
|
p: IPlayer;
|
|
placeOrder: placeOrderFn;
|
|
rerenderAllTickers: () => void;
|
|
sellStockLong: txFn;
|
|
sellStockShort: txFn;
|
|
stock: Stock;
|
|
};
|
|
|
|
export function StockTicker(props: IProps): React.ReactElement {
|
|
const [orderType, setOrderType] = useState(SelectorOrderType.Market);
|
|
const [position, setPosition] = useState(PositionTypes.Long);
|
|
const [qty, setQty] = useState("");
|
|
const [open, setOpen] = useState(false);
|
|
const [tickerOpen, setTicketOpen] = useState(false);
|
|
|
|
const [modalProps, setModalProps] = useState<{
|
|
text: string;
|
|
placeText: string;
|
|
place: (n: number) => boolean;
|
|
}>({
|
|
text: "",
|
|
placeText: "",
|
|
place: () => false,
|
|
});
|
|
|
|
function getBuyTransactionCostContent(): JSX.Element | null {
|
|
const stock = props.stock;
|
|
const qty: number = getQuantity();
|
|
if (isNaN(qty)) {
|
|
return null;
|
|
}
|
|
|
|
const cost = getBuyTransactionCost(stock, qty, position);
|
|
if (cost == null) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
Purchasing {numeralWrapper.formatShares(qty)} shares ({position === PositionTypes.Long ? "Long" : "Short"}
|
|
) will cost <Money money={cost} />.
|
|
</>
|
|
);
|
|
}
|
|
|
|
function getQuantity(): number {
|
|
return Math.round(parseFloat(qty));
|
|
}
|
|
|
|
function getSellTransactionCostContent(): JSX.Element | null {
|
|
const stock = props.stock;
|
|
const qty: number = getQuantity();
|
|
if (isNaN(qty)) {
|
|
return null;
|
|
}
|
|
|
|
if (position === PositionTypes.Long) {
|
|
if (qty > stock.playerShares) {
|
|
return <>You do not have this many shares in the Long position</>;
|
|
}
|
|
} else if (qty > stock.playerShortShares) {
|
|
return <>You do not have this many shares in the Short position</>;
|
|
}
|
|
|
|
const cost = getSellTransactionGain(stock, qty, position);
|
|
if (cost == null) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
Selling {numeralWrapper.formatShares(qty)} shares ({position === PositionTypes.Long ? "Long" : "Short"}) will
|
|
result in a gain of <Money money={cost} />.
|
|
</>
|
|
);
|
|
}
|
|
|
|
function handleBuyButtonClick(): void {
|
|
const shares = getQuantity();
|
|
if (isNaN(shares)) {
|
|
dialogBoxCreate(`Invalid input for quantity (number of shares): ${qty}`);
|
|
return;
|
|
}
|
|
|
|
switch (orderType) {
|
|
case SelectorOrderType.Market: {
|
|
if (position === PositionTypes.Short) {
|
|
props.buyStockShort(props.stock, shares);
|
|
} else {
|
|
props.buyStockLong(props.stock, shares);
|
|
}
|
|
props.rerenderAllTickers();
|
|
break;
|
|
}
|
|
case SelectorOrderType.Limit: {
|
|
setOpen(true);
|
|
setModalProps({
|
|
text: "Enter the price for your Limit Order",
|
|
placeText: "Place Buy Limit Order",
|
|
place: (price: number) => props.placeOrder(props.stock, shares, price, OrderTypes.LimitBuy, position),
|
|
});
|
|
break;
|
|
}
|
|
case SelectorOrderType.Stop: {
|
|
setOpen(true);
|
|
setModalProps({
|
|
text: "Enter the price for your Stop Order",
|
|
placeText: "Place Buy Stop Order",
|
|
place: (price: number) => props.placeOrder(props.stock, shares, price, OrderTypes.StopBuy, position),
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleBuyMaxButtonClick(): void {
|
|
const playerMoney: number = props.p.money;
|
|
|
|
const stock = props.stock;
|
|
let maxShares = calculateBuyMaxAmount(stock, position, playerMoney);
|
|
maxShares = Math.min(maxShares, Math.round(stock.maxShares - stock.playerShares - stock.playerShortShares));
|
|
|
|
switch (orderType) {
|
|
case SelectorOrderType.Market: {
|
|
if (position === PositionTypes.Short) {
|
|
props.buyStockShort(stock, maxShares);
|
|
} else {
|
|
props.buyStockLong(stock, maxShares);
|
|
}
|
|
props.rerenderAllTickers();
|
|
break;
|
|
}
|
|
default: {
|
|
dialogBoxCreate(`ERROR: 'Buy Max' only works for Market Orders`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleOrderTypeChange(e: SelectChangeEvent<string>): void {
|
|
const val = e.target.value;
|
|
|
|
// The select value returns a string. Afaik TypeScript doesnt make it easy
|
|
// to convert that string back to an enum type so we'll just do this for now
|
|
switch (val) {
|
|
case SelectorOrderType.Limit:
|
|
setOrderType(SelectorOrderType.Limit);
|
|
break;
|
|
case SelectorOrderType.Stop:
|
|
setOrderType(SelectorOrderType.Stop);
|
|
break;
|
|
case SelectorOrderType.Market:
|
|
default:
|
|
setOrderType(SelectorOrderType.Market);
|
|
}
|
|
}
|
|
|
|
function handlePositionTypeChange(e: SelectChangeEvent<string>): void {
|
|
const val = e.target.value;
|
|
|
|
if (val === PositionTypes.Short) {
|
|
setPosition(PositionTypes.Short);
|
|
} else {
|
|
setPosition(PositionTypes.Long);
|
|
}
|
|
}
|
|
|
|
function handleQuantityChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
|
setQty(e.target.value);
|
|
}
|
|
|
|
function handleSellButtonClick(): void {
|
|
const shares = getQuantity();
|
|
if (isNaN(shares)) {
|
|
dialogBoxCreate(`Invalid input for quantity (number of shares): ${qty}`);
|
|
return;
|
|
}
|
|
|
|
switch (orderType) {
|
|
case SelectorOrderType.Market: {
|
|
if (position === PositionTypes.Short) {
|
|
props.sellStockShort(props.stock, shares);
|
|
} else {
|
|
props.sellStockLong(props.stock, shares);
|
|
}
|
|
props.rerenderAllTickers();
|
|
break;
|
|
}
|
|
case SelectorOrderType.Limit: {
|
|
setOpen(true);
|
|
setModalProps({
|
|
text: "Enter the price for your Limit Order",
|
|
placeText: "Place Sell Limit Order",
|
|
place: (price: number) => props.placeOrder(props.stock, shares, price, OrderTypes.LimitSell, position),
|
|
});
|
|
break;
|
|
}
|
|
case SelectorOrderType.Stop: {
|
|
setOpen(true);
|
|
setModalProps({
|
|
text: "Enter the price for your Stop Order",
|
|
placeText: "Place Sell Stop Order",
|
|
place: (price: number) => props.placeOrder(props.stock, shares, price, OrderTypes.StopSell, position),
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleSellAllButtonClick(): void {
|
|
const stock = props.stock;
|
|
|
|
switch (orderType) {
|
|
case SelectorOrderType.Market: {
|
|
if (position === PositionTypes.Short) {
|
|
props.sellStockShort(stock, stock.playerShortShares);
|
|
} else {
|
|
props.sellStockLong(stock, stock.playerShares);
|
|
}
|
|
props.rerenderAllTickers();
|
|
break;
|
|
}
|
|
default: {
|
|
dialogBoxCreate(`ERROR: 'Sell All' only works for Market Orders`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Whether the player has access to orders besides market orders (limit/stop)
|
|
function hasOrderAccess(): boolean {
|
|
return props.p.bitNodeN === 8 || SourceFileFlags[8] >= 3;
|
|
}
|
|
|
|
// Whether the player has access to shorting stocks
|
|
function hasShortAccess(): boolean {
|
|
return props.p.bitNodeN === 8 || SourceFileFlags[8] >= 2;
|
|
}
|
|
|
|
return (
|
|
<Box component={Paper}>
|
|
<ListItemButton onClick={() => setTicketOpen((old) => !old)}>
|
|
<ListItemText primary={<StockTickerHeaderText p={props.p} stock={props.stock} />} />
|
|
{tickerOpen ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
|
</ListItemButton>
|
|
<Collapse in={tickerOpen} unmountOnExit>
|
|
<Box sx={{ mx: 4 }}>
|
|
<Box display="flex" alignItems="center">
|
|
<TextField onChange={handleQuantityChange} placeholder="Quantity (Shares)" value={qty} />
|
|
<Select onChange={handlePositionTypeChange} value={position}>
|
|
<MenuItem value={PositionTypes.Long}>Long</MenuItem>
|
|
{hasShortAccess() && <MenuItem value={PositionTypes.Short}>Short</MenuItem>}
|
|
</Select>
|
|
<Select onChange={handleOrderTypeChange} value={orderType}>
|
|
<MenuItem value={SelectorOrderType.Market}>{SelectorOrderType.Market}</MenuItem>
|
|
{hasOrderAccess() && <MenuItem value={SelectorOrderType.Limit}>{SelectorOrderType.Limit}</MenuItem>}
|
|
{hasOrderAccess() && <MenuItem value={SelectorOrderType.Stop}>{SelectorOrderType.Stop}</MenuItem>}
|
|
</Select>
|
|
|
|
<StockTickerTxButton onClick={handleBuyButtonClick} text={"Buy"} tooltip={getBuyTransactionCostContent()} />
|
|
<StockTickerTxButton
|
|
onClick={handleSellButtonClick}
|
|
text={"Sell"}
|
|
tooltip={getSellTransactionCostContent()}
|
|
/>
|
|
<StockTickerTxButton onClick={handleBuyMaxButtonClick} text={"Buy MAX"} />
|
|
<StockTickerTxButton onClick={handleSellAllButtonClick} text={"Sell ALL"} />
|
|
</Box>
|
|
<StockTickerPositionText p={props.p} stock={props.stock} />
|
|
<StockTickerOrderList cancelOrder={props.cancelOrder} orders={props.orders} p={props.p} stock={props.stock} />
|
|
|
|
<PlaceOrderModal
|
|
text={modalProps.text}
|
|
placeText={modalProps.placeText}
|
|
place={modalProps.place}
|
|
open={open}
|
|
onClose={() => setOpen(false)}
|
|
/>
|
|
</Box>
|
|
</Collapse>
|
|
</Box>
|
|
);
|
|
}
|