market_stats.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. from __future__ import annotations
  2. import collections
  3. import base64
  4. import datetime
  5. import io
  6. import json
  7. import sys
  8. import typing
  9. import altair
  10. import dulwich.repo
  11. import dulwich.objects
  12. if typing.TYPE_CHECKING:
  13. import market
  14. def main() -> None:
  15. weeks = int(sys.argv[1])
  16. repo = dulwich.repo.Repo('../refined-prices')
  17. today = datetime.datetime.now(tz=datetime.UTC).replace(hour=0, minute=0, second=0, microsecond=0)
  18. sunday = (today - datetime.timedelta(days=(today.weekday() + 1) % 7))
  19. weekly_stats: list[MarketStats] = []
  20. for _ in range(weeks):
  21. weekly_stats.append(analyze_markets(prices_on_day(repo, sunday), sunday.date()))
  22. sunday -= datetime.timedelta(days=7)
  23. weekly_stats.reverse()
  24. base = altair.Chart({'values': weekly_stats}).encode(x=altair.X('date:T', title='week'))
  25. render_chart(base, 'ranking:Q', 'ranking', 'average IC1 rank')
  26. print('higher is better\naverage = (0 + 3) ÷ 2 = 1.5')
  27. render_chart(base, 'ic1_lowest:Q', '%', 'IC1 lowest')
  28. render_chart(base, 'ic1_zero:Q', '#', 'IC1 zero trades')
  29. render_chart(base, 'gap:Q', '%', 'trade gap')
  30. def prices_on_day(repo: dulwich.repo.Repo, day: datetime.datetime) -> typing.Sequence[market.RawPrice]:
  31. '''refined-prices for the earliest commit on the given day'''
  32. day_ts = int(day.timestamp())
  33. next_day_ts = int((day + datetime.timedelta(days=1)).timestamp())
  34. *_, entry = repo.get_walker(paths=[b'all.json'], since=day_ts, until=next_day_ts)
  35. dt = datetime.datetime.fromtimestamp(entry.commit.author_time, tz=datetime.UTC)
  36. print('loading refined-prices', dt, entry.commit.tree.decode())
  37. tree = typing.cast(dulwich.objects.Tree, repo[entry.commit.tree])
  38. _, blob = tree[b'all.json']
  39. return json.loads(typing.cast(dulwich.objects.Blob, repo[blob]).data.decode())
  40. def analyze_markets(prices: typing.Sequence[market.RawPrice], date: datetime.date) -> MarketStats:
  41. markets: dict[str, list[market.RawPrice]] = collections.defaultdict(list)
  42. for price in prices:
  43. if price['ExchangeCode'].endswith('2'):
  44. continue
  45. markets[price['MaterialTicker']].append(price)
  46. markets_with_trades = ranking = ic1_lowest = ic1_zero = gap = 0
  47. for mat, mat_prices in markets.items():
  48. mat_prices.sort(key=lambda p: (p['Traded7D'] or 0))
  49. if mat_prices[-1]['Traded7D'] == None:
  50. continue
  51. markets_with_trades += 1
  52. for index, price in enumerate(mat_prices):
  53. if price['ExchangeCode'] == 'IC1':
  54. break
  55. else:
  56. raise AssertionError('IC1 not found for ' + mat)
  57. ranking += index
  58. if index == 0:
  59. ic1_lowest += 1
  60. ic1_traded = mat_prices[0]['Traded7D'] or 0
  61. lowest_traded = mat_prices[1]['Traded7D'] or 0
  62. if lowest_traded > 0:
  63. if ic1_traded == 0:
  64. ic1_zero += 1
  65. gap += (lowest_traded - ic1_traded) / lowest_traded
  66. print(f'{markets_with_trades} markets with trades, IC1 ranking {ranking}, lowest in {ic1_lowest}, zero in {ic1_zero}, gap {gap:.2f}')
  67. return {
  68. 'date': date.isoformat(),
  69. 'ranking': ranking / markets_with_trades,
  70. 'ic1_lowest': ic1_lowest / markets_with_trades,
  71. 'ic1_zero': ic1_zero,
  72. 'gap': gap / markets_with_trades,
  73. }
  74. def render_chart(base: altair.Chart, y: str, y_label: str, title: str) -> None:
  75. chart = base.mark_line(point=True).encode(y=altair.Y(y, title=y_label)).properties(title=title, height=160)
  76. buffer = io.BytesIO()
  77. chart.save(buffer, format='png')
  78. display_kitty_png(buffer.getvalue())
  79. def display_kitty_png(png: bytes) -> None:
  80. encoded = base64.b64encode(png).decode()
  81. chunk_size = 4096
  82. for offset in range(0, len(encoded), chunk_size):
  83. part = encoded[offset:offset + chunk_size]
  84. more = int(offset + chunk_size < len(encoded))
  85. if offset == 0:
  86. params = f'a=T,f=100,q=2,m={more}'
  87. else:
  88. params = f'm={more}'
  89. sys.stdout.write(f'\033_G{params};{part}\033\\')
  90. sys.stdout.write('\n')
  91. sys.stdout.flush()
  92. class MarketStats(typing.TypedDict):
  93. date: str
  94. ranking: float
  95. ic1_lowest: float
  96. ic1_zero: int
  97. gap: float
  98. if __name__ == '__main__':
  99. main()