2019-05-05 06:03:40 +02:00
|
|
|
import {
|
|
|
|
buyStock,
|
|
|
|
sellStock,
|
|
|
|
shortStock,
|
|
|
|
sellShort,
|
|
|
|
} from "./BuyingAndSelling";
|
2019-04-28 02:43:38 +02:00
|
|
|
import { Order } from "./Order";
|
2019-05-05 06:03:40 +02:00
|
|
|
import { processOrders } from "./OrderProcessing";
|
2019-04-23 10:48:15 +02:00
|
|
|
import { Stock } from "./Stock";
|
2019-04-28 04:20:51 +02:00
|
|
|
import {
|
|
|
|
getBuyTransactionCost,
|
|
|
|
getSellTransactionGain,
|
|
|
|
processBuyTransactionPriceMovement,
|
|
|
|
processSellTransactionPriceMovement
|
|
|
|
} from "./StockMarketHelpers";
|
2019-04-23 10:48:15 +02:00
|
|
|
import {
|
|
|
|
getStockMarket4SDataCost,
|
|
|
|
getStockMarket4STixApiCost
|
|
|
|
} from "./StockMarketCosts";
|
|
|
|
import { InitStockMetadata } from "./data/InitStockMetadata";
|
2019-04-28 02:43:38 +02:00
|
|
|
import { OrderTypes } from "./data/OrderTypes";
|
|
|
|
import { PositionTypes } from "./data/PositionTypes";
|
2019-04-23 10:48:15 +02:00
|
|
|
import { StockSymbols } from "./data/StockSymbols";
|
|
|
|
import { StockMarketRoot } from "./ui/Root";
|
|
|
|
|
|
|
|
import { CONSTANTS } from "../Constants";
|
2019-05-05 06:03:40 +02:00
|
|
|
import { WorkerScript } from "../Netscript/WorkerScript";
|
2019-04-23 10:48:15 +02:00
|
|
|
import { Player } from "../Player";
|
|
|
|
|
|
|
|
import { Page, routing } from ".././ui/navigationTracking";
|
|
|
|
import { numeralWrapper } from ".././ui/numeralFormat";
|
|
|
|
|
|
|
|
import { dialogBoxCreate } from "../../utils/DialogBox";
|
|
|
|
import { Reviver } from "../../utils/JSONReviver";
|
|
|
|
|
|
|
|
import React from "react";
|
2019-04-28 02:43:38 +02:00
|
|
|
import ReactDOM from "react-dom";
|
2019-04-23 10:48:15 +02:00
|
|
|
|
2019-05-05 06:03:40 +02:00
|
|
|
export let StockMarket = {}; // Maps full stock name -> Stock object
|
|
|
|
export let SymbolToStockMap = {}; // Maps symbol -> Stock object
|
|
|
|
|
2019-04-23 10:48:15 +02:00
|
|
|
export function placeOrder(stock, shares, price, type, position, workerScript=null) {
|
2019-05-05 06:03:40 +02:00
|
|
|
const tixApi = (workerScript instanceof WorkerScript);
|
2019-05-07 03:01:06 +02:00
|
|
|
if (!(stock instanceof Stock)) {
|
|
|
|
if (tixApi) {
|
|
|
|
workerScript.log(`ERROR: Invalid stock passed to placeOrder() function`);
|
|
|
|
} else {
|
|
|
|
dialogBoxCreate(`ERROR: Invalid stock passed to placeOrder() function`);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2019-05-05 06:03:40 +02:00
|
|
|
if (typeof shares !== "number" || typeof price !== "number") {
|
2019-04-23 10:48:15 +02:00
|
|
|
if (tixApi) {
|
2019-05-07 03:01:06 +02:00
|
|
|
workerScript.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
|
2019-04-23 10:48:15 +02:00
|
|
|
} else {
|
|
|
|
dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2019-05-05 06:03:40 +02:00
|
|
|
|
2019-05-07 03:01:06 +02:00
|
|
|
const order = new Order(stock.symbol, shares, price, type, position);
|
2019-04-23 10:48:15 +02:00
|
|
|
if (StockMarket["Orders"] == null) {
|
|
|
|
const orders = {};
|
|
|
|
for (const name in StockMarket) {
|
2019-05-05 06:03:40 +02:00
|
|
|
const stk = StockMarket[name];
|
|
|
|
if (!(stk instanceof Stock)) { continue; }
|
|
|
|
orders[stk.symbol] = [];
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
|
|
|
StockMarket["Orders"] = orders;
|
|
|
|
}
|
|
|
|
StockMarket["Orders"][stock.symbol].push(order);
|
2019-05-05 06:03:40 +02:00
|
|
|
|
|
|
|
// Process to see if it should be executed immediately
|
|
|
|
const processOrderRefs = {
|
|
|
|
rerenderFn: displayStockMarketContent,
|
|
|
|
stockMarket: StockMarket,
|
2019-05-07 03:01:06 +02:00
|
|
|
symbolToStockMap: SymbolToStockMap,
|
2019-05-05 06:03:40 +02:00
|
|
|
}
|
2019-05-07 03:01:06 +02:00
|
|
|
processOrders(stock, order.type, order.pos, processOrderRefs);
|
2019-04-23 10:48:15 +02:00
|
|
|
displayStockMarketContent();
|
2019-05-05 06:03:40 +02:00
|
|
|
|
2019-04-23 10:48:15 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if successfully cancels an order, false otherwise
|
|
|
|
export function cancelOrder(params, workerScript=null) {
|
|
|
|
var tixApi = (workerScript instanceof WorkerScript);
|
|
|
|
if (StockMarket["Orders"] == null) {return false;}
|
|
|
|
if (params.order && params.order instanceof Order) {
|
2019-05-07 03:01:06 +02:00
|
|
|
const order = params.order;
|
2019-04-28 02:43:38 +02:00
|
|
|
// An 'Order' object is passed in
|
2019-05-07 03:01:06 +02:00
|
|
|
var stockOrders = StockMarket["Orders"][order.stockSymbol];
|
2019-04-23 10:48:15 +02:00
|
|
|
for (var i = 0; i < stockOrders.length; ++i) {
|
|
|
|
if (order == stockOrders[i]) {
|
|
|
|
stockOrders.splice(i, 1);
|
|
|
|
displayStockMarketContent();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
} else if (params.stock && params.shares && params.price && params.type &&
|
|
|
|
params.pos && params.stock instanceof Stock) {
|
2019-04-28 02:43:38 +02:00
|
|
|
// Order properties are passed in. Need to look for the order
|
2019-04-23 10:48:15 +02:00
|
|
|
var stockOrders = StockMarket["Orders"][params.stock.symbol];
|
|
|
|
var orderTxt = params.stock.symbol + " - " + params.shares + " @ " +
|
2019-05-05 06:03:40 +02:00
|
|
|
numeralWrapper.formatMoney(params.price);
|
2019-04-23 10:48:15 +02:00
|
|
|
for (var i = 0; i < stockOrders.length; ++i) {
|
|
|
|
var order = stockOrders[i];
|
|
|
|
if (params.shares === order.shares &&
|
|
|
|
params.price === order.price &&
|
|
|
|
params.type === order.type &&
|
|
|
|
params.pos === order.pos) {
|
|
|
|
stockOrders.splice(i, 1);
|
|
|
|
displayStockMarketContent();
|
|
|
|
if (tixApi) {
|
|
|
|
workerScript.scriptRef.log("Successfully cancelled order: " + orderTxt);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (tixApi) {
|
|
|
|
workerScript.scriptRef.log("Failed to cancel order: " + orderTxt);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function loadStockMarket(saveString) {
|
|
|
|
if (saveString === "") {
|
|
|
|
StockMarket = {};
|
|
|
|
} else {
|
|
|
|
StockMarket = JSON.parse(saveString, Reviver);
|
2019-05-07 03:01:06 +02:00
|
|
|
|
|
|
|
// Backwards compatibility for v0.47.0
|
|
|
|
const orderBook = StockMarket["Orders"];
|
|
|
|
if (orderBook != null) {
|
|
|
|
// For each order, set its 'stockSymbol' property equal to the
|
|
|
|
// symbol of its 'stock' property
|
|
|
|
for (const stockSymbol in orderBook) {
|
|
|
|
const ordersForStock = orderBook[stockSymbol];
|
|
|
|
if (Array.isArray(ordersForStock)) {
|
|
|
|
for (const order of ordersForStock) {
|
|
|
|
if (order instanceof Order && order.stock instanceof Stock) {
|
|
|
|
order.stockSymbol = order.stock.symbol;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.log(`Converted Stock Market order book to v0.47.0 format`);
|
|
|
|
}
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-30 05:54:20 +02:00
|
|
|
export function deleteStockMarket() {
|
|
|
|
StockMarket = {};
|
|
|
|
}
|
|
|
|
|
2019-04-23 10:48:15 +02:00
|
|
|
export function initStockMarket() {
|
|
|
|
for (const stk in StockMarket) {
|
|
|
|
if (StockMarket.hasOwnProperty(stk)) {
|
|
|
|
delete StockMarket[stk];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const metadata of InitStockMetadata) {
|
|
|
|
const name = metadata.name;
|
|
|
|
StockMarket[name] = new Stock(metadata);
|
|
|
|
}
|
|
|
|
|
|
|
|
const orders = {};
|
|
|
|
for (const name in StockMarket) {
|
2019-05-05 06:03:40 +02:00
|
|
|
const stock = StockMarket[name];
|
|
|
|
if (!(stock instanceof Stock)) { continue; }
|
|
|
|
orders[stock.symbol] = [];
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
|
|
|
StockMarket["Orders"] = orders;
|
|
|
|
|
|
|
|
StockMarket.storedCycles = 0;
|
|
|
|
StockMarket.lastUpdate = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function initSymbolToStockMap() {
|
|
|
|
for (const name in StockSymbols) {
|
|
|
|
if (StockSymbols.hasOwnProperty(name)) {
|
|
|
|
const stock = StockMarket[name];
|
|
|
|
if (stock == null) {
|
|
|
|
console.error(`Could not find Stock for ${name}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const symbol = StockSymbols[name];
|
|
|
|
SymbolToStockMap[symbol] = stock;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function stockMarketCycle() {
|
2019-04-30 05:54:20 +02:00
|
|
|
for (const name in StockMarket) {
|
|
|
|
const stock = StockMarket[name];
|
|
|
|
if (!(stock instanceof Stock)) { continue; }
|
|
|
|
let thresh = 0.6;
|
|
|
|
if (stock.b) { thresh = 0.4; }
|
2019-05-23 02:23:30 +02:00
|
|
|
const roll = Math.random();
|
|
|
|
if (roll < 0.4) {
|
|
|
|
stock.flipForecastForecast();
|
|
|
|
} else if (roll < 0.6) {
|
|
|
|
stock.otlkMagForecast += 0.5;
|
|
|
|
stock.otlkMagForecast = Math.min(stock.otlkMagForecast * 1.02, 50);
|
|
|
|
} else if (roll < 0.8) {
|
|
|
|
stock.otlkMagForecast -= 0.5;
|
|
|
|
stock.otlkMagForecast = otlkMagForecast * (1 / 1.02);
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-30 05:54:20 +02:00
|
|
|
// Stock prices updated every 6 seconds
|
|
|
|
const msPerStockUpdate = 6e3;
|
|
|
|
const cyclesPerStockUpdate = msPerStockUpdate / CONSTANTS.MilliPerCycle;
|
2019-04-23 10:48:15 +02:00
|
|
|
export function processStockPrices(numCycles=1) {
|
|
|
|
if (StockMarket.storedCycles == null || isNaN(StockMarket.storedCycles)) { StockMarket.storedCycles = 0; }
|
|
|
|
StockMarket.storedCycles += numCycles;
|
|
|
|
|
|
|
|
if (StockMarket.storedCycles < cyclesPerStockUpdate) { return; }
|
|
|
|
|
2019-04-30 05:54:20 +02:00
|
|
|
// We can process the update every 4 seconds as long as there are enough
|
|
|
|
// stored cycles. This lets us account for offline time
|
2019-04-23 10:48:15 +02:00
|
|
|
const timeNow = new Date().getTime();
|
|
|
|
if (timeNow - StockMarket.lastUpdate < 4e3) { return; }
|
|
|
|
|
|
|
|
StockMarket.lastUpdate = timeNow;
|
|
|
|
StockMarket.storedCycles -= cyclesPerStockUpdate;
|
|
|
|
|
|
|
|
var v = Math.random();
|
2019-04-30 05:54:20 +02:00
|
|
|
for (const name in StockMarket) {
|
|
|
|
const stock = StockMarket[name];
|
|
|
|
if (!(stock instanceof Stock)) { continue; }
|
|
|
|
let av = (v * stock.mv) / 100;
|
|
|
|
if (isNaN(av)) { av = .02; }
|
|
|
|
|
|
|
|
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; }
|
|
|
|
|
|
|
|
const c = Math.random();
|
2019-05-05 06:03:40 +02:00
|
|
|
const processOrderRefs = {
|
|
|
|
rerenderFn: displayStockMarketContent,
|
|
|
|
stockMarket: StockMarket,
|
2019-05-07 03:01:06 +02:00
|
|
|
symbolToStockMap: SymbolToStockMap,
|
2019-05-05 06:03:40 +02:00
|
|
|
}
|
2019-04-30 05:54:20 +02:00
|
|
|
if (c < chc) {
|
2019-05-07 03:01:06 +02:00
|
|
|
stock.changePrice(stock.price * (1 + av));
|
2019-05-05 06:03:40 +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);
|
2019-04-30 05:54:20 +02:00
|
|
|
} else {
|
2019-05-07 03:01:06 +02:00
|
|
|
stock.changePrice(stock.price / (1 + av));
|
2019-05-05 06:03:40 +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);
|
2019-04-30 05:54:20 +02:00
|
|
|
}
|
2019-04-23 10:48:15 +02:00
|
|
|
|
2019-04-30 05:54:20 +02:00
|
|
|
let otlkMagChange = stock.otlkMag * av;
|
2019-05-23 02:23:30 +02:00
|
|
|
if (stock.otlkMag < 1) {
|
2019-04-30 05:54:20 +02:00
|
|
|
otlkMagChange = 1;
|
|
|
|
}
|
2019-05-23 02:23:30 +02:00
|
|
|
stock.cycleForecast(otlkMagChange);
|
2019-04-30 05:54:20 +02:00
|
|
|
|
|
|
|
// Shares required for price movement gradually approaches max over time
|
2019-05-23 02:23:30 +02:00
|
|
|
stock.shareTxUntilMovementUp = Math.min(stock.shareTxUntilMovementUp + 5, stock.shareTxForMovement);
|
|
|
|
stock.shareTxUntilMovementDown = Math.min(stock.shareTxUntilMovementDown + 5, stock.shareTxForMovement);
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
2019-04-28 02:43:38 +02:00
|
|
|
|
|
|
|
displayStockMarketContent();
|
2019-04-23 10:48:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let stockMarketContainer = null;
|
|
|
|
function setStockMarketContainer() {
|
|
|
|
stockMarketContainer = document.getElementById("stock-market-container");
|
|
|
|
document.removeEventListener("DOMContentLoaded", setStockMarketContainer);
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", setStockMarketContainer);
|
|
|
|
|
|
|
|
function initStockMarketFnForReact() {
|
|
|
|
initStockMarket();
|
|
|
|
initSymbolToStockMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function displayStockMarketContent() {
|
|
|
|
if (!routing.isOn(Page.StockMarket)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stockMarketContainer instanceof HTMLElement) {
|
|
|
|
ReactDOM.render(
|
|
|
|
<StockMarketRoot
|
|
|
|
buyStockLong={buyStock}
|
|
|
|
buyStockShort={shortStock}
|
|
|
|
cancelOrder={cancelOrder}
|
|
|
|
initStockMarket={initStockMarketFnForReact}
|
|
|
|
p={Player}
|
|
|
|
placeOrder={placeOrder}
|
|
|
|
sellStockLong={sellStock}
|
|
|
|
sellStockShort={sellShort}
|
|
|
|
stockMarket={StockMarket}
|
|
|
|
/>,
|
|
|
|
stockMarketContainer
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|