Stocks 4S no-api automation

Automation of stock management using forecast (cost 1b) without 4S API access bought (cost 25b)
mail@pastecode.io avatar
unknown
javascript
7 months ago
5.8 kB
77
Indexable
Never
const DOC = eval("document");
const STOCK_XPATH = "//p[contains(text(), 'Price Forecast')]"; // stock line recognition, assuming we unlocked 4S (just info, not API)
const MINIMUM_INVESTMENTS = 2000000;
const RESERVE = 1000000;
const LOG = [];
const LOG_SIZE = 20;
const LAST_FORECAST = {}

/** @param {NS} ns */
export async function main(ns) {
  ns.disableLog('ALL');
  ns.tail();

  while (true) {
    await ns.sleep(250);
    ns.clearLog();

    // read stocks from html
    let labels = find(STOCK_XPATH).map(el => el.innerText).map(text => parseStock(text));
    // if no labels, we are probably not on good page, so open stock market
    if (!labels.length) {
      await moveToMarket(ns); continue;
    }

    // get open positions
    let positions = labels.map(lbl => getPosition(ns, lbl.sym)).filter(pos => pos !== null);

    // sell if forecast changes
    positions.forEach(pos => {
      let forecast = labels.find(lbl => lbl.sym === pos.sym).forecast;
      let profit = `${ns.formatNumber(ns.stock.getSaleGain(pos.sym, pos.amt, pos.pos) - (pos.amt * pos.prc))}`
      if (forecast > 0 && pos.pos === 'Short') {
        let prc = ns.stock.sellShort(pos.sym, pos.amt);
        log(`${pos.sym} ${pos.pos} sell ${pos.amt} @ ${ns.formatNumber(prc)} profit: ${profit}`);
      }
      if (forecast < 0 && pos.pos === 'Long') {
        let prc = ns.stock.sellStock(pos.sym, pos.amt);
        log(`${pos.sym} ${pos.pos} sell ${pos.amt} @ ${ns.formatNumber(prc)} profit: ${profit}`);
      }
    });

    // find something to invest in
    let candidates = labels
      .filter(lbl => Math.abs(lbl.forecast) >= 2) // only with forecast --/++ or better
      .filter(lbl => ns.stock.getMaxShares(lbl.sym) - (positions.find(pos => pos.sym === lbl.sym)?.amt || 0) > 0) // only ones that have available stocks to buy
      .filter(lbl => (Date.now() - LAST_FORECAST[lbl.sym].time) < 1000 * 60 * 5) // change is newer than 2 minutes
      .sort((a, b) => b.volatility - a.volatility)
      .slice(0, 10); //top 10
    ns.print(`Candidates to buy: ${candidates.map(lbl => lbl.sym).join(', ')}`);
    if (budget(ns) > MINIMUM_INVESTMENTS && candidates.length > 0) {
      buyStock(ns, candidates[0].sym);
    }

    // print operation log
    ns.print('---'); LOG.forEach(entry => ns.print(entry));
    // print warning
    if (budget(ns) > MINIMUM_INVESTMENTS) { ns.print('---'); ns.print(`WARN Money ready to invest! ${ns.formatNumber(budget(ns))}`); }
    // print orders
    ns.print('---'); positions.forEach(pos => ns.print(`${pos.sym.padEnd(7)}${pos.pos.padEnd(7)}${ns.formatNumber(pos.amt * pos.prc).padEnd(11)}+${ns.formatNumber(ns.stock.getSaleGain(pos.sym, pos.amt, pos.pos) - (pos.amt * pos.prc))}`));
    // print labels
    // ns.print('---'); labels.forEach(lbl => ns.print(`${lbl.sym.padEnd(7)}${lbl.price.padEnd(11)}${('' + lbl.volatility).padEnd(8)}${('' + lbl.forecast).padStart(2, '+')}`));
  }
}
/** @param {NS} ns */
function buyStock(ns, sym) {
  let isLong = LAST_FORECAST[sym].value > 0;
  let buyAmt = getMaxStockBuyAmount(ns, sym, isLong);
  if (buyAmt.amt > 0) {
    let prc = isLong ? ns.stock.buyStock(sym, buyAmt.amt) : ns.stock.buyShort(sym, buyAmt.amt);
    if (prc > 0)
      log(`${sym} ${isLong ? 'Long' : 'Short'} buy ${buyAmt.amt} @ ${ns.formatNumber(buyAmt.prc)}`);
    else log(`ERROR Tried to buy ${isLong ? 'Long' : 'Short'} ${sym}, but failed`);
  }
}

function log(text) { LOG.push(`${timeTxt()} ${text}`); (LOG.length > LOG_SIZE) && LOG.shift(); }

/** extracts data from page element 
 * @param {string} text*/
function parseStock(text) {
  const texts = text.split(/\s+/);
  const len = texts.length;
  const forecast = (texts[len - 1].startsWith("+") ? 1 : -1) * texts[len - 1].length;
  const sym = texts[len - 10];
  updateLastForecast(sym, forecast);
  return {
    sym: sym,
    price: texts[len - 8],
    volatility: 1.0 * (texts[len - 5].slice(0, -1).replace(',', '.')),
    forecast: forecast
  }
}

function updateLastForecast(sym, forecast) {
  if (!LAST_FORECAST[sym]) {
    LAST_FORECAST[sym] = { value: forecast, time: 0 };
  } else if (LAST_FORECAST[sym].value !== forecast) {
    LAST_FORECAST[sym] = { value: forecast, time: Date.now() };
  }
}

function getPosition(ns, sym) {
  let pos = ns.stock.getPosition(sym);
  if (pos[0] > 0) return { sym: sym, pos: 'Long', amt: pos[0], prc: pos[1] };
  if (pos[2] > 0) return { sym: sym, pos: 'Short', amt: pos[2], prc: pos[3] };
  return null;
}

/** @param {NS} ns @param {string} sym @param {boolean} isLong */
function getMaxStockBuyAmount(ns, sym, isLong) {
  let price = isLong ? ns.stock.getAskPrice(sym) : ns.stock.getBidPrice(sym);
  if (budget(ns) < MINIMUM_INVESTMENTS) return { amt: 0, prc: 0 };
  let maxStocks = Math.min(Math.floor(budget(ns) / price), ns.stock.getMaxShares(sym));
  // log(`DEBUG maxAmountToBuy: ${sym} ${isLong} ${maxStocks} ${price}`);
  return { amt: maxStocks, prc: price };
}

function budget(ns) { return ns.getServerMoneyAvailable('home') - RESERVE; }

/** @param {NS} ns */
async function moveToMarket(ns) {
  ns.singularity.goToLocation("World Stock Exchange");
}

function timeTxt() { let d = new Date(); let h = `${d.getHours()}`.padStart(2, '0'); let m = `${d.getMinutes()}`.padStart(2, '0'); return `${h}:${m}` }
function findSingle(xpath) { return DOC.evaluate(xpath, DOC, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; }
function find(xpath) {
  const snapshot = DOC.evaluate(xpath, DOC, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  let results = [];
  for (let i = 0, length = snapshot.snapshotLength; i < length; i++) {
    results.push(snapshot.snapshotItem(i));
  }
  return results;
}