market_stats.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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 = total_rank = ic1_lowest = ic1_zero = gap = 0
  47. for mat, mat_prices in markets.items():
  48. # find IC1's Traded7D rank among the CXes
  49. # group CXes by Traded7D first in case of ties
  50. rankings: dict[int, list[str]] = collections.defaultdict(list)
  51. for price in mat_prices:
  52. rankings[price['Traded7D'] or 0].append(price['ExchangeCode'])
  53. if len(rankings) == 1 and 0 in rankings: # no markets had any trades
  54. continue
  55. sorted_markets = sorted(rankings.items(), key=lambda item: item[0])
  56. rank = 0
  57. for _, cxes in sorted_markets:
  58. if 'IC1' in cxes:
  59. break
  60. rank += len(cxes)
  61. else:
  62. raise AssertionError('IC1 not found for ' + mat)
  63. markets_with_trades += 1
  64. total_rank += rank
  65. if rank == 0:
  66. ic1_lowest += 1
  67. ic1_traded = sorted_markets[0][0]
  68. if len(sorted_markets[0][1]) > 1: # another market is tied for last with us
  69. lowest_traded = ic1_traded
  70. else:
  71. lowest_traded = sorted_markets[1][0]
  72. if lowest_traded > 0:
  73. if ic1_traded == 0:
  74. ic1_zero += 1
  75. gap += (lowest_traded - ic1_traded) / lowest_traded
  76. print(f'{markets_with_trades} markets with trades, IC1 total rank {total_rank}, lowest in {ic1_lowest}, zero in {ic1_zero}, gap {gap:.2f}')
  77. return {
  78. 'date': date.isoformat(),
  79. 'ranking': total_rank / markets_with_trades,
  80. 'ic1_lowest': ic1_lowest / markets_with_trades * 100,
  81. 'ic1_zero': ic1_zero,
  82. 'gap': gap / markets_with_trades * 100,
  83. }
  84. def render_chart(base: altair.Chart, y: str, y_label: str, title: str) -> None:
  85. chart = base.mark_line(point=True).encode(y=altair.Y(y, title=y_label)).properties(title=title, height=160)
  86. buffer = io.BytesIO()
  87. chart.save(buffer, format='png')
  88. display_kitty_png(buffer.getvalue())
  89. def display_kitty_png(png: bytes) -> None:
  90. encoded = base64.b64encode(png).decode()
  91. chunk_size = 4096
  92. for offset in range(0, len(encoded), chunk_size):
  93. part = encoded[offset:offset + chunk_size]
  94. more = int(offset + chunk_size < len(encoded))
  95. if offset == 0:
  96. params = f'a=T,f=100,q=2,m={more}'
  97. else:
  98. params = f'm={more}'
  99. sys.stdout.write(f'\033_G{params};{part}\033\\')
  100. sys.stdout.write('\n')
  101. sys.stdout.flush()
  102. class MarketStats(typing.TypedDict):
  103. date: str
  104. ranking: float
  105. ic1_lowest: float
  106. ic1_zero: int
  107. gap: float
  108. if __name__ == '__main__':
  109. main()