Stocks 4S no-api automation
Automation of stock management using forecast (cost 1b) without 4S API access bought (cost 25b)unknown
javascript
a year ago
5.8 kB
102
Indexable
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; }
Editor is loading...