market_stats.py 3.9 KB

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