|
|
@@ -0,0 +1,54 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import dataclasses
|
|
|
+import sys
|
|
|
+import typing
|
|
|
+
|
|
|
+import cache
|
|
|
+import roi
|
|
|
+
|
|
|
+def main() -> None:
|
|
|
+ from_cx, to_cx = sys.argv[1:]
|
|
|
+ raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
|
|
|
+ materials: dict[str, roi.Material] = {m['Ticker']: m for m in cache.get('https://api.prunplanner.org/data/materials')}
|
|
|
+
|
|
|
+ bids: dict[str, float] = {}
|
|
|
+ for price in raw_prices:
|
|
|
+ if price['ExchangeCode'] != to_cx or price['MMBuy'] is None:
|
|
|
+ continue
|
|
|
+ assert price['Bid'] is not None
|
|
|
+ bids[price['MaterialTicker']] = price['Bid']
|
|
|
+
|
|
|
+ rates: list[FX] = []
|
|
|
+ for price in raw_prices:
|
|
|
+ if price['ExchangeCode'] != from_cx or price['Ask'] is None or (to_price := bids.get(price['MaterialTicker'])) is None:
|
|
|
+ continue
|
|
|
+ rates.append(FX(price['MaterialTicker'], to_price, (price['Ask'] - to_price) / to_price))
|
|
|
+
|
|
|
+ rates.sort()
|
|
|
+ for rate in rates:
|
|
|
+ print(f'{rate.ticker:4} {rate.rate:8.5f}', end=' ')
|
|
|
+ mat = materials[rate.ticker]
|
|
|
+ if mat['Weight'] >= mat['Volume']:
|
|
|
+ print(f'{rate.to_price / mat["Weight"]:9,.0f}/t')
|
|
|
+ else:
|
|
|
+ print(f'{rate.to_price / mat["Volume"]:9,.0f}/m³')
|
|
|
+
|
|
|
+class RawPrice(typing.TypedDict):
|
|
|
+ MaterialTicker: str
|
|
|
+ ExchangeCode: str
|
|
|
+ Ask: float | None
|
|
|
+ Bid: float | None
|
|
|
+ MMBuy: float | None
|
|
|
+
|
|
|
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
+class FX:
|
|
|
+ ticker: str
|
|
|
+ to_price: float
|
|
|
+ rate: float
|
|
|
+
|
|
|
+ def __lt__(self, other: FX) -> bool:
|
|
|
+ return self.rate < other.rate
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ main()
|