|
|
@@ -18,8 +18,9 @@ def main() -> None:
|
|
|
|
|
|
def calc_for_cx(cx: str, recipes: typing.Collection[Recipe], buildings: typing.Mapping[str, Building],
|
|
|
materials: typing.Mapping[str, Material], raw_prices: typing.Collection[RawPrice]) -> typing.Sequence[Profit]:
|
|
|
+ # EXTREME DETAIL: We extract 'Bid' and 'Ask' directly from the raw_prices API payload.
|
|
|
prices: dict[str, Price] = {
|
|
|
- p['MaterialTicker']: Price(p['VWAP7D'], p['AverageTraded7D'], p['VWAP30D']) for p in raw_prices # pyright: ignore[reportArgumentType]
|
|
|
+ p['MaterialTicker']: Price(p['VWAP7D'], p['AverageTraded7D'], p['VWAP30D'], p['Bid'], p['Ask']) for p in raw_prices # pyright: ignore[reportArgumentType]
|
|
|
if p['ExchangeCode'] == cx
|
|
|
}
|
|
|
habitation: typing.Mapping[Worker, str] = {
|
|
|
@@ -30,10 +31,11 @@ def calc_for_cx(cx: str, recipes: typing.Collection[Recipe], buildings: typing.M
|
|
|
'scientists': 'HB5',
|
|
|
}
|
|
|
hab_area_cost: dict[Worker, float] = {}
|
|
|
- hab_capex: dict[Worker, float] = {}
|
|
|
+ hab_capex: dict[Worker, dict[str, float]] = {}
|
|
|
for worker, hab in habitation.items():
|
|
|
hab_area_cost[worker] = buildings[hab]['area_cost'] / 100
|
|
|
- hab_capex[worker] = building_construction_cost(buildings[hab], prices) / 100
|
|
|
+ base_capex = building_construction_cost(buildings[hab], prices)
|
|
|
+ hab_capex[worker] = {k: v / 100 for k, v in base_capex.items()}
|
|
|
|
|
|
profits: list[Profit] = []
|
|
|
for recipe in recipes:
|
|
|
@@ -42,57 +44,64 @@ def calc_for_cx(cx: str, recipes: typing.Collection[Recipe], buildings: typing.M
|
|
|
profits.sort()
|
|
|
return profits
|
|
|
|
|
|
+def get_metrics(amount: float, price: Price) -> dict[str, float]:
|
|
|
+ # EXTREME DETAIL: Helper function to generate a 3-part dictionary (VWAP, Bid, Ask) for any item quantity.
|
|
|
+ # If Bid or Ask data is absent from the exchange, we gracefully fall back to the VWAP to prevent math crashes.
|
|
|
+ v = price.vwap_7d or price.vwap_30d or 0.0
|
|
|
+ b = price.bid if price.bid is not None else v
|
|
|
+ a = price.ask if price.ask is not None else v
|
|
|
+ return {'vwap': amount * v, 'bid': amount * b, 'ask': amount * a}
|
|
|
+
|
|
|
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],
|
|
|
+ hab_capex: typing.Mapping[Worker, dict[str, float]], materials: typing.Mapping[str, Material],
|
|
|
prices: typing.Mapping[str, Price]) -> Profit | None:
|
|
|
if len(recipe['outputs']) == 0:
|
|
|
return
|
|
|
|
|
|
+ building = buildings[recipe['building_ticker']]
|
|
|
+ area = building['area_cost'] + sum(hab_area_cost[worker] * building[worker] for worker in hab_area_cost)
|
|
|
+ runs_per_day = 24 * 60 * 60 * 1000 / recipe['time_ms'] * 1.25 # assume CoGC
|
|
|
+ if building['building_ticker'] in ('FRM', 'ORC'):
|
|
|
+ runs_per_day *= 1.1212 # promitor's fertility
|
|
|
+
|
|
|
outputs: list[MatPrice] = []
|
|
|
- revenue = 0
|
|
|
+ revenue = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
|
|
|
output_prices: dict[str, PriceNonNull] = {}
|
|
|
for output in recipe['outputs']:
|
|
|
price = prices[output['material_ticker']]
|
|
|
if price.vwap_7d is None or price.average_traded_7d is None:
|
|
|
return # skip recipes with thinly traded outputs
|
|
|
output_prices[output['material_ticker']] = typing.cast(PriceNonNull, price)
|
|
|
- outputs.append(MatPrice(output['material_ticker'], output['material_amount'], price.vwap_7d))
|
|
|
- revenue += price.vwap_7d * output['material_amount']
|
|
|
+
|
|
|
+ # EXTREME DETAIL: Calculate total daily revenue outputs per metric type.
|
|
|
+ m = get_metrics(output['material_amount'] * runs_per_day, price)
|
|
|
+ for k in revenue: revenue[k] += m[k]
|
|
|
+ outputs.append(MatPrice(output['material_ticker'], output['material_amount'], price.vwap_7d, price.bid, price.ask))
|
|
|
|
|
|
input_costs: list[MatPrice] = []
|
|
|
- cost = 0
|
|
|
+ opex = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
|
|
|
for input in recipe['inputs']:
|
|
|
- if (input_cost := prices[input['material_ticker']].vwap_7d) is None:
|
|
|
+ price = prices[input['material_ticker']]
|
|
|
+ if price.vwap_7d is None:
|
|
|
return # skip recipes with thinly traded inputs
|
|
|
- input_costs.append(MatPrice(input['material_ticker'], input['material_amount'], input_cost))
|
|
|
- cost += input_cost * input['material_amount']
|
|
|
- profit_per_run = revenue - cost
|
|
|
+
|
|
|
+ m = get_metrics(input['material_amount'] * runs_per_day, price)
|
|
|
+ for k in opex: opex[k] += m[k]
|
|
|
+ input_costs.append(MatPrice(input['material_ticker'], input['material_amount'], price.vwap_7d, price.bid, price.ask))
|
|
|
|
|
|
- building = buildings[recipe['building_ticker']]
|
|
|
- area = building['area_cost'] + 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)
|
|
|
- runs_per_day = 24 * 60 * 60 * 1000 / recipe['time_ms'] * 1.25 # assume CoGC
|
|
|
- if building['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
|
|
|
-
|
|
|
- profit_per_day = profit_per_run * runs_per_day - worker_consumable_daily_cost
|
|
|
- if profit_per_day > 0:
|
|
|
- break_even = (capex + 3 * cost_per_day) / profit_per_day
|
|
|
- else:
|
|
|
- break_even = 10000 - profit_per_day
|
|
|
-
|
|
|
- profit_per_area = profit_per_day / area
|
|
|
+ worker_consumable = building_daily_cost(building, prices)
|
|
|
+ for k in opex: opex[k] += worker_consumable[k]
|
|
|
+
|
|
|
+ capex = building_construction_cost(building, prices)
|
|
|
+ for worker, hab_cost in hab_capex.items():
|
|
|
+ workers = building[worker]
|
|
|
+ if workers > 0:
|
|
|
+ for k in capex: capex[k] += hab_cost[k] * workers
|
|
|
|
|
|
lowest_liquidity = min(recipe['outputs'],
|
|
|
key=lambda output: output['material_amount'] / output_prices[output['material_ticker']].average_traded_7d)
|
|
|
output_per_day = lowest_liquidity['material_amount'] * runs_per_day
|
|
|
-
|
|
|
- # EXTREME DETAIL: We calculate the Market Capacity in Areas here.
|
|
|
- # By dividing the 7-day average traded volume by the daily production yield of a single area,
|
|
|
- # we derive how many 'areas' worth of production the market can absorb.
|
|
|
+
|
|
|
average_traded_7d = output_prices[lowest_liquidity['material_ticker']].average_traded_7d
|
|
|
output_per_area = output_per_day / area
|
|
|
market_capacity_area = average_traded_7d / output_per_area
|
|
|
@@ -107,27 +116,28 @@ def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], hab_ar
|
|
|
return Profit(outputs, recipe['recipe_name'],
|
|
|
expertise=building['expertise'],
|
|
|
building=building['building_ticker'],
|
|
|
- profit_per_day=profit_per_day,
|
|
|
area=area,
|
|
|
capex=capex,
|
|
|
- cost_per_day=cost_per_day,
|
|
|
+ opex=opex,
|
|
|
+ revenue=revenue,
|
|
|
input_costs=input_costs,
|
|
|
- worker_consumable_cost_per_day=worker_consumable_daily_cost,
|
|
|
runs_per_day=runs_per_day,
|
|
|
logistics_per_area=logistics_per_area,
|
|
|
output_per_day=output_per_day,
|
|
|
average_traded_7d=average_traded_7d,
|
|
|
- profit_per_area=profit_per_area,
|
|
|
- break_even=break_even,
|
|
|
market_capacity_area=market_capacity_area)
|
|
|
|
|
|
-def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
|
|
|
- cost = sum(bc['material_amount'] * prices[bc['material_ticker']].vwap_7d for bc in building['costs']) # pyright: ignore[reportOperatorIssue]
|
|
|
+def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> dict[str, float]:
|
|
|
+ cost = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
|
|
|
+ for bc in building['costs']:
|
|
|
+ m = get_metrics(bc['material_amount'], prices[bc['material_ticker']])
|
|
|
+ for k in cost: cost[k] += m[k]
|
|
|
# https://handbook.apex.prosperousuniverse.com/wiki/building-costs/#rocky-planets
|
|
|
- cost += building['area_cost'] * 4 * prices['MCG'].vwap_7d # pyright: ignore[reportOperatorIssue]
|
|
|
+ mcg = get_metrics(building['area_cost'] * 4, prices['MCG'])
|
|
|
+ for k in cost: cost[k] += mcg[k]
|
|
|
return cost
|
|
|
|
|
|
-def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
|
|
|
+def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> dict[str, 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)],
|
|
|
@@ -135,12 +145,12 @@ def building_daily_cost(building: Building, prices: typing.Mapping[str, Price])
|
|
|
'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.05)],
|
|
|
}
|
|
|
- cost = 0
|
|
|
+ cost = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
|
|
|
for worker, mats in consumption.items():
|
|
|
workers = building[worker]
|
|
|
for mat, per_100 in mats:
|
|
|
- mat_price = prices[mat]
|
|
|
- cost += (mat_price.vwap_7d or mat_price.vwap_30d) * workers * per_100 / 100
|
|
|
+ m = get_metrics(workers * per_100 / 100, prices[mat])
|
|
|
+ for k in cost: cost[k] += m[k]
|
|
|
return cost
|
|
|
|
|
|
Worker = typing.Literal['pioneers', 'settlers', 'technicians', 'engineers', 'scientists']
|
|
|
@@ -179,15 +189,19 @@ class Material(typing.TypedDict):
|
|
|
class RawPrice(typing.TypedDict):
|
|
|
MaterialTicker: str
|
|
|
ExchangeCode: str
|
|
|
- VWAP7D: float | None # volume-weighted average price over last 7 days
|
|
|
- AverageTraded7D: float | None # averaged daily traded volume over last 7 days
|
|
|
+ VWAP7D: float | None
|
|
|
+ AverageTraded7D: float | None
|
|
|
VWAP30D: float | None
|
|
|
+ Bid: float | None # Added Bid extraction
|
|
|
+ Ask: float | None # Added Ask extraction
|
|
|
|
|
|
@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class Price:
|
|
|
vwap_7d: float | None
|
|
|
average_traded_7d: float | None
|
|
|
vwap_30d: float | None
|
|
|
+ bid: float | None
|
|
|
+ ask: float | None
|
|
|
|
|
|
@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class PriceNonNull:
|
|
|
@@ -200,28 +214,36 @@ class Profit:
|
|
|
recipe: str
|
|
|
expertise: str
|
|
|
building: str
|
|
|
- profit_per_day: float
|
|
|
area: float
|
|
|
- capex: float
|
|
|
- cost_per_day: float
|
|
|
+ capex: dict[str, float] # Transformed from float to Dict
|
|
|
+ opex: dict[str, float] # Transformed from float to Dict
|
|
|
+ revenue: dict[str, float] # Transformed from float to Dict
|
|
|
input_costs: typing.Collection[MatPrice]
|
|
|
- worker_consumable_cost_per_day: float
|
|
|
runs_per_day: float
|
|
|
logistics_per_area: float
|
|
|
output_per_day: float
|
|
|
average_traded_7d: float
|
|
|
- profit_per_area: float
|
|
|
- break_even: float
|
|
|
- market_capacity_area: float # Added pre-calculated property
|
|
|
+ market_capacity_area: float
|
|
|
|
|
|
def __lt__(self, other: Profit) -> bool:
|
|
|
- return self.break_even < other.break_even
|
|
|
+ # EXTREME DETAIL: We establish a baseline VWAP sort for the raw JSON payload.
|
|
|
+ # Even though the frontend now dynamically resorts based on UI dropdown permutations,
|
|
|
+ # sorting the initial JSON correctly saves the client from experiencing a 'pop-in' rearrangement
|
|
|
+ # on their very first page load.
|
|
|
+ p_a = self.revenue['vwap'] - self.opex['vwap']
|
|
|
+ be_a = (self.capex['vwap'] + 3 * self.opex['vwap']) / p_a if p_a > 0 else 10000 - p_a
|
|
|
+
|
|
|
+ p_b = other.revenue['vwap'] - other.opex['vwap']
|
|
|
+ be_b = (other.capex['vwap'] + 3 * other.opex['vwap']) / p_b if p_b > 0 else 10000 - p_b
|
|
|
+ return be_a < be_b
|
|
|
|
|
|
@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class MatPrice:
|
|
|
ticker: str
|
|
|
amount: int
|
|
|
vwap_7d: float
|
|
|
+ bid: float | None
|
|
|
+ ask: float | None
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
main()
|