| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- from __future__ import annotations
- import dataclasses
- import json
- import typing
- import cache
- def main() -> None:
- recipes: list[Recipe] = cache.get('https://api.prunplanner.org/data/recipes')
- buildings: dict[str, Building] = {m['Ticker']: m for m in cache.get('https://api.prunplanner.org/data/buildings')}
- 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
- }
- habitation: typing.Mapping[Worker, str] = {
- 'Pioneers': 'HB1',
- 'Settlers': 'HB2',
- 'Technicians': 'HB3',
- 'Engineers': 'HB4',
- 'Scientists': 'HB5',
- }
- hab_area_cost: dict[Worker, float] = {}
- hab_capex: dict[Worker, float] = {}
- for worker, hab in habitation.items():
- hab_area_cost[worker] = buildings[hab]['AreaCost'] / 100
- hab_capex[worker] = building_construction_cost(buildings[hab], prices) / 100
- profits: list[Profit] = []
- for recipe in recipes:
- if profit := calc_profit(recipe, buildings, hab_area_cost, hab_capex, materials, prices):
- profits.append(profit)
- profits.sort()
- with open('www/roi.json', 'w') as f:
- json.dump([dataclasses.asdict(p) for p in profits], f, indent='\t')
- def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], hab_area_cost: typing.Mapping[Worker, float],
- hab_capex: typing.Mapping[Worker, float], 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']
- building = buildings[recipe['BuildingTicker']]
- area = building['AreaCost'] + sum(hab_area_cost[worker] * building[worker] for worker in hab_area_cost)
- capex = building_construction_cost(building, prices) + \
- sum(hab_capex[worker] * building[worker] for worker in hab_capex)
- profit_per_run = revenue - cost
- runs_per_day = 24 * 60 * 60 * 1000 / recipe['TimeMs']
- if building['Ticker'] in ('FRM', 'ORC'):
- runs_per_day *= 1.1212 # promitor's fertility
- worker_consumable_daily_cost = building_daily_cost(building, prices)
- cost_per_day = cost * runs_per_day + worker_consumable_daily_cost
- output_per_day = output['Amount'] * runs_per_day
- logistics_per_area = 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 / area
- return Profit(output['Ticker'], recipe['RecipeName'],
- expertise=building['Expertise'].replace('_', ' ').lower(),
- profit_per_day=(profit_per_run * runs_per_day - worker_consumable_daily_cost),
- area=area,
- capex=capex,
- cost_per_day=cost_per_day,
- logistics_per_area=logistics_per_area,
- output_per_day=output_per_day,
- average_traded_7d=output_price.average_traded_7d)
- def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
- return sum(bc['Amount'] * prices[bc['CommodityTicker']].vwap for bc in building['BuildingCosts'])
- def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
- consumption = {
- 'Pioneers': [('COF', 0.5), ('DW', 4), ('RAT', 4), ('OVE', 0.5), ('PWO', 0.2)],
- 'Settlers': [('DW', 5), ('RAT', 6), ('KOM', 1), ('EXO', 0.5), ('REP', 0.2), ('PT', 0.5)],
- 'Technicians': [('DW', 7.5), ('RAT', 7), ('ALE', 1), ('MED', 0.5), ('SC', 0.1), ('HMS', 0.5), ('SCN', 0.1)],
- 'Engineers': [('DW', 10), ('MED', 0.5), ('GIN', 1), ('FIM', 7), ('VG', 0.2), ('HSS', 0.2), ('PDA', 0.1)],
- 'Scientists': [('DW', 10), ('MED', 0.5), ('WIN', 1), ('MEA', 7), ('NST', 0.1), ('LC', 0.2), ('WS', 0.1)],
- }
- cost = 0
- for worker, mats in consumption.items():
- workers = building[worker]
- for mat, per_100 in mats:
- cost += prices[mat].vwap * workers * per_100 / 100
- return cost
- Worker = typing.Literal['Pioneers', 'Settlers', 'Technicians', 'Engineers', 'Scientists']
- 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 Building(typing.TypedDict):
- Ticker: str
- Expertise: str
- AreaCost: int
- BuildingCosts: list[BuildingMat]
- Pioneers: int
- Settlers: int
- Technicians: int
- Engineers: int
- Scientists: int
- class BuildingMat(typing.TypedDict):
- CommodityTicker: 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_7d: float
- @dataclasses.dataclass(eq=False, frozen=True, slots=True)
- class Profit:
- output: str
- recipe: str
- expertise: str
- profit_per_day: float
- area: float
- capex: float
- cost_per_day: float
- logistics_per_area: float
- output_per_day: float
- average_traded_7d: float
- def __lt__(self, other: Profit) -> bool:
- if (break_even := self.capex / self.profit_per_day) < 0:
- break_even = 10000 - self.profit_per_day
- if (other_break_even := other.capex / other.profit_per_day) < 0:
- other_break_even = 10000 - other.profit_per_day
- return break_even < other_break_even
- if __name__ == '__main__':
- main()
|