market.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. from __future__ import annotations
  2. import collections
  3. import dataclasses
  4. import sys
  5. import typing
  6. import cache
  7. from config import config
  8. def main() -> None:
  9. if len(sys.argv) > 1:
  10. exchange_tickers = sys.argv[1:]
  11. for ticker in exchange_tickers:
  12. bids_filled, asks_filled = estimate_filled_orders(ticker, 0)
  13. print(f'{ticker}: {bids_filled=}, {asks_filled=}')
  14. return
  15. check_cxos()
  16. raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
  17. markets: dict[str, list[Market]] = collections.defaultdict(list)
  18. for price in raw_prices:
  19. if (traded := price['AverageTraded7D']) is None or traded < 100:
  20. continue
  21. if price['Bid'] is None or price['Ask'] is None:
  22. continue
  23. if (high := price['HighYesterday']) is None or (low := price['LowYesterday']) is None:
  24. continue
  25. if (high - low) / high < 0.1:
  26. continue
  27. spread = (price['Ask'] - price['Bid']) / price['Ask']
  28. if spread < 0.15:
  29. continue
  30. bids_filled, asks_filled = estimate_filled_orders(price['FullTicker'], (price['Bid'] + price['Ask']) / 2)
  31. bid_fill_ratio = bids_filled / (bids_filled + asks_filled)
  32. if bid_fill_ratio < 0.05 or bid_fill_ratio > 0.95:
  33. continue
  34. max_profit = (price['Ask'] - price['Bid']) * min(bids_filled, asks_filled)
  35. markets[price['ExchangeCode']].append(Market(price['FullTicker'], bid=price['Bid'], ask=price['Ask'],
  36. spread=spread, traded=traded, bids_filled=bids_filled, asks_filled=asks_filled, max_profit=max_profit))
  37. print(f'{"mat":^8} {"bid":^5} {"ask":^5} spread {"traded":^7} bids filled asks filled max profit')
  38. for commodities in markets.values():
  39. commodities.sort(reverse=True)
  40. for m in commodities:
  41. print(f'{m.full_ticker:>8} {m.bid:5} {m.ask:5} {m.spread*100: 5.0f}% {m.traded:7.0f} {m.bids_filled:10.0f} {m.asks_filled:10.0f} {m.max_profit:10.0f}')
  42. print()
  43. def check_cxos() -> None:
  44. orders: typing.Sequence[ExchangeOrder] = cache.get('https://rest.fnar.net/cxos/' + config.username,
  45. headers={'Authorization': config.fio_api_key})
  46. summary: typing.Mapping[tuple[str, str], ExchangeSummary] = {
  47. (summary['MaterialTicker'], summary['ExchangeCode']): summary
  48. for summary in cache.get('https://rest.fnar.net/exchange/all')
  49. }
  50. for order in orders:
  51. if order['Status'] == 'FILLED':
  52. continue
  53. state = summary[order['MaterialTicker'], order['ExchangeCode']]
  54. if order['OrderType'] == 'BUYING' and state['Bid'] is not None and state['Bid'] > order['Limit']:
  55. print('outbid on', f'{order["MaterialTicker"]}.{order["ExchangeCode"]}')
  56. elif order['OrderType'] == 'SELLING' and state['Ask'] is not None and state['Ask'] < order['Limit']:
  57. print('undercut on', f'{order["MaterialTicker"]}.{order["ExchangeCode"]}')
  58. print()
  59. warehouses: typing.Sequence[Warehouse] = cache.get('https://rest.fnar.net/sites/warehouses/' + config.username,
  60. headers={'Authorization': config.fio_api_key})
  61. for warehouse in warehouses:
  62. if warehouse['LocationNaturalId'] in config.ignore_warehouses:
  63. continue
  64. storage: Storage = cache.get(f'https://rest.fnar.net/storage/{config.username}/{warehouse["StoreId"]}',
  65. headers={'Authorization': config.fio_api_key})
  66. if storage['WeightLoad'] > 0 or storage['VolumeLoad'] > 0:
  67. print('warehouse', warehouse['LocationNaturalId'], 'is not empty')
  68. print()
  69. def estimate_filled_orders(exchange_ticker: str, midpoint: float) -> tuple[float, float]:
  70. '''use price chart to estimate how many bids and asks were filled'''
  71. bids = asks = 0
  72. for hist in cache.get('https://rest.fnar.net/exchange/cxpc/' + exchange_ticker):
  73. if hist['Interval'] != 'MINUTE_FIVE':
  74. continue
  75. if hist['Low'] > midpoint:
  76. asks += hist['Traded']
  77. elif hist['High'] < midpoint:
  78. bids += hist['Traded']
  79. elif hist['High'] == hist['Low']:
  80. assert hist['High'] == midpoint
  81. else:
  82. interval_bids = (hist['High'] * hist['Traded'] - hist['Volume']) / (hist['High'] - hist['Low'])
  83. interval_asks = hist['Traded'] - interval_bids
  84. bids += interval_bids
  85. asks += interval_asks
  86. return bids, asks
  87. class ExchangeOrder(typing.TypedDict):
  88. MaterialTicker: str
  89. ExchangeCode: str
  90. OrderType: typing.Literal['SELLING'] | typing.Literal['BUYING']
  91. Limit: float
  92. class ExchangeSummary(typing.TypedDict):
  93. MaterialTicker: str
  94. ExchangeCode: str
  95. Bid: float | None
  96. Ask: float | None
  97. class Warehouse(typing.TypedDict):
  98. StoreId: str
  99. LocationNaturalId: str
  100. class Storage(typing.TypedDict):
  101. StorageItems: typing.Sequence
  102. WeightLoad: float
  103. VolumeLoad: float
  104. class RawPrice(typing.TypedDict):
  105. FullTicker: str
  106. MaterialTicker: str
  107. ExchangeCode: str
  108. Bid: float | None
  109. Ask: float | None
  110. HighYesterday: float | None
  111. LowYesterday: float | None
  112. AverageTraded7D: float | None # averaged daily traded volume over last 7 days
  113. class PriceChartPoint(typing.TypedDict):
  114. Interval: typing.Literal['MINUTE_FIVE'] | typing.Literal['MINUTE_FIFTEEN'] | typing.Literal['MINUTE_THIRTY'] | typing.Literal['HOUR_ONE'] | typing.Literal['HOUR_TWO'] | typing.Literal['HOUR_FOUR'] | typing.Literal['HOUR_SIX'] | typing.Literal['HOUR_TWELVE'] | typing.Literal['DAY_ONE'] | typing.Literal['DAY_THREE']
  115. High: float
  116. Low: float
  117. Volume: float
  118. Traded: int
  119. @dataclasses.dataclass(eq=False, slots=True)
  120. class Market:
  121. full_ticker: str
  122. bid: float
  123. ask: float
  124. spread: float
  125. traded: float
  126. bids_filled: float
  127. asks_filled: float
  128. max_profit: float
  129. def __lt__(self, o) -> bool:
  130. return self.max_profit < o.max_profit
  131. if __name__ == '__main__':
  132. main()