market.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. from __future__ import annotations
  2. import collections
  3. import dataclasses
  4. import datetime
  5. import statistics
  6. import sys
  7. import typing
  8. import cache
  9. from config import config
  10. def main() -> None:
  11. raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
  12. if len(sys.argv) > 1:
  13. exchange_tickers = sys.argv[1:]
  14. for ticker in exchange_tickers:
  15. (price,) = (p for p in raw_prices if p['FullTicker'] == ticker)
  16. a = analyze_price_chart(ticker, (price['Bid'] + price['Ask']) / 2) # pyright: ignore[reportOperatorIssue]
  17. print(f'{ticker}: bids filled = {a.bids_filled:6.0f}, asks filled = {a.asks_filled:6.0f}, profit per interval = {a.profits:10.1f}')
  18. return
  19. check_cxos()
  20. markets: dict[str, list[Market]] = collections.defaultdict(list)
  21. for price in raw_prices:
  22. if (traded := price['AverageTraded7D']) is None or traded < 100:
  23. continue
  24. if price['Bid'] is None or price['Ask'] is None:
  25. continue
  26. if (high := price['HighYesterday']) is None or (low := price['LowYesterday']) is None:
  27. continue
  28. if (high - low) / high < 0.1:
  29. continue
  30. spread = (price['Ask'] - price['Bid']) / price['Ask']
  31. if spread < 0.15:
  32. continue
  33. chart_analysis = analyze_price_chart(price['FullTicker'], (price['Bid'] + price['Ask']) / 2)
  34. markets[price['ExchangeCode']].append(Market(price['FullTicker'], bid=price['Bid'], ask=price['Ask'],
  35. spread=spread, chart_analysis=chart_analysis))
  36. print(' mat bid ask spread bids filled asks filled profit p75 fill time')
  37. for commodities in markets.values():
  38. commodities.sort(reverse=True)
  39. for m in commodities:
  40. print(f'{m.full_ticker:>8} {m.bid:5} {m.ask:5} {m.spread*100: 5.0f}% {m.chart_analysis.bids_filled:12.0f} '
  41. f'{m.chart_analysis.asks_filled:12.0f} {m.chart_analysis.profits:10.0f} {m.chart_analysis.p75_fill_time}')
  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 analyze_price_chart(exchange_ticker: str, midpoint: float) -> ChartAnalysis:
  70. '''use price chart to estimate how long it takes to fill a bid and then an ask'''
  71. pcpoints: list[PriceChartPoint] = [p for p in cache.get('https://rest.fnar.net/exchange/cxpc/' + exchange_ticker)
  72. if p['Interval'] == 'MINUTE_FIVE']
  73. pcpoints.reverse()
  74. five_min = 5 * 60 * 1000
  75. asks_filled: list[AskFilled] = []
  76. fill_time = []
  77. r = ChartAnalysis(bids_filled=0, asks_filled=0, profits=0, p75_fill_time=datetime.timedelta.max)
  78. for hist in pcpoints:
  79. time = hist['DateEpochMs'] // five_min
  80. bids = asks = 0
  81. if hist['Low'] > midpoint:
  82. asks = hist['Traded']
  83. r.asks_filled += asks
  84. elif hist['High'] < midpoint:
  85. bids = hist['Traded']
  86. r.bids_filled += bids
  87. elif hist['High'] == hist['Low']: # all trades right at midpoint
  88. assert hist['High'] == midpoint
  89. else:
  90. interval_bids = (hist['High'] * hist['Traded'] - hist['Volume']) / (hist['High'] - hist['Low'])
  91. interval_asks = hist['Traded'] - interval_bids
  92. r.bids_filled += interval_bids
  93. r.asks_filled += interval_asks
  94. bids = int(interval_bids)
  95. asks = int(interval_asks)
  96. if bids and asks:
  97. intra_interval_trades = min(bids, asks)
  98. r.profits += (hist['High'] - hist['Low']) * intra_interval_trades
  99. bids -= intra_interval_trades
  100. asks -= intra_interval_trades
  101. assert bids == 0 or asks == 0
  102. while bids > 0 and len(asks_filled) > 0:
  103. profit = (asks_filled[-1].price - hist['Low'])
  104. if asks_filled[-1].amount >= bids:
  105. r.profits += bids * profit
  106. asks_filled[-1].amount -= bids
  107. if asks_filled[-1].amount == 0:
  108. fill_time.append(asks_filled[-1].time - time)
  109. asks_filled.pop()
  110. bids = 0
  111. else:
  112. r.profits += asks_filled[-1].amount * profit
  113. bids -= asks_filled[-1].amount
  114. fill_time.append(asks_filled[-1].time - time)
  115. asks_filled.pop()
  116. if asks:
  117. asks_filled.append(AskFilled(price=hist['High'], amount=asks, time=time))
  118. if len(fill_time) > 0:
  119. r.p75_fill_time = statistics.quantiles(fill_time, n=4)[2] * datetime.timedelta(minutes=5)
  120. return r
  121. class ExchangeOrder(typing.TypedDict):
  122. MaterialTicker: str
  123. ExchangeCode: str
  124. OrderType: typing.Literal['SELLING'] | typing.Literal['BUYING']
  125. Status: typing.Literal['FILLED'] | typing.Literal['PARTIALLY_FILLED'] | typing.Literal['FILLED']
  126. Amount: int
  127. Limit: float
  128. class ExchangeSummary(typing.TypedDict):
  129. MaterialTicker: str
  130. ExchangeCode: str
  131. Bid: float | None
  132. Ask: float | None
  133. class Warehouse(typing.TypedDict):
  134. StoreId: str
  135. LocationNaturalId: str
  136. class Storage(typing.TypedDict):
  137. Name: str
  138. StorageItems: typing.Sequence[StorageItem]
  139. WeightLoad: float
  140. VolumeLoad: float
  141. Type: typing.Literal['STORE'] | typing.Literal['WAREHOUSE_STORE'] | typing.Literal['FTL_FUEL_STORE'] | typing.Literal['STL_FUEL_STORE'] | typing.Literal['SHIP_STORE']
  142. class StorageItem(typing.TypedDict):
  143. MaterialTicker: str
  144. MaterialAmount: int
  145. class RawPrice(typing.TypedDict):
  146. FullTicker: str
  147. MaterialTicker: str
  148. ExchangeCode: str
  149. Bid: float | None
  150. Ask: float | None
  151. HighYesterday: float | None
  152. LowYesterday: float | None
  153. AverageTraded7D: float | None # averaged daily traded volume over last 7 days
  154. class PriceChartPoint(typing.TypedDict):
  155. 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']
  156. DateEpochMs: int
  157. High: float
  158. Low: float
  159. Volume: float
  160. Traded: int
  161. @dataclasses.dataclass(eq=False, slots=True)
  162. class AskFilled:
  163. price: float
  164. amount: int
  165. time: int
  166. @dataclasses.dataclass(eq=False, slots=True)
  167. class ChartAnalysis:
  168. bids_filled: float
  169. asks_filled: float
  170. profits: float
  171. p75_fill_time: datetime.timedelta
  172. @dataclasses.dataclass(eq=False, frozen=True, slots=True)
  173. class Market:
  174. full_ticker: str
  175. bid: float
  176. ask: float
  177. spread: float
  178. chart_analysis: ChartAnalysis
  179. def __lt__(self, o: Market) -> bool:
  180. return self.chart_analysis.profits < o.chart_analysis.profits
  181. if __name__ == '__main__':
  182. main()