bitburner-src/src/StockMarket/StockMarket.tsx

284 lines
8.7 KiB
TypeScript
Raw Normal View History

import { IOrderBook } from "./IOrderBook";
import { IStockMarket } from "./IStockMarket";
import { Order } from "./Order";
import { processOrders } from "./OrderProcessing";
import { Stock } from "./Stock";
import { TicksPerCycle } from "./StockMarketConstants";
import { InitStockMetadata } from "./data/InitStockMetadata";
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
import { StockSymbols } from "./data/StockSymbols";
import { CONSTANTS } from "../Constants";
2022-03-06 05:05:55 +01:00
import { numeralWrapper } from "../ui/numeralFormat";
2021-09-25 20:42:57 +02:00
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver } from "../utils/JSONReviver";
import { NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
2021-09-17 08:31:19 +02:00
export let StockMarket: IStockMarket = {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket; // Maps full stock name -> Stock object
2022-10-03 18:12:16 +02:00
export const SymbolToStockMap: Record<string, Stock> = {}; // Maps symbol -> Stock object
2021-09-05 01:09:30 +02:00
export function placeOrder(
stock: Stock,
shares: number,
price: number,
type: OrderTypes,
position: PositionTypes,
2022-08-21 01:08:05 +02:00
ctx?: NetscriptContext,
2021-09-05 01:09:30 +02:00
): boolean {
if (!(stock instanceof Stock)) {
if (ctx) {
helpers.log(ctx, () => `Invalid stock: '${stock}'`);
2021-09-05 01:09:30 +02:00
} else {
dialogBoxCreate(`ERROR: Invalid stock passed to placeOrder() function`);
}
2021-09-05 01:09:30 +02:00
return false;
}
if (typeof shares !== "number" || typeof price !== "number") {
if (ctx) {
helpers.log(ctx, () => `Invalid arguments: shares='${shares}' price='${price}'`);
2021-09-05 01:09:30 +02:00
} else {
2021-09-09 05:47:34 +02:00
dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
}
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
const order = new Order(stock.symbol, shares, price, type, position);
if (StockMarket["Orders"] == null) {
const orders: IOrderBook = {};
2022-01-16 01:45:03 +01:00
for (const name of Object.keys(StockMarket)) {
2021-09-05 01:09:30 +02:00
const stk = StockMarket[name];
if (!(stk instanceof Stock)) {
continue;
}
orders[stk.symbol] = [];
}
2021-09-05 01:09:30 +02:00
StockMarket["Orders"] = orders;
}
StockMarket["Orders"][stock.symbol].push(order);
// Process to see if it should be executed immediately
const processOrderRefs = {
2022-07-15 07:51:30 +02:00
stockMarket: StockMarket,
2021-09-05 01:09:30 +02:00
symbolToStockMap: SymbolToStockMap,
};
processOrders(stock, order.type, order.pos, processOrderRefs);
return true;
}
// Returns true if successfully cancels an order, false otherwise
2022-07-15 07:51:30 +02:00
export interface ICancelOrderParams {
2021-09-05 01:09:30 +02:00
order?: Order;
pos?: PositionTypes;
price?: number;
shares?: number;
stock?: Stock;
type?: OrderTypes;
}
export function cancelOrder(params: ICancelOrderParams, ctx?: NetscriptContext): boolean {
2022-08-21 01:08:05 +02:00
if (StockMarket["Orders"] == null) return false;
2021-09-05 01:09:30 +02:00
if (params.order && params.order instanceof Order) {
const order = params.order;
// An 'Order' object is passed in
const stockOrders = StockMarket["Orders"][order.stockSymbol];
for (let i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
stockOrders.splice(i, 1);
return true;
}
}
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
const stockOrders = StockMarket["Orders"][params.stock.symbol];
2021-09-09 05:47:34 +02:00
const orderTxt = params.stock.symbol + " - " + params.shares + " @ " + numeralWrapper.formatMoney(params.price);
2021-09-05 01:09:30 +02:00
for (let i = 0; i < stockOrders.length; ++i) {
const order = stockOrders[i];
if (
params.shares === order.shares &&
params.price === order.price &&
params.type === order.type &&
params.pos === order.pos
) {
stockOrders.splice(i, 1);
2022-08-21 01:08:05 +02:00
if (ctx) helpers.log(ctx, () => "Successfully cancelled order: " + orderTxt);
2021-09-05 01:09:30 +02:00
return true;
}
}
2022-08-21 01:08:05 +02:00
if (ctx) helpers.log(ctx, () => "Failed to cancel order: " + orderTxt);
return false;
2021-09-05 01:09:30 +02:00
}
return false;
}
2021-05-01 09:17:31 +02:00
export function loadStockMarket(saveString: string): void {
2021-09-05 01:09:30 +02:00
if (saveString === "") {
2021-09-17 08:31:19 +02:00
StockMarket = {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket;
2022-08-21 01:08:05 +02:00
} else StockMarket = JSON.parse(saveString, Reviver);
}
2021-05-01 09:17:31 +02:00
export function deleteStockMarket(): void {
2021-09-17 08:31:19 +02:00
StockMarket = {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket;
}
2021-05-01 09:17:31 +02:00
export function initStockMarket(): void {
2022-01-16 01:45:03 +01:00
for (const stk of Object.keys(StockMarket)) {
2022-08-21 01:08:05 +02:00
if (StockMarket.hasOwnProperty(stk)) delete StockMarket[stk];
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
for (const metadata of InitStockMetadata) {
const name = metadata.name;
StockMarket[name] = new Stock(metadata);
}
2021-09-05 01:09:30 +02:00
const orders: IOrderBook = {};
2022-01-16 01:45:03 +01:00
for (const name of Object.keys(StockMarket)) {
2021-09-05 01:09:30 +02:00
const stock = StockMarket[name];
2022-08-21 01:08:05 +02:00
if (!(stock instanceof Stock)) continue;
2021-09-05 01:09:30 +02:00
orders[stock.symbol] = [];
}
StockMarket["Orders"] = orders;
2021-09-05 01:09:30 +02:00
StockMarket.storedCycles = 0;
StockMarket.lastUpdate = 0;
StockMarket.ticksUntilCycle = TicksPerCycle;
initSymbolToStockMap();
}
2021-05-01 09:17:31 +02:00
export function initSymbolToStockMap(): void {
for (const [name, symbol] of Object.entries(StockSymbols)) {
const stock = StockMarket[name];
if (stock == null) {
console.error(`Could not find Stock for ${name}`);
continue;
}
SymbolToStockMap[symbol] = stock;
2021-09-05 01:09:30 +02:00
}
}
2021-10-15 00:45:50 +02:00
function stockMarketCycle(): void {
2022-01-16 01:45:03 +01:00
for (const name of Object.keys(StockMarket)) {
2021-09-05 01:09:30 +02:00
const stock = StockMarket[name];
2022-08-21 01:08:05 +02:00
if (!(stock instanceof Stock)) continue;
2021-09-05 01:09:30 +02:00
const roll = Math.random();
if (roll < 0.45) {
stock.b = !stock.b;
stock.flipForecastForecast();
}
2021-09-05 01:09:30 +02:00
StockMarket.ticksUntilCycle = TicksPerCycle;
}
}
// Stock prices updated every 6 seconds
const msPerStockUpdate = 6e3;
const cyclesPerStockUpdate = msPerStockUpdate / CONSTANTS.MilliPerCycle;
2021-09-05 01:09:30 +02:00
export function processStockPrices(numCycles = 1): void {
if (StockMarket.storedCycles == null || isNaN(StockMarket.storedCycles)) {
StockMarket.storedCycles = 0;
}
StockMarket.storedCycles += numCycles;
if (StockMarket.storedCycles < cyclesPerStockUpdate) {
return;
}
// We can process the update every 4 seconds as long as there are enough
// stored cycles. This lets us account for offline time
const timeNow = new Date().getTime();
2022-08-21 01:08:05 +02:00
if (timeNow - StockMarket.lastUpdate < 4e3) return;
2021-09-05 01:09:30 +02:00
StockMarket.lastUpdate = timeNow;
StockMarket.storedCycles -= cyclesPerStockUpdate;
// Cycle
2021-09-09 05:47:34 +02:00
if (StockMarket.ticksUntilCycle == null || typeof StockMarket.ticksUntilCycle !== "number") {
2021-09-05 01:09:30 +02:00
StockMarket.ticksUntilCycle = TicksPerCycle;
}
--StockMarket.ticksUntilCycle;
2022-08-21 01:08:05 +02:00
if (StockMarket.ticksUntilCycle <= 0) stockMarketCycle();
2021-09-05 01:09:30 +02:00
const v = Math.random();
2022-01-16 01:45:03 +01:00
for (const name of Object.keys(StockMarket)) {
2021-09-05 01:09:30 +02:00
const stock = StockMarket[name];
2022-08-21 01:08:05 +02:00
if (!(stock instanceof Stock)) continue;
2021-09-05 01:09:30 +02:00
let av = (v * stock.mv) / 100;
if (isNaN(av)) {
av = 0.02;
}
2021-09-05 01:09:30 +02:00
let chc = 50;
if (stock.b) {
chc = (chc + stock.otlkMag) / 100;
} else {
chc = (chc - stock.otlkMag) / 100;
}
if (stock.price >= stock.cap) {
chc = 0.1; // "Soft Limit" on stock price. It could still go up but its unlikely
stock.b = false;
}
if (isNaN(chc)) {
chc = 0.5;
}
2021-09-05 01:09:30 +02:00
const c = Math.random();
const processOrderRefs = {
2021-09-17 08:31:19 +02:00
stockMarket: StockMarket,
2021-09-05 01:09:30 +02:00
symbolToStockMap: SymbolToStockMap,
};
if (c < chc) {
stock.changePrice(stock.price * (1 + av));
2021-09-09 05:47:34 +02:00
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Short, processOrderRefs);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Long, processOrderRefs);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Long, processOrderRefs);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Short, processOrderRefs);
2021-09-05 01:09:30 +02:00
} else {
stock.changePrice(stock.price / (1 + av));
2021-09-09 05:47:34 +02:00
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Long, processOrderRefs);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Short, processOrderRefs);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Short, processOrderRefs);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Long, processOrderRefs);
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
let otlkMagChange = stock.otlkMag * av;
if (stock.otlkMag < 5) {
if (stock.otlkMag <= 1) {
otlkMagChange = 1;
} else {
otlkMagChange *= 10;
}
}
2021-09-05 01:09:30 +02:00
stock.cycleForecast(otlkMagChange);
stock.cycleForecastForecast(otlkMagChange / 2);
2021-09-05 01:09:30 +02:00
// Shares required for price movement gradually approaches max over time
2021-09-09 05:47:34 +02:00
stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovement + 10, stock.shareTxForMovement);
2021-09-05 01:09:30 +02:00
}
}