buy.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. from __future__ import annotations
  2. import collections
  3. import concurrent.futures
  4. import dataclasses
  5. import json
  6. import math
  7. import sys
  8. import typing
  9. import cache
  10. from config import config
  11. import supply
  12. if typing.TYPE_CHECKING:
  13. import market
  14. def main() -> None:
  15. days = int(sys.argv[1])
  16. with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
  17. futures = [
  18. executor.submit(get_raw_prices),
  19. executor.submit(get_total_buy, days), # what we need to buy
  20. executor.submit(supply.warehouse_inventory), # what we have
  21. executor.submit(get_planet_exports_and_ship_storage),
  22. executor.submit(get_bids), # what we already are bidding for
  23. ]
  24. raw_prices, buy, warehouse, exports, (bids, orders) = (f.result() for f in futures)
  25. executor.shutdown()
  26. # what's left to buy
  27. materials: list[Material] = []
  28. price_limits: dict[str, int] = {}
  29. for mat, amount in buy.items():
  30. remaining = max(amount - bids[mat] - warehouse.get(mat, 0) - exports.get(mat, 0), 0)
  31. price = raw_prices[mat]
  32. if price['Bid'] is None or price['Ask'] is None:
  33. print(mat, 'has no bid/ask')
  34. continue
  35. spread = price['Ask'] - price['Bid']
  36. materials.append(Material(mat, amount=amount, bids=bids[mat], have=warehouse.get(mat, 0) + exports.get(mat, 0),
  37. spread=spread, savings=spread * remaining))
  38. epsilon = 10 ** (int(math.log10(price['Bid'])) - 2)
  39. price_limits[mat] = price['Bid'] + 2 * epsilon
  40. materials.sort(reverse=True)
  41. to_buy: dict[str, int] = {}
  42. print('mat want bids have buy savings')
  43. for m in materials:
  44. buy = max(m.amount - m.bids - m.have, 0)
  45. if m.bids == 0 and buy > 0:
  46. bids = f'\033[91m{m.bids:5}\033[0m'
  47. else:
  48. bids = str(m.bids).rjust(5)
  49. print(f'{m.ticker:4} {m.amount:>5} {bids} {m.have:>5} {buy:>5} {m.savings:8.0f}')
  50. if buy > 0:
  51. to_buy[m.ticker] = buy
  52. print('\n' + json.dumps({
  53. 'actions': [
  54. {'name': 'BuyItems', 'type': 'CX Buy', 'group': 'A1', 'exchange': 'IC1',
  55. 'priceLimits': price_limits, 'buyPartial': True, 'allowUnfilled': True, 'useCXInv': False},
  56. ],
  57. 'global': {'name': f'buy orders for {days} days'},
  58. 'groups': [{'type': 'Manual', 'name': 'A1', 'materials': to_buy}],
  59. }))
  60. # deposits of current bids
  61. orders.sort(key=lambda order: order['Limit'] * order['Amount'], reverse=True)
  62. print('\ncurrent bid deposits:')
  63. for order in orders:
  64. print(f"{order['MaterialTicker']:4} {order['Limit'] * order['Amount']:7,.0f}")
  65. def get_raw_prices() -> typing.Mapping[str, market.RawPrice]:
  66. return {p['MaterialTicker']: p
  67. for p in cache.get('https://refined-prun.github.io/refined-prices/all.json') if p['ExchangeCode'] == 'IC1'}
  68. def get_total_buy(days: int) -> typing.Mapping[str, int]:
  69. planets = [supply.Planet(fio_burn) for fio_burn in cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
  70. headers={'Authorization': config.fio_api_key})]
  71. buy: dict[str, int] = collections.defaultdict(int)
  72. for planet in planets:
  73. for mat, amount in planet.supply_for_days(days).items():
  74. buy[mat] += amount
  75. return buy
  76. def get_planet_exports_and_ship_storage() -> typing.Mapping[str, int]:
  77. '''materials in base storage that aren't being consumed and materials in ship storage'''
  78. avail = collections.defaultdict(int)
  79. fio_burn: typing.Sequence[supply.FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
  80. headers={'Authorization': config.fio_api_key})
  81. for burn in fio_burn:
  82. planet = supply.Planet(burn)
  83. for mat in planet.exporting:
  84. avail[mat] += planet.inventory.get(mat, 0)
  85. stores: typing.Sequence[market.Storage] = cache.get('https://rest.fnar.net/storage/' + config.username,
  86. headers={'Authorization': config.fio_api_key})
  87. for store in stores:
  88. if store['Type'] != 'SHIP_STORE':
  89. continue
  90. for item in store['StorageItems']:
  91. avail[item['MaterialTicker']] += item['MaterialAmount']
  92. return avail
  93. def get_bids() -> tuple[typing.Mapping[str, int], list[market.ExchangeOrder]]:
  94. orders: typing.Sequence[market.ExchangeOrder] = cache.get('https://rest.fnar.net/cxos/' + config.username,
  95. headers={'Authorization': config.fio_api_key})
  96. orders = [order for order in orders
  97. if order['OrderType'] == 'BUYING' and order['Status'] != 'FILLED' and order['ExchangeCode'] == 'IC1']
  98. bids = collections.defaultdict(int)
  99. for order in orders:
  100. bids[order['MaterialTicker']] += order['Amount']
  101. return bids, orders
  102. @dataclasses.dataclass(eq=False, frozen=True, slots=True)
  103. class Material:
  104. ticker: str
  105. amount: int
  106. bids: int
  107. have: int
  108. spread: float
  109. savings: float
  110. def __lt__(self, o: Material) -> bool:
  111. return self.savings< o.savings
  112. if __name__ == '__main__':
  113. main()