|
|
@@ -1,58 +1,50 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import collections
|
|
|
-import concurrent.futures
|
|
|
import dataclasses
|
|
|
+import datetime
|
|
|
import typing
|
|
|
|
|
|
import cache
|
|
|
|
|
|
def main() -> None:
|
|
|
- raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
|
|
|
+ current_prices: typing.Sequence[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
|
|
|
+ old_prices = get_old_prices()
|
|
|
|
|
|
movers: dict[str, list[Mover]] = collections.defaultdict(list)
|
|
|
- with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
|
|
- futures: list[concurrent.futures.Future[Mover | None]] = []
|
|
|
- for price in raw_prices:
|
|
|
- futures.append(executor.submit(analyze_raw_price, price))
|
|
|
- for future in futures:
|
|
|
- if (mover := future.result()) is not None:
|
|
|
- movers[mover.ticker].append(mover)
|
|
|
- executor.shutdown()
|
|
|
+ for current_price in current_prices:
|
|
|
+ old_price = old_prices[current_price['FullTicker']]
|
|
|
+ if (mover := analyze_raw_price(current_price, old_price)) is not None:
|
|
|
+ movers[mover.ticker].append(mover)
|
|
|
|
|
|
for ticker_movers in movers.values():
|
|
|
ticker_movers.sort(reverse=True)
|
|
|
top_movers = sorted(movers.values(), key=lambda m: m[0].score, reverse=True)
|
|
|
|
|
|
for commodity in top_movers:
|
|
|
- print(f'{commodity[0].score:7.1f}', ' '.join(f'{mover.ticker}.{mover.exchange_code}' for mover in commodity))
|
|
|
+ print(f'{commodity[0].score:9,.0f}', ' '.join(f'{mover.ticker}.{mover.exchange_code}' for mover in commodity))
|
|
|
|
|
|
-def analyze_raw_price(price: RawPrice) -> Mover | None:
|
|
|
- if (traded := price['AverageTraded7D']) is None:
|
|
|
- return
|
|
|
- if (vwap7d := price['VWAP7D']) is None or (vwap30d := price['VWAP30D']) is None:
|
|
|
+def get_old_prices() -> typing.Mapping[str, RawPrice]:
|
|
|
+ week_ago = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=14)
|
|
|
+ commits = cache.get(f'https://api.github.com/repos/refined-prun/refined-prices/commits?until={week_ago.isoformat()}&per_page=1')
|
|
|
+ return {p['FullTicker']: p
|
|
|
+ for p in cache.get(f'https://raw.githubusercontent.com/refined-prun/refined-prices/{commits[0]["sha"]}/all.json')}
|
|
|
+
|
|
|
+def analyze_raw_price(current_price: RawPrice, old_price: RawPrice) -> Mover | None:
|
|
|
+ if (traded := current_price['AverageTraded30D']) is None:
|
|
|
return
|
|
|
- if (bid := price['Bid']) is None or (ask := price['Ask']) is None:
|
|
|
+ if (current_vwap7d := current_price['VWAP7D']) is None or (old_vwap7d := old_price['VWAP7D']) is None:
|
|
|
return
|
|
|
- scores = [
|
|
|
- (bid - vwap30d) / bid,
|
|
|
- (vwap30d - ask) / vwap30d,
|
|
|
- abs(vwap7d - vwap30d) / max(vwap7d, vwap30d),
|
|
|
- ]
|
|
|
- score = max(scores) * traded
|
|
|
- if score > 100:
|
|
|
- return Mover(price['ExchangeCode'], price['MaterialTicker'], score)
|
|
|
+ diff = abs(current_vwap7d - old_vwap7d)
|
|
|
+ if diff / min(current_vwap7d, old_vwap7d) > 0.15:
|
|
|
+ return Mover(current_price['ExchangeCode'], current_price['MaterialTicker'], diff * traded)
|
|
|
|
|
|
class RawPrice(typing.TypedDict):
|
|
|
+ FullTicker: str
|
|
|
ExchangeCode: str
|
|
|
MaterialTicker: str
|
|
|
VWAP7D: float | None # volume-weighted average price over last 7 days
|
|
|
- AverageTraded7D: float | None # averaged daily traded volume over last 7 days
|
|
|
- VWAP30D: float | None
|
|
|
- Bid: float | None
|
|
|
- Ask: float | None
|
|
|
- HighYesterday: float | None
|
|
|
- LowYesterday: float | None
|
|
|
+ AverageTraded30D: float | None # averaged daily traded volume over last 30 days
|
|
|
|
|
|
@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class Mover:
|