bitburner-src/src/StockMarket/BuyingAndSelling.tsx

402 lines
13 KiB
TypeScript
Raw Normal View History

/**
* Functions for buying/selling stocks. There are four functions total, two for
* long positions and two for short positions.
*/
import { Stock } from "./Stock";
import {
2021-09-05 01:09:30 +02:00
getBuyTransactionCost,
getSellTransactionGain,
processTransactionForecastMovement,
} from "./StockMarketHelpers";
import { PositionTypes } from "./data/PositionTypes";
import { CONSTANTS } from "../Constants";
import { WorkerScript } from "../Netscript/WorkerScript";
import { Player } from "../Player";
import { numeralWrapper } from "../ui/numeralFormat";
import { Money } from "../ui/React/Money";
2021-09-25 20:42:57 +02:00
import { dialogBoxCreate } from "../ui/React/DialogBox";
import * as React from "react";
/**
2021-09-05 01:09:30 +02:00
* Each function takes an optional config object as its last argument
*/
interface IOptions {
2021-09-05 01:09:30 +02:00
rerenderFn?: () => void;
suppressDialog?: boolean;
}
/**
* Attempt to buy a stock in the long position
* @param {Stock} stock - Stock to buy
* @param {number} shares - Number of shares to buy
* @param {WorkerScript} workerScript - If this is being called through Netscript
* @param opts - Optional configuration for this function's behavior. See top of file
* @returns {boolean} - true if successful, false otherwise
*/
2021-09-05 01:09:30 +02:00
export function buyStock(
stock: Stock,
shares: number,
workerScript: WorkerScript | null = null,
opts: IOptions = {},
): boolean {
// Validate arguments
shares = Math.round(shares);
if (shares <= 0) {
return false;
}
if (stock == null || isNaN(shares)) {
if (workerScript) {
2021-11-27 00:43:50 +01:00
workerScript.log("stock.buy", () => `Invalid arguments: stock='${stock}' shares='${shares}'`);
2021-09-05 01:09:30 +02:00
} else if (opts.suppressDialog !== true) {
2021-09-09 05:47:34 +02:00
dialogBoxCreate("Failed to buy stock. This may be a bug, contact developer");
}
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
// Does player have enough money?
const totalPrice = getBuyTransactionCost(stock, shares, PositionTypes.Long);
if (totalPrice == null) {
return false;
}
2021-11-12 02:42:19 +01:00
if (Player.money < totalPrice) {
2021-09-05 01:09:30 +02:00
if (workerScript) {
workerScript.log(
2021-11-27 00:43:50 +01:00
"stock.buy",
() =>
`You do not have enough money to purchase this position. You need ${numeralWrapper.formatMoney(totalPrice)}.`,
2021-09-05 01:09:30 +02:00
);
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
You do not have enough money to purchase this. You need <Money money={totalPrice} />
2021-09-05 01:09:30 +02:00
</>,
);
}
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
// Would this purchase exceed the maximum number of shares?
if (shares + stock.playerShares + stock.playerShortShares > stock.maxShares) {
2021-05-01 09:17:31 +02:00
if (workerScript) {
2021-09-05 01:09:30 +02:00
workerScript.log(
2021-11-27 00:43:50 +01:00
"stock.buy",
() =>
`Purchasing '${shares + stock.playerShares + stock.playerShortShares}' shares would exceed ${
stock.symbol
}'s maximum (${stock.maxShares}) number of shares`,
2021-09-05 01:09:30 +02:00
);
} else if (opts.suppressDialog !== true) {
2021-09-05 01:09:30 +02:00
dialogBoxCreate(
2021-09-09 05:47:34 +02:00
`You cannot purchase this many shares. ${stock.symbol} has a maximum of ${numeralWrapper.formatShares(
2021-09-05 01:09:30 +02:00
stock.maxShares,
)} shares.`,
);
}
2021-09-05 01:09:30 +02:00
return false;
}
const origTotal = stock.playerShares * stock.playerAvgPx;
2021-10-27 20:18:33 +02:00
Player.loseMoney(totalPrice, "stock");
2021-09-05 01:09:30 +02:00
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShares = Math.round(stock.playerShares + shares);
stock.playerAvgPx = newTotal / stock.playerShares;
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
}
if (workerScript) {
const resultTxt =
2021-09-09 05:47:34 +02:00
`Bought ${numeralWrapper.formatShares(shares)} shares of ${stock.symbol} for ${numeralWrapper.formatMoney(
totalPrice,
)}. ` + `Paid ${numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} in commission fees.`;
2021-11-27 00:43:50 +01:00
workerScript.log("stock.buy", () => resultTxt);
2021-09-05 01:09:30 +02:00
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
Bought {numeralWrapper.formatShares(shares)} shares of {stock.symbol} for <Money money={totalPrice} />. Paid{" "}
2021-09-05 01:09:30 +02:00
<Money money={CONSTANTS.StockMarketCommission} /> in commission fees.
</>,
);
}
return true;
}
/**
* Attempt to sell a stock in the long position
* @param {Stock} stock - Stock to sell
* @param {number} shares - Number of shares to sell
* @param {WorkerScript} workerScript - If this is being called through Netscript
* @param opts - Optional configuration for this function's behavior. See top of file
* returns {boolean} - true if successfully sells given number of shares OR MAX owned, false otherwise
*/
2021-09-05 01:09:30 +02:00
export function sellStock(
stock: Stock,
shares: number,
workerScript: WorkerScript | null = null,
opts: IOptions = {},
): boolean {
// Sanitize/Validate arguments
if (stock == null || shares < 0 || isNaN(shares)) {
2021-05-01 09:17:31 +02:00
if (workerScript) {
2021-11-27 00:43:50 +01:00
workerScript.log("stock.sell", () => `Invalid arguments: stock='${stock}' shares='${shares}'`);
2021-09-05 01:09:30 +02:00
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
"Failed to sell stock. This is probably due to an invalid quantity. Otherwise, this may be a bug, contact developer",
);
}
2021-09-05 01:09:30 +02:00
return false;
}
shares = Math.round(shares);
if (shares > stock.playerShares) {
shares = stock.playerShares;
}
if (shares === 0) {
return false;
}
const gains = getSellTransactionGain(stock, shares, PositionTypes.Long);
if (gains == null) {
return false;
}
let netProfit = gains - stock.playerAvgPx * shares;
if (isNaN(netProfit)) {
netProfit = 0;
}
2021-10-27 20:18:33 +02:00
Player.gainMoney(gains, "stock");
2021-09-05 01:09:30 +02:00
if (workerScript) {
workerScript.scriptRef.onlineMoneyMade += netProfit;
Player.scriptProdSinceLastAug += netProfit;
}
stock.playerShares = Math.round(stock.playerShares - shares);
if (stock.playerShares === 0) {
stock.playerAvgPx = 0;
}
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
}
if (workerScript) {
const resultTxt =
2021-09-09 05:47:34 +02:00
`Sold ${numeralWrapper.formatShares(shares)} shares of ${stock.symbol}. ` +
`After commissions, you gained a total of ${numeralWrapper.formatMoney(gains)}.`;
2021-11-27 00:43:50 +01:00
workerScript.log("stock.sell", () => resultTxt);
2021-09-05 01:09:30 +02:00
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
Sold {numeralWrapper.formatShares(shares)} shares of {stock.symbol}. After commissions, you gained a total of{" "}
<Money money={gains} />.
2021-09-05 01:09:30 +02:00
</>,
);
}
return true;
}
/**
* Attempt to buy a stock in the short position
* @param {Stock} stock - Stock to sell
* @param {number} shares - Number of shares to short
* @param {WorkerScript} workerScript - If this is being called through Netscript
* @param opts - Optional configuration for this function's behavior. See top of file
* @returns {boolean} - true if successful, false otherwise
*/
2021-09-05 01:09:30 +02:00
export function shortStock(
stock: Stock,
shares: number,
workerScript: WorkerScript | null = null,
opts: IOptions = {},
): boolean {
// Validate arguments
shares = Math.round(shares);
if (shares <= 0) {
return false;
}
if (stock == null || isNaN(shares)) {
if (workerScript) {
2021-11-27 00:43:50 +01:00
workerScript.log("stock.short", () => `Invalid arguments: stock='${stock}' shares='${shares}'`);
2021-09-05 01:09:30 +02:00
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
"Failed to initiate a short position in a stock. This is probably " +
"due to an invalid quantity. Otherwise, this may be a bug, so contact developer",
);
}
2021-09-05 01:09:30 +02:00
return false;
}
// Does the player have enough money?
const totalPrice = getBuyTransactionCost(stock, shares, PositionTypes.Short);
if (totalPrice == null) {
return false;
}
2021-11-12 02:42:19 +01:00
if (Player.money < totalPrice) {
2021-09-05 01:09:30 +02:00
if (workerScript) {
workerScript.log(
2021-11-27 00:43:50 +01:00
"stock.short",
() =>
"You do not have enough " +
2021-09-05 01:09:30 +02:00
"money to purchase this short position. You need " +
numeralWrapper.formatMoney(totalPrice),
);
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
You do not have enough money to purchase this short position. You need <Money money={totalPrice} />
2021-09-05 01:09:30 +02:00
</>,
);
}
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
// Would this purchase exceed the maximum number of shares?
if (shares + stock.playerShares + stock.playerShortShares > stock.maxShares) {
2021-05-01 09:17:31 +02:00
if (workerScript) {
2021-09-05 01:09:30 +02:00
workerScript.log(
2021-11-27 00:43:50 +01:00
"stock.short",
() =>
`This '${shares + stock.playerShares + stock.playerShortShares}' short shares would exceed ${
stock.symbol
}'s maximum (${stock.maxShares}) number of shares.`,
2021-09-05 01:09:30 +02:00
);
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(
`You cannot purchase this many shares. ${stock.symbol} has a maximum of ${stock.maxShares} shares.`,
);
}
2021-09-05 01:09:30 +02:00
return false;
}
const origTotal = stock.playerShortShares * stock.playerAvgShortPx;
2021-10-27 20:18:33 +02:00
Player.loseMoney(totalPrice, "stock");
2021-09-05 01:09:30 +02:00
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShortShares = Math.round(stock.playerShortShares + shares);
stock.playerAvgShortPx = newTotal / stock.playerShortShares;
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
}
if (workerScript) {
const resultTxt =
2021-09-09 05:47:34 +02:00
`Bought a short position of ${numeralWrapper.formatShares(shares)} shares of ${stock.symbol} ` +
`for ${numeralWrapper.formatMoney(totalPrice)}. Paid ${numeralWrapper.formatMoney(
2021-09-05 01:09:30 +02:00
CONSTANTS.StockMarketCommission,
)} ` +
`in commission fees.`;
2021-11-27 00:43:50 +01:00
workerScript.log("stock.short", () => resultTxt);
2021-09-05 01:09:30 +02:00
} else if (!opts.suppressDialog) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
Bought a short position of {numeralWrapper.formatShares(shares)} shares of {stock.symbol} for{" "}
<Money money={totalPrice} />. Paid <Money money={CONSTANTS.StockMarketCommission} /> in commission fees.
2021-09-05 01:09:30 +02:00
</>,
);
}
return true;
}
/**
* Attempt to sell a stock in the short position
* @param {Stock} stock - Stock to sell
* @param {number} shares - Number of shares to sell
* @param {WorkerScript} workerScript - If this is being called through Netscript
* @param opts - Optional configuration for this function's behavior. See top of file
* @returns {boolean} true if successfully sells given amount OR max owned, false otherwise
*/
2021-09-05 01:09:30 +02:00
export function sellShort(
stock: Stock,
shares: number,
workerScript: WorkerScript | null = null,
opts: IOptions = {},
): boolean {
if (stock == null || isNaN(shares) || shares < 0) {
2021-05-01 09:17:31 +02:00
if (workerScript) {
2021-11-27 00:43:50 +01:00
workerScript.log("stock.sellShort", () => `Invalid arguments: stock='${stock}' shares='${shares}'`);
2021-09-05 01:09:30 +02:00
} else if (!opts.suppressDialog) {
dialogBoxCreate(
"Failed to sell a short position in a stock. This is probably " +
"due to an invalid quantity. Otherwise, this may be a bug, so contact developer",
);
}
2021-09-05 01:09:30 +02:00
return false;
}
shares = Math.round(shares);
if (shares > stock.playerShortShares) {
shares = stock.playerShortShares;
}
if (shares === 0) {
return false;
}
const origCost = shares * stock.playerAvgShortPx;
const totalGain = getSellTransactionGain(stock, shares, PositionTypes.Short);
if (totalGain == null || isNaN(totalGain) || origCost == null) {
2021-05-01 09:17:31 +02:00
if (workerScript) {
2021-09-05 01:09:30 +02:00
workerScript.log(
2021-11-27 00:43:50 +01:00
"stock.sellShort",
() => `Failed to sell short position in a stock. This is probably either due to invalid arguments, or a bug`,
2021-09-05 01:09:30 +02:00
);
} else if (!opts.suppressDialog) {
2021-09-05 01:09:30 +02:00
dialogBoxCreate(
`Failed to sell short position in a stock. This is probably either due to invalid arguments, or a bug`,
);
}
2021-09-05 01:09:30 +02:00
return false;
}
let profit = totalGain - origCost;
if (isNaN(profit)) {
profit = 0;
}
2021-10-27 20:18:33 +02:00
Player.gainMoney(totalGain, "stock");
2021-09-05 01:09:30 +02:00
if (workerScript) {
workerScript.scriptRef.onlineMoneyMade += profit;
Player.scriptProdSinceLastAug += profit;
}
stock.playerShortShares = Math.round(stock.playerShortShares - shares);
if (stock.playerShortShares === 0) {
stock.playerAvgShortPx = 0;
}
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
}
if (workerScript) {
const resultTxt =
2021-09-09 05:47:34 +02:00
`Sold your short position of ${numeralWrapper.formatShares(shares)} shares of ${stock.symbol}. ` +
`After commissions, you gained a total of ${numeralWrapper.formatMoney(totalGain)}`;
2021-11-27 00:43:50 +01:00
workerScript.log("stock.sellShort", () => resultTxt);
2021-09-05 01:09:30 +02:00
} else if (!opts.suppressDialog) {
dialogBoxCreate(
<>
2021-09-09 05:47:34 +02:00
Sold your short position of {numeralWrapper.formatShares(shares)} shares of {stock.symbol}. After commissions,
you gained a total of <Money money={totalGain} />
2021-09-05 01:09:30 +02:00
</>,
);
}
return true;
}