buy.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. bid_deposits: dict[str, float] = collections.defaultdict(float)
  62. for order in orders:
  63. bid_deposits[order['MaterialTicker']] += order['Limit'] * order['Amount']
  64. print('\ncurrent bid deposits:')
  65. for mat, deposit in sorted(bid_deposits.items(), key=lambda kv: kv[1], reverse=True):
  66. print(f"{mat:4} {deposit:7,.0f}")
  67. def get_raw_prices() -> typing.Mapping[str, market.RawPrice]:
  68. return {p['MaterialTicker']: p
  69. for p in cache.get('https://refined-prun.github.io/refined-prices/all.json') if p['ExchangeCode'] == 'IC1'}
  70. def get_total_buy(days: int) -> typing.Mapping[str, int]:
  71. planets = [supply.Planet(fio_burn) for fio_burn in cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
  72. headers={'Authorization': config.fio_api_key})]
  73. total_consumption: dict[str, float] = collections.defaultdict(float)
  74. for planet in planets:
  75. supply_config = config.supply_config(planet.name)
  76. for mat, amount in planet.net_consumption.items():
  77. if mat not in supply_config.ignore_materials:
  78. total_consumption[mat] += amount
  79. buy: dict[str, int] = collections.defaultdict(int)
  80. for mat, consumption in total_consumption.items():
  81. if consumption <= 0:
  82. continue
  83. buy[mat] = round(consumption * days)
  84. return buy
  85. def get_planet_exports_and_ship_storage() -> typing.Mapping[str, int]:
  86. '''materials in base storage that aren't being consumed and materials in ship storage'''
  87. avail = collections.defaultdict(int)
  88. fio_burn: typing.Sequence[supply.FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
  89. headers={'Authorization': config.fio_api_key})
  90. for burn in fio_burn:
  91. planet = supply.Planet(burn)
  92. for mat in planet.exporting:
  93. avail[mat] += planet.inventory.get(mat, 0)
  94. stores: typing.Sequence[market.Storage] = cache.get('https://rest.fnar.net/storage/' + config.username,
  95. headers={'Authorization': config.fio_api_key})
  96. for store in stores:
  97. if store['Type'] != 'SHIP_STORE':
  98. continue
  99. for item in store['StorageItems']:
  100. avail[item['MaterialTicker']] += item['MaterialAmount']
  101. return avail
  102. def get_bids() -> tuple[typing.Mapping[str, int], list[market.ExchangeOrder]]:
  103. orders: typing.Sequence[market.ExchangeOrder] = cache.get('https://rest.fnar.net/cxos/' + config.username,
  104. headers={'Authorization': config.fio_api_key})
  105. orders = [order for order in orders
  106. if order['OrderType'] == 'BUYING' and order['Status'] != 'FILLED' and order['ExchangeCode'] == 'IC1']
  107. bids = collections.defaultdict(int)
  108. for order in orders:
  109. bids[order['MaterialTicker']] += order['Amount']
  110. return bids, orders
  111. @dataclasses.dataclass(eq=False, frozen=True, slots=True)
  112. class Material:
  113. ticker: str
  114. amount: int
  115. bids: int
  116. have: int
  117. spread: float
  118. savings: float
  119. def __lt__(self, o: Material) -> bool:
  120. return self.savings< o.savings
  121. if __name__ == '__main__':
  122. main()