update 03-28 11:38

This commit is contained in:
2026-03-28 11:38:53 +09:00
parent 49e033f373
commit 710949f034
9 changed files with 138 additions and 38 deletions

View File

@@ -34,6 +34,26 @@ class TradeDB:
self._ensure_tables()
self._log.info("database_ready")
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def close(self) -> None:
"""Flush WAL and close the underlying SQLite connection."""
try:
if self._db and hasattr(self._db, "conn") and self._db.conn:
self._db.conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
self._db.conn.close()
self._log.info("database_closed")
except Exception:
self._log.exception("database_close_error")
def __enter__(self):
return self
def __exit__(self, *exc):
self.close()
# ------------------------------------------------------------------
# Schema
# ------------------------------------------------------------------
@@ -321,3 +341,23 @@ class TradeDB:
[today],
).fetchone()
return int(row[0])
# ------------------------------------------------------------------
# Maintenance
# ------------------------------------------------------------------
def periodic_maintenance(self, retention_days: int = 7) -> None:
"""Prune old time-series data and checkpoint WAL."""
cutoff = time.time() - retention_days * 86400
pruned = {}
for table in ("oracle_snapshots", "balance_history"):
if table in self._db.table_names():
before = self._db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
self._db.execute(f"DELETE FROM {table} WHERE timestamp < ?", [cutoff])
after = self._db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
pruned[table] = before - after
try:
self._db.conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
except Exception:
pass
self._log.info("db_maintenance", pruned=pruned, retention_days=retention_days)

View File

@@ -147,8 +147,8 @@ class ArbBot:
self._signal_aggregator: Optional[SignalAggregator] = None
self._telegram: Optional[TelegramNotifier] = None
# Discovered markets cache
self._active_markets: list[ActiveMarket] = []
# Discovered markets cache (keyed by condition_id to prevent duplicates)
self._active_markets: dict[str, ActiveMarket] = {}
# Orderbook state
self._orderbooks: dict[str, OrderBookSnapshot] = {}
@@ -302,7 +302,8 @@ class ArbBot:
def _on_new_markets(self, markets: list[ActiveMarket]) -> None:
"""Callback invoked when MarketDiscovery finds new markets."""
self._active_markets.extend(markets)
for mkt in markets:
self._active_markets[mkt.condition_id] = mkt
if self._tracker is not None:
_link_markets_to_tracker(markets, self._tracker, self._log)
if self._poly_feed is not None:
@@ -573,7 +574,7 @@ class ArbBot:
)
self._log.info("running_initial_discovery")
initial_markets = await self._discovery.discover()
self._active_markets = initial_markets
self._active_markets = {m.condition_id: m for m in initial_markets}
self._log.info(
"initial_discovery_complete",
count=len(initial_markets),
@@ -706,6 +707,14 @@ class ArbBot:
)
await self._telegram.close()
# Close discovery session
if self._discovery is not None:
await self._discovery._close_session_if_owned()
# Close database connection
if self._db is not None:
self._db.close()
self._log.info("bot_stopped")

View File

@@ -54,6 +54,7 @@ class OracleMonitor:
}
def __init__(self, rpc_url: Optional[str] = None) -> None:
from collections import deque
self._rpc_url = rpc_url or "https://polygon.drpc.org"
self._w3 = None
self._contracts: dict[str, object] = {}
@@ -61,10 +62,11 @@ class OracleMonitor:
self._last_oracle_prices: dict[str, float] = {}
self._last_oracle_timestamps: dict[str, float] = {}
self._last_oracle_round_ids: dict[str, int] = {}
self._update_intervals: dict[str, list[float]] = {
"BTC": [], "ETH": [], "SOL": []
self._update_intervals: dict[str, deque[float]] = {
asset: deque(maxlen=100) for asset in self.FEEDS
}
self._initialized = False
self._shutdown_event: Optional[asyncio.Event] = None
async def initialize(self) -> bool:
"""Initialize web3 connection and contract instances."""
@@ -106,7 +108,10 @@ class OracleMonitor:
try:
contract = self._contracts[asset]
result = contract.functions.latestRoundData().call()
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, contract.functions.latestRoundData().call
)
round_id, answer, started_at, updated_at, answered_in_round = result
decimals = self._decimals.get(asset, 8)
@@ -127,11 +132,7 @@ class OracleMonitor:
prev_ts = self._last_oracle_timestamps.get(asset)
if prev_ts is not None and timestamp > prev_ts:
interval = timestamp - prev_ts
intervals = self._update_intervals[asset]
intervals.append(interval)
# Keep last 100 intervals
if len(intervals) > 100:
intervals.pop(0)
self._update_intervals[asset].append(interval)
self._last_oracle_prices[asset] = price
self._last_oracle_timestamps[asset] = timestamp
@@ -159,19 +160,33 @@ class OracleMonitor:
return None
return (cex_price - oracle_price) / oracle_price * 100
async def poll_loop(self, interval: float = 5.0) -> None:
async def poll_loop(
self, interval: float = 5.0, shutdown_event: Optional[asyncio.Event] = None
) -> None:
"""Continuously poll oracle prices."""
if not self._initialized:
log.warning("oracle_poll_not_initialized")
return
while True:
if shutdown_event is not None:
self._shutdown_event = shutdown_event
while not (self._shutdown_event and self._shutdown_event.is_set()):
for asset in self.FEEDS:
try:
await self.get_oracle_price(asset)
except Exception:
log.exception("oracle_poll_error", asset=asset)
await asyncio.sleep(interval)
try:
if self._shutdown_event:
await asyncio.wait_for(
self._shutdown_event.wait(), timeout=interval
)
break
else:
await asyncio.sleep(interval)
except asyncio.TimeoutError:
pass
def get_stats(self) -> dict:
return {