Bladeren bron

buy webpage

raylu 11 uur geleden
bovenliggende
commit
1c89938b5c
7 gewijzigde bestanden met toevoegingen van 285 en 14 verwijderingen
  1. 1 0
      .gitignore
  2. 1 1
      package.json
  3. 213 0
      ts/buy.ts
  4. 39 0
      www/buy.html
  5. 2 1
      www/index.html
  6. 1 1
      www/roi.html
  7. 28 11
      www/style.css

+ 1 - 0
.gitignore

@@ -3,4 +3,5 @@ cache/
 config.toml
 node_modules/
 uv.lock
+www/buy.js*
 www/roi.js*

+ 1 - 1
package.json

@@ -3,7 +3,7 @@
 	"version": "0",
 	"description": "GitHub Pages site with TypeScript",
 	"scripts": {
-		"build": "bun build ts/roi.ts --outdir www --target browser --sourcemap=external",
+		"build": "bun build ts/buy.ts ts/roi.ts --outdir www --target browser --sourcemap=external",
 		"typecheck": "tsgo --noEmit",
 		"serve": "python3 -m http.server -d www 8000"
 	},

+ 213 - 0
ts/buy.ts

@@ -0,0 +1,213 @@
+document.querySelector("#fetch")!.addEventListener("click", async () => {
+	const username = (document.querySelector("#username") as HTMLInputElement).value;
+	const apiKey = (document.querySelector("#api-key") as HTMLInputElement).value;
+	const supplyForDays = parseInt((document.querySelector("#days") as HTMLInputElement).value, 10);
+	const output = await calculate(username, apiKey, supplyForDays);
+	console.log(output);
+});
+
+async function calculate(username: string, apiKey: string, supplyForDays: number): Promise<void> {
+	const [prices, planets, warehouse, {bids, orders}] = await Promise.all([
+		getPrices(),
+		getPlanets(username, apiKey),
+		warehouseInventory(username, apiKey),
+		getBids(username, apiKey)
+	]);
+	const buy = new Map<string, number>();
+	for (const planet of planets)
+		for (const [mat, amount] of planet.supplyForDays(supplyForDays))
+			buy.set(mat, (buy.get(mat) ?? 0) + amount);
+
+	// what's left to buy
+	const materials: Material[] = [];
+	for (const [mat, amount] of buy) {
+		const remaining = Math.max(amount - (bids.get(mat) ?? 0) - (warehouse.get(mat) ?? 0), 0);
+		const price = prices.get(mat);
+		if (!price || price.Bid === null || price.Ask === null) {
+			console.log(mat, 'has no bid/ask');
+			continue;
+		}
+		const spread = price.Ask - price.Bid;
+		materials.push({
+			ticker: mat,
+			amount,
+			bids: bids.get(mat) ?? 0,
+			warehouse: warehouse.get(mat) ?? 0,
+			spread,
+			savings: spread * remaining,
+		});
+	}
+	materials.sort((a, b) => b.savings - a.savings);
+
+	const tbody = document.querySelector("tbody")!;
+	tbody.innerHTML = '';
+	const format= new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
+	for (const m of materials) {
+		const tr = document.createElement("tr");
+		const buyAmount = Math.max(m.amount - m.bids - m.warehouse, 0);
+		tr.innerHTML = `
+			<td>${m.ticker}</td>
+			<td>${format(m.amount)}</td>
+			<td>${format(m.bids)}</td>
+			<td>${format(m.warehouse)}</td>
+			<td>${format(buyAmount)}</td>
+			<td>${format(m.spread)}</td>
+			<td>${format(m.savings)}</td>
+		`;
+		tbody.appendChild(tr);
+	}
+
+	// deposits of current bids
+	orders.sort((a, b) => (b.Limit * b.Amount) - (a.Limit * a.Amount));
+	for (const order of orders) {
+		const deposit = order.Limit * order.Amount;
+		console.log(`${order.MaterialTicker.padEnd(4)} ${deposit}\n`);
+	}
+}
+
+async function getPrices() {
+	const rawPrices= await fetch('https://refined-prun.github.io/refined-prices/all.json').then(r => r.json());
+	const prices = new Map<string, RawPrice>();
+	for (const p of rawPrices)
+		if (p.ExchangeCode === 'IC1')
+			prices.set(p.MaterialTicker, p);
+	return prices;
+}
+
+async function getPlanets(username: string, apiKey: string) {
+	const fioBurns: FIOBurn[] = await fetch('https://rest.fnar.net/fioweb/burn/user/' + username,
+		{headers: {'Authorization': apiKey}}).then(r => r.json());
+	const planets = fioBurns.map(burn => new Planet(burn));
+	return planets;
+}
+
+async function warehouseInventory(username: string, apiKey: string): Promise<Map<string, number>> {
+	const warehouses: Warehouse[] = await fetch('https://rest.fnar.net/sites/warehouses/' + username,
+		{headers: {'Authorization': apiKey}}).then(r => r.json());
+	
+	for (const warehouse of warehouses)
+		if (warehouse.LocationNaturalId === 'HRT') {
+			const storage: Storage = await fetch(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`,
+				{headers: {'Authorization': apiKey}}).then(r => r.json());
+			const inventory = new Map<string, number>();
+			for (const item of storage.StorageItems)
+				inventory.set(item.MaterialTicker, item.MaterialAmount);
+			return inventory;
+		}
+	throw new Error("couldn't find HRT warehouse");
+}
+
+async function getBids(username: string, apiKey: string) {
+	const allOrders: ExchangeOrder[] = await fetch('https://rest.fnar.net/cxos/' + username,
+		{headers: {'Authorization': apiKey}}).then(r => r.json());
+	const orders = allOrders.filter(order =>
+			order.OrderType === 'BUYING' && order.Status !== 'FILLED' && order.ExchangeCode === 'IC1');
+
+	const bids = new Map<string, number>();
+	for (const order of orders)
+		bids.set(order.MaterialTicker, (bids.get(order.MaterialTicker) ?? 0) + order.Amount);
+	return {bids, orders};
+}
+
+class Planet {
+	name: string;
+	inventory: Map<string, number>;
+	netConsumption: Amount[];
+
+	constructor(fioBurn: FIOBurn) {
+		this.name = fioBurn.PlanetName || fioBurn.PlanetNaturalId;
+		this.inventory = new Map();
+		for (const item of fioBurn.Inventory)
+			this.inventory.set(item.MaterialTicker, item.MaterialAmount);
+
+		const producing = new Map<string, Amount>();
+		for (const item of fioBurn.OrderProduction)
+			producing.set(item.MaterialTicker, item);
+
+		this.netConsumption = [];
+		for (const c of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption]) {
+			let net = c.DailyAmount;
+			const production = producing.get(c.MaterialTicker);
+			if (production) {
+				net -= production.DailyAmount;
+				if (net < 0)
+					continue;
+			}
+			c.netConsumption = net;
+			this.netConsumption.push(c);
+		}
+	}
+
+	supplyForDays(targetDays: number): Map<string, number> {
+		const buy = new Map<string, number>();
+		for (const consumption of this.netConsumption) {
+			const ticker = consumption.MaterialTicker;
+			const avail = this.inventory.get(ticker) ?? 0;
+			const dailyConsumption = consumption.netConsumption!;
+			const days = avail / dailyConsumption;
+			if (days < targetDays)
+				buy.set(ticker, Math.ceil((targetDays - days) * dailyConsumption));
+		}
+		return buy;
+	}
+}
+
+interface Material {
+	ticker: string;
+	amount: number;
+	bids: number;
+	warehouse: number;
+	spread: number;
+	savings: number;
+}
+
+interface RawPrice {
+	MaterialTicker: string;
+	ExchangeCode: string;
+	Bid: number | null;
+	Ask: number | null;
+	FullTicker: string;
+}
+
+interface ExchangeOrder {
+	MaterialTicker: string;
+	ExchangeCode: string;
+	OrderType: 'SELLING' | 'BUYING';
+	Status: 'FILLED' | 'PARTIALLY_FILLED';
+	Amount: number;
+	Limit: number;
+}
+
+interface StorageItem {
+	MaterialTicker: string;
+	MaterialAmount: number;
+}
+
+interface Storage {
+	Name: string;
+	StorageItems: StorageItem[];
+	WeightLoad: number;
+	VolumeLoad: number;
+	Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE';
+}
+
+interface Warehouse {
+	StoreId: string;
+	LocationNaturalId: string;
+}
+
+interface Amount {
+	MaterialTicker: string;
+	DailyAmount: number;
+	netConsumption?: number;
+}
+
+interface FIOBurn {
+	PlanetName: string;
+	PlanetNaturalId: string;
+	Error: any;
+	OrderConsumption: Amount[];
+	WorkforceConsumption: Amount[];
+	Inventory: StorageItem[];
+	OrderProduction: Amount[];
+}

+ 39 - 0
www/buy.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>PrUn buy</title>
+	<link rel="stylesheet" type="text/css" href="style.css">
+	<link rel="icon" href="https://www.raylu.net/hammer-man.svg" />
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<meta name="theme-color" content="#222">
+</head>
+<body>
+	<a href="/">← back</a>
+	<main>
+		<form>
+			<label>FIO username: <input type="text" id="username"></label>
+			<label>FIO API key: <input type="text" size="30" id="api-key"></label>
+			<label>supply for <input type="number" size="2" value="7" id="days"> days</label>
+			<label>CX <input type="text" id="cx" value="IC1" size="3" disabled></label>
+			<input type="button" value="fetch" id="fetch">
+		</form>
+		<table class="buy">
+			<thead>
+				<tr>
+					<th>mat</th>
+					<th data-tooltip="needed to supply all planets">want</th>
+					<th>bids</th>
+					<th data-tooltip="in warehouse">have</th>
+					<th data-tooltip="want - bids - have">buy</th>
+					<th data-tooltip="ask - bid">spread</th>
+					<th data-tooltip="buy × spread">savings</th>
+				</tr>
+			</thead>
+			<tbody></tbody>
+		</table>
+	</main>
+	<div id="popover" popover="hint"></div>
+	<script src="buy.js"></script>
+</body>
+</html>

+ 2 - 1
www/index.html

@@ -10,7 +10,8 @@
 </head>
 <body>
 	<main>
-		<a href="roi.html">RoI</a>
+		<p><a href="roi.html">RoI</a></p>
+		<p><a href="buy.html">buy</a></p>
 	</main>
 </body>
 </html>

+ 1 - 1
www/roi.html

@@ -12,7 +12,7 @@
 	<a href="/">← back</a>
 	<main>
         <label><input type="checkbox" id="low-volume">show low volume</label>
-		<table>
+		<table class="roi">
 			<thead>
 				<tr>
 					<th>wrought<br>product</th>

+ 28 - 11
www/style.css

@@ -23,8 +23,19 @@ a:hover {
 	color: #5ad;
 }
 
+form {
+	display: flex;
+	flex-direction: column;
+	align-items: flex-start;
+}
 input {
 	background-color: #111;
+	color: inherit;
+	font-family: inherit;
+	padding: 4px;
+	border: 1px solid #888;
+}
+input[type="checkbox"] {
 	accent-color: #f70;
 }
 
@@ -49,17 +60,23 @@ table {
 	th {
 		white-space: no-wrap;
 	}
-	tbody {
-		td:nth-child(1),
-		td:nth-child(2) {
-			font-family: inherit;
-			text-align: inherit;
-		}
-		td {
-			font-family: monospace;
-			text-align: right;
-			border-top: 1px solid #222;
-		}
+	td {
+		font-family: monospace;
+		text-align: right;
+		border-top: 1px solid #222;
+	}
+}
+table.buy {
+	td:nth-child(1) {
+		font-family: inherit;
+		text-align: inherit;
+	}
+}
+table.roi {
+	td:nth-child(1),
+	td:nth-child(2) {
+		font-family: inherit;
+		text-align: inherit;
 	}
 }