build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,135 @@
# app/tools/dzengi_probe/ticker24hr_update_probe.py
from __future__ import annotations
import asyncio
import csv
import json
import time
from dataclasses import dataclass
from decimal import Decimal
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from app.tools.dzengi_probe.config import DzengiProbeConfig
@dataclass(frozen=True)
class Ticker24hrSample:
sample: int
local_time_ms: int
ask_price: str | None
bid_price: str | None
last_price: str | None
close_time: int | None
class Ticker24hrUpdateProbe:
def __init__(self, config: DzengiProbeConfig) -> None:
self.config = config
async def run(
self,
*,
samples: int = 300,
interval_seconds: float = 0.2,
) -> Path:
output_path = (
self.config.output_dir
/ "reports"
/ f"ticker24hr_update_{self.config.symbol_file_name}.csv"
)
output_path.parent.mkdir(parents=True, exist_ok=True)
rows: list[Ticker24hrSample] = []
for index in range(1, samples + 1):
payload = self._get_ticker24hr()
rows.append(
Ticker24hrSample(
sample=index,
local_time_ms=int(time.time() * 1000),
ask_price=self._decimal_str(payload.get("askPrice")),
bid_price=self._decimal_str(payload.get("bidPrice")),
last_price=self._decimal_str(payload.get("lastPrice")),
close_time=self._int_or_none(payload.get("closeTime")),
)
)
await asyncio.sleep(interval_seconds)
self._save_csv(output_path, rows)
return output_path
def _get_ticker24hr(self) -> dict:
query = urlencode({"symbol": self.config.symbol})
url = (
f"{self.config.base_url}"
f"/api/{self.config.api_version}/ticker/24hr"
f"?{query}"
)
request = Request(
url=url,
method="GET",
headers={
"Accept": "application/json",
"User-Agent": "dzentra-dzengi-probe/1.0",
},
)
with urlopen(request, timeout=self.config.timeout_seconds) as response:
return json.loads(response.read().decode("utf-8"))
def _decimal_str(self, value: object) -> str | None:
if value is None:
return None
return str(Decimal(str(value)))
def _int_or_none(self, value: object) -> int | None:
if value is None:
return None
return int(str(value))
def _save_csv(self, path: Path, rows: list[Ticker24hrSample]) -> None:
with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerow(
[
"sample",
"local_time_ms",
"close_time",
"ask_price",
"bid_price",
"last_price",
"ask_changed",
"bid_changed",
"last_changed",
"close_time_changed",
"last_equals_bid",
]
)
previous: Ticker24hrSample | None = None
for row in rows:
writer.writerow(
[
row.sample,
row.local_time_ms,
row.close_time,
row.ask_price,
row.bid_price,
row.last_price,
previous is not None and row.ask_price != previous.ask_price,
previous is not None and row.bid_price != previous.bid_price,
previous is not None and row.last_price != previous.last_price,
previous is not None and row.close_time != previous.close_time,
row.last_price == row.bid_price,
]
)
previous = row