|
|
@@ -39,45 +39,61 @@ def main() -> None:
|
|
|
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
|
|
|
- output_price = prices[output['Ticker']]
|
|
|
- if output_price.vwap_7d is None or output_price.average_traded_7d is None: # skip recipes with thinly traded output
|
|
|
+ if len(recipe['Outputs']) == 0:
|
|
|
return
|
|
|
+
|
|
|
+ outputs: list[MatPrice] = []
|
|
|
+ revenue = 0
|
|
|
+ output_prices: dict[str, PriceNonNull] = {}
|
|
|
+ for output in recipe['Outputs']:
|
|
|
+ price = prices[output['Ticker']]
|
|
|
+ if price.vwap_7d is None or price.average_traded_7d is None:
|
|
|
+ return # skip recipes with thinly traded outputs
|
|
|
+ output_prices[output['Ticker']] = typing.cast(PriceNonNull, price)
|
|
|
+ outputs.append(MatPrice(output['Ticker'], output['Amount'], price.vwap_7d))
|
|
|
+ revenue += price.vwap_7d * output['Amount']
|
|
|
+
|
|
|
+ input_costs: list[MatPrice] = []
|
|
|
cost = 0
|
|
|
for input in recipe['Inputs']:
|
|
|
if (input_cost := prices[input['Ticker']].vwap_7d) is None:
|
|
|
return # skip recipes with thinly traded inputs
|
|
|
+ input_costs.append(MatPrice(input['Ticker'], input['Amount'], input_cost))
|
|
|
cost += input_cost * input['Amount']
|
|
|
- revenue = output_price.vwap_7d * output['Amount']
|
|
|
+ profit_per_run = revenue - cost
|
|
|
+
|
|
|
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
|
|
|
+
|
|
|
+ lowest_liquidity = min(recipe['Outputs'],
|
|
|
+ key=lambda output: output['Amount'] / output_prices[output['Ticker']].average_traded_7d)
|
|
|
+ output_per_day = lowest_liquidity['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'],
|
|
|
+ sum(materials[output['Ticker']]['Weight'] * output['Amount'] for output in recipe['Outputs']),
|
|
|
+ sum(materials[output['Ticker']]['Volume'] * output['Amount'] for output in recipe['Outputs']),
|
|
|
) * runs_per_day / area
|
|
|
- return Profit(output['Ticker'], recipe['RecipeName'],
|
|
|
+ return Profit(outputs, recipe['RecipeName'],
|
|
|
expertise=building['Expertise'],
|
|
|
profit_per_day=(profit_per_run * runs_per_day - worker_consumable_daily_cost),
|
|
|
area=area,
|
|
|
capex=capex,
|
|
|
cost_per_day=cost_per_day,
|
|
|
+ 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=output_price.average_traded_7d)
|
|
|
+ average_traded_7d=output_prices[lowest_liquidity['Ticker']].average_traded_7d)
|
|
|
|
|
|
def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
|
|
|
return sum(bc['Amount'] * prices[bc['CommodityTicker']].vwap_7d for bc in building['BuildingCosts']) # pyright: ignore[reportOperatorIssue]
|
|
|
@@ -144,15 +160,23 @@ class Price:
|
|
|
average_traded_7d: float | None
|
|
|
vwap_30d: float | None
|
|
|
|
|
|
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
+class PriceNonNull:
|
|
|
+ vwap_7d: float
|
|
|
+ average_traded_7d: float
|
|
|
+
|
|
|
@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class Profit:
|
|
|
- output: str
|
|
|
+ outputs: typing.Collection[MatPrice]
|
|
|
recipe: str
|
|
|
expertise: str
|
|
|
profit_per_day: float
|
|
|
area: float
|
|
|
capex: float
|
|
|
cost_per_day: float
|
|
|
+ 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
|
|
|
@@ -164,5 +188,11 @@ class Profit:
|
|
|
other_break_even = 10000 - other.profit_per_day
|
|
|
return break_even < other_break_even
|
|
|
|
|
|
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
+class MatPrice:
|
|
|
+ ticker: str
|
|
|
+ amount: int
|
|
|
+ vwap_7d: float
|
|
|
+
|
|
|
if __name__ == '__main__':
|
|
|
main()
|