Stocks 4S no-api automation
Automation of stock management using forecast (cost 1b) without 4S API access bought (cost 25b)unknown
javascript
2 years ago
5.8 kB
114
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...