buy.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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. buy: dict[str, int] = collections.defaultdict(int)
  74. for planet in planets:
  75. for mat, amount in planet.supply_for_days(days).items():
  76. buy[mat] += amount
  77. return buy
  78. def get_planet_exports_and_ship_storage() -> typing.Mapping[str, int]:
  79. '''materials in base storage that aren't being consumed and materials in ship storage'''
  80. avail = collections.defaultdict(int)
  81. fio_burn: typing.Sequence[supply.FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
  82. headers={'Authorization': config.fio_api_key})
  83. for burn in fio_burn:
  84. planet = supply.Planet(burn)
  85. for mat in planet.exporting:
  86. avail[mat] += planet.inventory.get(mat, 0)
  87. stores: typing.Sequence[market.Storage] = cache.get('https://rest.fnar.net/storage/' + config.username,
  88. headers={'Authorization': config.fio_api_key})
  89. for store in stores:
  90. if store['Type'] != 'SHIP_STORE':
  91. continue
  92. for item in store['StorageItems']:
  93. avail[item['MaterialTicker']] += item['MaterialAmount']
  94. return avail
  95. def get_bids() -> tuple[typing.Mapping[str, int], list[market.ExchangeOrder]]:
  96. orders: typing.Sequence[market.ExchangeOrder] = cache.get('https://rest.fnar.net/cxos/' + config.username,
  97. headers={'Authorization': config.fio_api_key})
  98. orders = [order for order in orders
  99. if order['OrderType'] == 'BUYING' and order['Status'] != 'FILLED' and order['ExchangeCode'] == 'IC1']
  100. bids = collections.defaultdict(int)
  101. for order in orders:
  102. bids[order['MaterialTicker']] += order['Amount']
  103. return bids, orders
  104. @dataclasses.dataclass(eq=False, frozen=True, slots=True)
  105. class Material:
  106. ticker: str
  107. amount: int
  108. bids: int
  109. have: int
  110. spread: float
  111. savings: float
  112. def __lt__(self, o: Material) -> bool:
  113. return self.savings< o.savings
  114. if __name__ == '__main__':
  115. main()