raylu 1 mesiac pred
rodič
commit
0d1f39a451
1 zmenil súbory, kde vykonal 108 pridanie a 0 odobranie
  1. 108 0
      roi.py

+ 108 - 0
roi.py

@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import dataclasses
+import typing
+
+import cache
+
+def main() -> None:
+	recipes: list[Recipe] = cache.get('https://api.prunplanner.org/data/recipes')
+	materials: dict[str, Material] = {m['Ticker']: m for m in cache.get('https://api.prunplanner.org/data/materials')}
+	raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
+	prices: dict[str, Price] = {
+		p['MaterialTicker']: Price(p['VWAP7D'], p['AverageTraded7D']) for p in raw_prices # pyright: ignore[reportArgumentType]
+		if p['ExchangeCode'] == 'IC1' and p['VWAP7D'] is not None
+	}
+
+	profits = []
+	for recipe in recipes:
+		if profit := calc_profit(recipe, materials, prices):
+			profits.append(profit)
+	profits.sort(reverse=True)
+	for p in profits:
+		print(f'{p.recipe:40} {p.profit:10.2f}\033[31m', end='')
+		if p.low_volume:
+			print(' low volume', end='')
+		if p.high_opex:
+			print(' high opex', end='')
+		if p.heavy_logistics:
+			print(' heavy logistics', end='')
+		print('\033[0m')
+
+def calc_profit(recipe: Recipe, materials: typing.Mapping[str, Material], prices: typing.Mapping[str, Price]) -> Profit | None:
+	try:
+		(output,) = recipe['Outputs']
+	except ValueError: # skip recipes that don't have exactly 1 output
+		return
+	try:
+		output_price = prices[output['Ticker']]
+		cost = sum(prices[input['Ticker']].vwap * input['Amount'] for input in recipe['Inputs'])
+	except KeyError: # skip recipes with thinly traded materials
+		return
+	revenue = output_price.vwap * output['Amount']
+	profit_per_run = revenue - cost
+	runs_per_day = 24 * 60 * 60 * 1000 / recipe['TimeMs']
+	cost_per_day = cost * runs_per_day
+	output_per_day = output['Amount'] * runs_per_day
+	logistics_per_day = max(
+		sum(materials[input['Ticker']]['Weight'] * input['Amount'] for input in recipe['Inputs']),
+		sum(materials[input['Ticker']]['Volume'] * input['Amount'] for input in recipe['Inputs']),
+		materials[output['Ticker']]['Weight'] * output['Amount'],
+		materials[output['Ticker']]['Volume'] * output['Amount'],
+	) * runs_per_day
+	return Profit(recipe['RecipeName'], profit=profit_per_run * runs_per_day,
+			low_volume=output_price.average_traded < output_per_day * 20,
+			high_opex=cost_per_day > 100_000,
+			heavy_logistics=logistics_per_day > 100)
+
+class Recipe(typing.TypedDict):
+	RecipeName: str
+	BuildingTicker: str
+	Inputs: list[RecipeMat]
+	Outputs: list[RecipeMat]
+	TimeMs: int
+
+class RecipeMat(typing.TypedDict):
+	Ticker: str
+	Amount: int
+
+class Material(typing.TypedDict):
+	Ticker: str
+	Weight: float
+	Volume: float
+
+class RawPrice(typing.TypedDict):
+	MaterialTicker: str
+	ExchangeCode: str
+	PriceAverage: int
+	VWAP7D: float | None # volume-weighted average price over last 7 days
+	AverageTraded7D: float | None # averaged daily traded volume over last 7 days
+
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
+class Price:
+	vwap: float
+	average_traded: float
+
+@dataclasses.dataclass(eq=False, slots=True)
+class Profit:
+	recipe: str
+	profit: float
+	low_volume: bool
+	high_opex: bool
+	heavy_logistics: bool
+	score: float = dataclasses.field(init=False)
+
+	def __post_init__(self) -> None:
+		self.score = self.profit
+		if self.low_volume:
+			self.score *= 0.2
+		if self.high_opex:
+			self.score *= 0.8
+		if self.heavy_logistics:
+			self.score *= 0.5
+
+	def __lt__(self, other: Profit) -> bool:
+		return self.score < other.score
+
+if __name__ == '__main__':
+	main()