diff --git a/src/StockMarket/OrderProcessing.ts b/src/StockMarket/OrderProcessing.ts index f77e29a08..adfe0b967 100644 --- a/src/StockMarket/OrderProcessing.ts +++ b/src/StockMarket/OrderProcessing.ts @@ -96,7 +96,7 @@ export function processOrders(stock: Stock, orderType: OrderTypes, posType: Posi /** * Execute a Stop or Limit Order. * @param {Order} order - Order being executed - * @param {IStockMarket} stockMarket - Reference to StockMarket object + * @param {IProcessOrderRefs} refs - References to objects/functions that are required for this function */ function executeOrder(order: Order, refs: IProcessOrderRefs) { const stock = refs.symbolToStockMap[order.stockSymbol]; diff --git a/src/StockMarket/Stock.ts b/src/StockMarket/Stock.ts index c4214fc21..a95455eba 100644 --- a/src/StockMarket/Stock.ts +++ b/src/StockMarket/Stock.ts @@ -6,6 +6,8 @@ import { } from "../../utils/JSONReviver"; import { getRandomInt } from "../../utils/helpers/getRandomInt"; +export const StockForecastInfluenceLimit = 5; + export interface IConstructorParams { b: boolean; initPrice: number | IMinMaxRange; @@ -290,6 +292,32 @@ export class Stock { return (50 + Math.min(Math.max(diff, -45), 45)) / 100; } + /** + * Changes a stock's forecast. This is used when the stock is influenced + * by a transaction. The stock's forecast always goes towards 50, but the + * movement is capped by a certain threshold/limit + */ + influenceForecast(change: number): void { + if (this.otlkMag > StockForecastInfluenceLimit) { + this.otlkMag = Math.max(StockForecastInfluenceLimit, this.otlkMag - change); + } + } + + /** + * Changes a stock's second-order forecast. This is used when the stock is + * influenced by a transaction. The stock's second-order forecast always + * goes towards 50. + */ + influenceForecastForecast(change: number): void { + if (this.otlkMagForecast > 50) { + this.otlkMagForecast -= change; + this.otlkMagForecast = Math.max(50, this.otlkMagForecast); + } else if (this.otlkMagForecast < 50) { + this.otlkMagForecast += change; + this.otlkMagForecast = Math.min(50, this.otlkMagForecast); + } + } + /** * Serialize the Stock to a JSON save state. */ diff --git a/src/StockMarket/StockMarket.tsx b/src/StockMarket/StockMarket.tsx index b3d2bf290..c8fa589ed 100644 --- a/src/StockMarket/StockMarket.tsx +++ b/src/StockMarket/StockMarket.tsx @@ -187,12 +187,12 @@ export function stockMarketCycle() { const roll = Math.random(); if (roll < 0.1) { stock.flipForecastForecast(); - StockMarket.ticksUntilCycle = 4 * TicksPerCycle; } else if (roll < 0.55) { stock.b = !stock.b; stock.flipForecastForecast(); - StockMarket.ticksUntilCycle = TicksPerCycle; } + + StockMarket.ticksUntilCycle = TicksPerCycle; } } @@ -262,8 +262,8 @@ export function processStockPrices(numCycles=1) { } let otlkMagChange = stock.otlkMag * av; - if (stock.otlkMag < 1) { - otlkMagChange = 1; + if (stock.otlkMag < 5) { + otlkMagChange *= 10; } stock.cycleForecast(otlkMagChange); stock.cycleForecastForecast(otlkMagChange / 2); diff --git a/src/StockMarket/StockMarketHelpers.ts b/src/StockMarket/StockMarketHelpers.ts index 61c1bba2d..476305668 100644 --- a/src/StockMarket/StockMarketHelpers.ts +++ b/src/StockMarket/StockMarketHelpers.ts @@ -6,7 +6,7 @@ import { PositionTypes } from "./data/PositionTypes"; import { CONSTANTS } from "../Constants"; // Amount by which a stock's forecast changes during each price movement -export const forecastChangePerPriceMovement = 0.01; +export const forecastChangePerPriceMovement = 0.006; /** * Calculate the total cost of a "buy" transaction. This accounts for spread and commission. @@ -60,7 +60,8 @@ export function getSellTransactionGain(stock: Stock, shares: number, posType: Po } /** - * Processes a stock's change in forecast whenever it is transacted + * Processes a stock's change in forecast & second-order forecast + * whenever it is transacted * @param {Stock} stock - Stock being sold * @param {number} shares - Number of sharse being transacted * @param {PositionTypes} posType - Long or short position @@ -78,7 +79,8 @@ export function processTransactionForecastMovement(stock: Stock, shares: number) stock.shareTxUntilMovement -= shares; if (stock.shareTxUntilMovement <= 0) { stock.shareTxUntilMovement = stock.shareTxForMovement; - stock.otlkMag -= (forecastChangePerPriceMovement); + stock.influenceForecast(forecastChangePerPriceMovement); + stock.influenceForecastForecast(forecastChangePerPriceMovement * (stock.mv / 100)); } return; @@ -95,13 +97,11 @@ export function processTransactionForecastMovement(stock: Stock, shares: number) stock.shareTxUntilMovement = stock.shareTxForMovement; } - // Forecast always decreases in magnitude const forecastChange = forecastChangePerPriceMovement * (numIterations - 1); - const changeLimit = 6; - if (stock.otlkMag > changeLimit) { - stock.otlkMag = Math.max(changeLimit, stock.otlkMag - forecastChange); - } + const forecastForecastChange = forecastChange * (stock.mv / 100); + stock.influenceForecast(forecastChange); + stock.influenceForecastForecast(forecastForecastChange); } /** diff --git a/src/StockMarket/data/InitStockMetadata.ts b/src/StockMarket/data/InitStockMetadata.ts index 3e84c8cd0..44bad8a9a 100644 --- a/src/StockMarket/data/InitStockMetadata.ts +++ b/src/StockMarket/data/InitStockMetadata.ts @@ -761,8 +761,8 @@ export const InitStockMetadata: IConstructorParams[] = [ min: 6, }, shareTxForMovement: { - max: 84e3, - min: 24e3, + max: 70e3, + min: 20e3, }, symbol: StockSymbols["Sigma Cosmetics"], }, @@ -787,8 +787,8 @@ export const InitStockMetadata: IConstructorParams[] = [ min: 6, }, shareTxForMovement: { - max: 64e3, - min: 18e3, + max: 52e3, + min: 15e3, }, symbol: StockSymbols["Joes Guns"], }, diff --git a/src/StockMarket/ui/StockTickerHeaderText.tsx b/src/StockMarket/ui/StockTickerHeaderText.tsx index 1c1725df7..3227f17aa 100644 --- a/src/StockMarket/ui/StockTickerHeaderText.tsx +++ b/src/StockMarket/ui/StockTickerHeaderText.tsx @@ -27,7 +27,7 @@ export function StockTickerHeaderText(props: IProps): React.ReactElement { let plusOrMinus = stock.b; // True for "+", false for "-" if (stock.otlkMag < 0) { plusOrMinus = !plusOrMinus } hdrText += (plusOrMinus ? "+" : "-").repeat(Math.floor(Math.abs(stock.otlkMag) / 10) + 1); - // hdrText += ` - ${stock.getAbsoluteForecast()} / ${stock.otlkMagForecast}`; + hdrText += ` - ${stock.getAbsoluteForecast()} / ${stock.otlkMagForecast}`; } let styleMarkup = { diff --git a/test/StockMarketTests.js b/test/StockMarketTests.js index 62b2ac90c..fdf7f07ac 100644 --- a/test/StockMarketTests.js +++ b/test/StockMarketTests.js @@ -8,7 +8,7 @@ import { } from "../src/StockMarket/BuyingAndSelling"; import { Order } from "../src/StockMarket/Order"; import { processOrders } from "../src/StockMarket/OrderProcessing"; -import { Stock } from "../src/StockMarket/Stock"; +import { Stock , StockForecastInfluenceLimit } from "../src/StockMarket/Stock"; import { deleteStockMarket, initStockMarket, @@ -40,7 +40,7 @@ describe("Stock Market Tests", function() { b: true, initPrice: 10e3, marketCap: 5e9, - mv: 1, + mv: 2, name: "MockStock", otlkMag: 20, spreadPerc: 1, @@ -309,6 +309,57 @@ describe("Stock Market Tests", function() { expect(stock.getForecastIncreaseChance()).to.equal(0.3); }); }); + + describe("#influenceForecast()", function() { + beforeEach(function() { + stock.otlkMag = 10; + }); + + it("should change the forecast's value towards 50", function() { + stock.influenceForecast(2); + expect(stock.otlkMag).to.equal(8); + }); + + it("should not care about whether the stock is in bull or bear mode", function() { + stock.b = true; + stock.influenceForecast(1); + expect(stock.otlkMag).to.equal(9); + + stock.b = false; + stock.influenceForecast(2); + expect(stock.otlkMag).to.equal(7); + }); + + it("should not influence the forecast beyond the limit", function() { + stock.influenceForecast(10); + expect(stock.otlkMag).to.equal(StockForecastInfluenceLimit); + + stock.influenceForecast(10); + expect(stock.otlkMag).to.equal(StockForecastInfluenceLimit); + }); + }); + + describe("#influenceForecastForecast()", function() { + it("should change the second-order forecast's value towards 50", function() { + stock.otlkMagForecast = 75; + stock.influenceForecastForecast(15); + expect(stock.otlkMagForecast).to.equal(60); + + stock.otlkMagForecast = 25; + stock.influenceForecastForecast(15); + expect(stock.otlkMagForecast).to.equal(40); + }); + + it("should not change the second-order forecast past 50", function() { + stock.otlkMagForecast = 40; + stock.influenceForecastForecast(20); + expect(stock.otlkMagForecast).to.equal(50); + + stock.otlkMagForecast = 60; + stock.influenceForecastForecast(20); + expect(stock.otlkMagForecast).to.equal(50); + }); + }); }); describe("StockMarket object", function() { @@ -508,6 +559,16 @@ describe("Stock Market Tests", function() { return origForecast - forecastChangePerPriceMovement * (n - 1); } + function getNthForecastForecast(origForecastForecast, n) { + if (stock.otlkMagForecast > 50) { + const expected = origForecastForecast - (forecastChangePerPriceMovement * (n - 1) * (stock.mv / 100)); + return expected < 50 ? 50 : expected; + } else if (stock.otlkMagForecast < 50) { + const expected = origForecastForecast + (forecastChangePerPriceMovement * (n - 1) * (stock.mv / 100)); + return expected > 50 ? 50 : expected; + } + } + describe("processTransactionForecastMovement() for buy transactions", function() { const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2); const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares; @@ -547,69 +608,85 @@ describe("Stock Market Tests", function() { it("should properly evaluate LONG transactions that triggers forecast movements", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, mvmtShares, PositionTypes.Long); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 4)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); }); it("should properly evaluate SHORT transactions that triggers forecast movements", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, mvmtShares, PositionTypes.Short); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 4)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); }); it("should properly evaluate LONG transactions of exactly 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, stock.shareTxForMovement, PositionTypes.Long); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 2)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); it("should properly evaluate LONG transactions that total to 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Long); expect(stock.shareTxUntilMovement).to.be.below(stock.shareTxForMovement); processTransactionForecastMovement(stock, stock.shareTxUntilMovement, PositionTypes.Long); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 2)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); it("should properly evaluate LONG transactions that are a multiple of 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Long); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 4)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); it("should properly evaluate SHORT transactions of exactly 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, stock.shareTxForMovement, PositionTypes.Short); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 2)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); it("should properly evaluate SHORT transactions that total to 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Short); expect(stock.shareTxUntilMovement).to.be.below(stock.shareTxForMovement); processTransactionForecastMovement(stock, stock.shareTxUntilMovement, PositionTypes.Short); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 2)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); it("should properly evaluate SHORT transactions that are a multiple of 'shareTxForMovement' shares", function() { const oldForecast = stock.otlkMag; + const oldForecastForecast = stock.otlkMagForecast; processTransactionForecastMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Short); expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4)); + expect(stock.otlkMagForecast).to.equal(getNthForecastForecast(oldForecastForecast, 4)); expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement); }); }); @@ -932,7 +1009,15 @@ describe("Stock Market Tests", function() { }); describe("Order Processing", function() { - // TODO + before(function() { + expect(initStockMarket).to.not.throw(); + expect(initSymbolToStockMap).to.not.throw(); + }); + + describe() + describe("executeOrder()", function() { + + }); }); describe("Player Influencing", function() {