analysis-poly 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .web import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1,587 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import csv
5
+ import json
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Protocol
10
+ from datetime import datetime, timezone
11
+
12
+ from loguru import logger
13
+
14
+ from .market_cache import MarketMetadataCache
15
+ from .market_result_cache import AddressMarketResultCache
16
+ from .models import (
17
+ AnalysisReport,
18
+ AnalysisRequest,
19
+ CurvePoint,
20
+ MarketReport,
21
+ SummaryStats,
22
+ WarningItem,
23
+ )
24
+ from .polymarket_client import PolymarketApiClient
25
+ from .profit_engine import PnlDelta, ProfitEngine, build_curve
26
+ from .slugs import MarketSlugSpec, generate_market_slug_specs
27
+
28
+ MARKET_FETCH_CONCURRENCY_DEFAULT = 10
29
+ MARKET_TIMESTAMP_CHUNK_SIZE_DEFAULT = 20
30
+ MARKET_RESULT_CACHE_RECENT_WINDOW_SEC = 30 * 60
31
+
32
+
33
+ @dataclass
34
+ class _MarketProcessResult:
35
+ market_slug: str
36
+ market_report: MarketReport
37
+ market_report_no_fee: MarketReport
38
+ deltas: list[PnlDelta]
39
+ deltas_no_fee: list[PnlDelta]
40
+ warnings: list[WarningItem]
41
+ cache_updated: bool = False
42
+
43
+
44
+ class AnalyzerHooks(Protocol):
45
+ async def on_run_started(self, total_markets: int) -> None: ...
46
+
47
+ async def on_progress(self, current: int, total: int, market_slug: str) -> None: ...
48
+
49
+ async def on_warning(self, warning: WarningItem) -> None: ...
50
+
51
+ async def on_total_point(self, timestamp: int, delta: float, cumulative: float) -> None: ...
52
+
53
+ async def on_market_point(
54
+ self, market_slug: str, timestamp: int, delta: float, cumulative: float
55
+ ) -> None: ...
56
+
57
+ async def on_total_point_no_fee(self, timestamp: int, delta: float, cumulative: float) -> None: ...
58
+
59
+ async def on_market_point_no_fee(
60
+ self, market_slug: str, timestamp: int, delta: float, cumulative: float
61
+ ) -> None: ...
62
+
63
+
64
+ class NullHooks:
65
+ async def on_run_started(self, total_markets: int) -> None:
66
+ return
67
+
68
+ async def on_progress(self, current: int, total: int, market_slug: str) -> None:
69
+ return
70
+
71
+ async def on_warning(self, warning: WarningItem) -> None:
72
+ return
73
+
74
+ async def on_total_point(self, timestamp: int, delta: float, cumulative: float) -> None:
75
+ return
76
+
77
+ async def on_market_point(
78
+ self, market_slug: str, timestamp: int, delta: float, cumulative: float
79
+ ) -> None:
80
+ return
81
+
82
+ async def on_total_point_no_fee(self, timestamp: int, delta: float, cumulative: float) -> None:
83
+ return
84
+
85
+ async def on_market_point_no_fee(
86
+ self, market_slug: str, timestamp: int, delta: float, cumulative: float
87
+ ) -> None:
88
+ return
89
+
90
+
91
+ class PolymarketProfitAnalyzer:
92
+ def __init__(self):
93
+ self._market_cache = MarketMetadataCache()
94
+ self._market_result_cache = AddressMarketResultCache()
95
+ self._market_fetch_concurrency = MARKET_FETCH_CONCURRENCY_DEFAULT
96
+ self._timestamp_chunk_size = MARKET_TIMESTAMP_CHUNK_SIZE_DEFAULT
97
+
98
+ async def run(
99
+ self,
100
+ req: AnalysisRequest,
101
+ stop_event: asyncio.Event | None = None,
102
+ hooks: AnalyzerHooks | None = None,
103
+ ) -> AnalysisReport:
104
+ stop_event = stop_event or asyncio.Event()
105
+ hooks = hooks or NullHooks()
106
+
107
+ client = PolymarketApiClient(timeout_sec=req.request_timeout_sec)
108
+ engine = ProfitEngine(
109
+ fee_rate_bps=req.fee_rate_bps,
110
+ maker_reward_ratio=req.maker_reward_ratio,
111
+ missing_cost_warn_qty=req.missing_cost_warn_qty,
112
+ )
113
+ engine_no_fee = ProfitEngine(
114
+ fee_rate_bps=req.fee_rate_bps,
115
+ maker_reward_ratio=req.maker_reward_ratio,
116
+ missing_cost_warn_qty=req.missing_cost_warn_qty,
117
+ charge_taker_fee=False,
118
+ )
119
+
120
+ all_warnings: list[WarningItem] = []
121
+ market_reports: list[MarketReport] = []
122
+ total_deltas: list[PnlDelta] = []
123
+ total_deltas_no_fee: list[PnlDelta] = []
124
+ market_deltas: dict[str, list[PnlDelta]] = defaultdict(list)
125
+ market_deltas_no_fee: dict[str, list[PnlDelta]] = defaultdict(list)
126
+
127
+ total_by_ts: dict[int, float] = defaultdict(float)
128
+ market_by_ts: dict[str, dict[int, float]] = defaultdict(lambda: defaultdict(float))
129
+ total_by_ts_no_fee: dict[int, float] = defaultdict(float)
130
+ market_by_ts_no_fee: dict[str, dict[int, float]] = defaultdict(lambda: defaultdict(float))
131
+ address_market_cache = self._market_result_cache.load(req.address)
132
+ result_cache_dirty = False
133
+
134
+ try:
135
+ specs = generate_market_slug_specs(req.symbols, req.intervals, req.start_ts, req.end_ts)
136
+ total_markets = len(specs)
137
+ spec_chunks = _chunk_specs_by_timestamp(specs, self._timestamp_chunk_size)
138
+ logger.info(
139
+ "analyzer prepared spec_count={} timestamp_chunks={} market_fetch_concurrency={} process_concurrency={}",
140
+ len(specs),
141
+ len(spec_chunks),
142
+ self._market_fetch_concurrency,
143
+ max(1, req.concurrency),
144
+ )
145
+ await hooks.on_run_started(total_markets)
146
+
147
+ process_concurrency = max(1, req.concurrency)
148
+ processed_count = 0
149
+
150
+ for spec_chunk in spec_chunks:
151
+ if stop_event.is_set():
152
+ break
153
+
154
+ chunk_slugs = [s.slug for s in spec_chunk]
155
+ chunk_fetch_results = await self._fetch_markets_with_status(
156
+ client,
157
+ chunk_slugs,
158
+ self._market_fetch_concurrency,
159
+ )
160
+ chunk_markets = [market for _, market in chunk_fetch_results if market is not None]
161
+ chunk_markets.sort(key=lambda m: _market_order_key(m.slug))
162
+
163
+ for batch_start in range(0, len(chunk_markets), process_concurrency):
164
+ if stop_event.is_set():
165
+ break
166
+
167
+ batch_markets = chunk_markets[batch_start : batch_start + process_concurrency]
168
+ batch_results = await asyncio.gather(
169
+ *(
170
+ self._process_single_market(
171
+ client=client,
172
+ engine=engine,
173
+ engine_no_fee=engine_no_fee,
174
+ address=req.address,
175
+ address_market_cache=address_market_cache,
176
+ req=req,
177
+ market=market,
178
+ )
179
+ for market in batch_markets
180
+ )
181
+ )
182
+ # Keep push order stable by market timestamp inside one concurrent batch.
183
+ batch_results.sort(key=lambda x: _market_order_key(x.market_slug))
184
+
185
+ for result in batch_results:
186
+ if stop_event.is_set():
187
+ break
188
+
189
+ processed_count += 1
190
+ has_trade_activity = _has_market_trade_activity(result.market_report)
191
+ if has_trade_activity:
192
+ market_reports.append(result.market_report)
193
+ total_deltas.extend(result.deltas)
194
+ total_deltas_no_fee.extend(result.deltas_no_fee)
195
+ if result.deltas:
196
+ market_deltas[result.market_slug].extend(result.deltas)
197
+ if result.deltas_no_fee:
198
+ market_deltas_no_fee[result.market_slug].extend(result.deltas_no_fee)
199
+ else:
200
+ logger.debug("skip market without trades in output slug={}", result.market_slug)
201
+
202
+ for warning in result.warnings:
203
+ all_warnings.append(warning)
204
+ await hooks.on_warning(warning)
205
+ if result.cache_updated:
206
+ result_cache_dirty = True
207
+
208
+ for delta in result.deltas:
209
+ total_by_ts[delta.timestamp] += delta.delta_pnl_usdc
210
+ market_by_ts[delta.market_slug][delta.timestamp] += delta.delta_pnl_usdc
211
+
212
+ total_cumulative = _cumulative_at(total_by_ts, delta.timestamp)
213
+ market_cumulative = _cumulative_at(market_by_ts[delta.market_slug], delta.timestamp)
214
+
215
+ await hooks.on_total_point(
216
+ delta.timestamp,
217
+ delta.delta_pnl_usdc,
218
+ total_cumulative,
219
+ )
220
+ await hooks.on_market_point(
221
+ delta.market_slug,
222
+ delta.timestamp,
223
+ delta.delta_pnl_usdc,
224
+ market_cumulative,
225
+ )
226
+
227
+ for delta in result.deltas_no_fee:
228
+ total_by_ts_no_fee[delta.timestamp] += delta.delta_pnl_usdc
229
+ market_by_ts_no_fee[delta.market_slug][delta.timestamp] += delta.delta_pnl_usdc
230
+
231
+ total_cumulative_no_fee = _cumulative_at(total_by_ts_no_fee, delta.timestamp)
232
+ market_cumulative_no_fee = _cumulative_at(
233
+ market_by_ts_no_fee[delta.market_slug], delta.timestamp
234
+ )
235
+
236
+ await hooks.on_total_point_no_fee(
237
+ delta.timestamp,
238
+ delta.delta_pnl_usdc,
239
+ total_cumulative_no_fee,
240
+ )
241
+ await hooks.on_market_point_no_fee(
242
+ delta.market_slug,
243
+ delta.timestamp,
244
+ delta.delta_pnl_usdc,
245
+ market_cumulative_no_fee,
246
+ )
247
+
248
+ await hooks.on_progress(processed_count, total_markets, result.market_slug)
249
+
250
+ missing_count = sum(1 for _, market in chunk_fetch_results if market is None)
251
+ if missing_count > 0:
252
+ processed_count += missing_count
253
+ await hooks.on_progress(processed_count, total_markets, chunk_slugs[-1])
254
+
255
+ total_curve = [
256
+ CurvePoint(
257
+ timestamp=ts,
258
+ delta_realized_pnl_usdc=round(delta, 10),
259
+ cumulative_realized_pnl_usdc=round(cum, 10),
260
+ )
261
+ for ts, delta, cum in build_curve(total_deltas)
262
+ ]
263
+
264
+ market_curves: dict[str, list[CurvePoint]] = {}
265
+ for market_slug, deltas in market_deltas.items():
266
+ if not deltas:
267
+ continue
268
+ market_curves[market_slug] = [
269
+ CurvePoint(
270
+ timestamp=ts,
271
+ delta_realized_pnl_usdc=round(delta, 10),
272
+ cumulative_realized_pnl_usdc=round(cum, 10),
273
+ )
274
+ for ts, delta, cum in build_curve(deltas)
275
+ ]
276
+
277
+ total_curve_no_fee = [
278
+ CurvePoint(
279
+ timestamp=ts,
280
+ delta_realized_pnl_usdc=round(delta, 10),
281
+ cumulative_realized_pnl_usdc=round(cum, 10),
282
+ )
283
+ for ts, delta, cum in build_curve(total_deltas_no_fee)
284
+ ]
285
+
286
+ market_curves_no_fee: dict[str, list[CurvePoint]] = {}
287
+ for market_slug, deltas in market_deltas_no_fee.items():
288
+ if not deltas:
289
+ continue
290
+ market_curves_no_fee[market_slug] = [
291
+ CurvePoint(
292
+ timestamp=ts,
293
+ delta_realized_pnl_usdc=round(delta, 10),
294
+ cumulative_realized_pnl_usdc=round(cum, 10),
295
+ )
296
+ for ts, delta, cum in build_curve(deltas)
297
+ ]
298
+
299
+ summary = SummaryStats(
300
+ total_realized_pnl_usdc=round(sum(m.realized_pnl_usdc for m in market_reports), 10),
301
+ total_taker_fee_usdc=round(sum(m.taker_fee_usdc for m in market_reports), 10),
302
+ total_maker_reward_usdc=round(sum(m.maker_reward_usdc for m in market_reports), 10),
303
+ markets_total=total_markets,
304
+ markets_processed=len(market_reports),
305
+ )
306
+
307
+ report = AnalysisReport(
308
+ request=req,
309
+ summary=summary,
310
+ markets=sorted(market_reports, key=lambda x: x.market_slug),
311
+ total_curve=total_curve,
312
+ market_curves=market_curves,
313
+ total_curve_no_fee=total_curve_no_fee,
314
+ market_curves_no_fee=market_curves_no_fee,
315
+ warnings=all_warnings,
316
+ is_partial=stop_event.is_set() and len(market_reports) < total_markets,
317
+ )
318
+
319
+ return report
320
+ finally:
321
+ if result_cache_dirty:
322
+ try:
323
+ self._market_result_cache.save(req.address, address_market_cache)
324
+ except Exception as exc: # noqa: BLE001
325
+ logger.warning("market result cache save failed address={} error={}", req.address, exc)
326
+ await client.aclose()
327
+
328
+ async def _fetch_markets_with_status(
329
+ self,
330
+ client: PolymarketApiClient,
331
+ slugs: list[str],
332
+ concurrency: int,
333
+ ) -> list[tuple[str, object | None]]:
334
+ sem = asyncio.Semaphore(max(1, concurrency))
335
+
336
+ async def fetch(slug: str):
337
+ async with sem:
338
+ market = await self._fetch_market_with_cache(client, slug)
339
+ return slug, market
340
+
341
+ return await asyncio.gather(*(fetch(s) for s in slugs))
342
+
343
+ async def _fetch_market_with_cache(
344
+ self,
345
+ client: PolymarketApiClient,
346
+ slug: str,
347
+ ):
348
+ now_ts = int(datetime.now(timezone.utc).timestamp())
349
+ use_cache = self._market_cache.is_cache_eligible(slug, now_ts=now_ts)
350
+
351
+ if use_cache:
352
+ cached = self._market_cache.get(slug)
353
+ if cached is not None:
354
+ logger.debug("market cache hit slug={}", slug)
355
+ return cached
356
+
357
+ market = await client.get_market_by_slug(slug)
358
+ if market is None:
359
+ logger.warning("market not found slug={}", slug)
360
+ return None
361
+
362
+ if market is not None and use_cache:
363
+ self._market_cache.set(slug, market)
364
+ logger.debug("market cache write slug={}", slug)
365
+
366
+ return market
367
+
368
+ async def _process_single_market(
369
+ self,
370
+ client: PolymarketApiClient,
371
+ engine: ProfitEngine,
372
+ engine_no_fee: ProfitEngine,
373
+ address: str,
374
+ address_market_cache: dict[str, dict],
375
+ req: AnalysisRequest,
376
+ market,
377
+ ) -> _MarketProcessResult:
378
+ now_ts = int(datetime.now(timezone.utc).timestamp())
379
+ use_result_cache = _is_market_result_cache_eligible(
380
+ market.slug,
381
+ now_ts=now_ts,
382
+ recent_window_sec=MARKET_RESULT_CACHE_RECENT_WINDOW_SEC,
383
+ )
384
+
385
+ if use_result_cache:
386
+ cached_payload = address_market_cache.get(market.slug)
387
+ if cached_payload is not None:
388
+ cached_result = _result_from_cache_payload(market.slug, cached_payload)
389
+ if cached_result is not None:
390
+ logger.debug("market result cache hit address={} slug={}", address, market.slug)
391
+ return cached_result
392
+ logger.warning("market result cache invalid address={} slug={}", address, market.slug)
393
+
394
+ taker_trades, all_trades, split_acts, redeem_acts = await asyncio.gather(
395
+ client.get_trades(req.address, market.condition_id, True, req.page_limit),
396
+ client.get_trades(req.address, market.condition_id, False, req.page_limit),
397
+ client.get_activity(req.address, market.condition_id, "SPLIT", req.page_limit),
398
+ client.get_activity(req.address, market.condition_id, "REDEEM", req.page_limit),
399
+ )
400
+
401
+ market_report, deltas, warnings = engine.process_market(
402
+ market=market,
403
+ taker_trades=taker_trades,
404
+ all_trades=all_trades,
405
+ split_activities=split_acts,
406
+ redeem_activities=redeem_acts,
407
+ )
408
+ market_report_no_fee, deltas_no_fee, _ = engine_no_fee.process_market(
409
+ market=market,
410
+ taker_trades=taker_trades,
411
+ all_trades=all_trades,
412
+ split_activities=split_acts,
413
+ redeem_activities=redeem_acts,
414
+ )
415
+ result = _MarketProcessResult(
416
+ market_slug=market.slug,
417
+ market_report=market_report,
418
+ market_report_no_fee=market_report_no_fee,
419
+ deltas=deltas,
420
+ deltas_no_fee=deltas_no_fee,
421
+ warnings=warnings,
422
+ )
423
+ if use_result_cache:
424
+ new_payload = _result_to_cache_payload(result)
425
+ if address_market_cache.get(market.slug) != new_payload:
426
+ address_market_cache[market.slug] = new_payload
427
+ result.cache_updated = True
428
+ return result
429
+
430
+ def save_json(self, report: AnalysisReport, path: str | None = None) -> str:
431
+ output_dir = Path(report.request.output_dir)
432
+ output_dir.mkdir(parents=True, exist_ok=True)
433
+
434
+ if not path:
435
+ suffix = "partial" if report.is_partial else "final"
436
+ path = str(output_dir / f"pnl_summary_{suffix}.json")
437
+
438
+ Path(path).write_text(report.model_dump_json(indent=2), encoding="utf-8")
439
+ return path
440
+
441
+ def save_total_curve_csv(self, report: AnalysisReport, path: str | None = None) -> str:
442
+ output_dir = Path(report.request.output_dir)
443
+ output_dir.mkdir(parents=True, exist_ok=True)
444
+
445
+ if not path:
446
+ suffix = "partial" if report.is_partial else "final"
447
+ path = str(output_dir / f"pnl_total_curve_{suffix}.csv")
448
+
449
+ with Path(path).open("w", newline="", encoding="utf-8") as fp:
450
+ writer = csv.writer(fp)
451
+ writer.writerow(["timestamp", "delta_realized_pnl_usdc", "cumulative_realized_pnl_usdc"])
452
+ for p in report.total_curve:
453
+ writer.writerow([p.timestamp, p.delta_realized_pnl_usdc, p.cumulative_realized_pnl_usdc])
454
+ return path
455
+
456
+ def save_market_curve_csv(self, report: AnalysisReport, path: str | None = None) -> str:
457
+ output_dir = Path(report.request.output_dir)
458
+ output_dir.mkdir(parents=True, exist_ok=True)
459
+
460
+ if not path:
461
+ suffix = "partial" if report.is_partial else "final"
462
+ path = str(output_dir / f"pnl_market_curve_{suffix}.csv")
463
+
464
+ with Path(path).open("w", newline="", encoding="utf-8") as fp:
465
+ writer = csv.writer(fp)
466
+ writer.writerow(
467
+ [
468
+ "market_slug",
469
+ "timestamp",
470
+ "delta_realized_pnl_usdc",
471
+ "cumulative_realized_pnl_usdc",
472
+ ]
473
+ )
474
+ for market_slug, points in report.market_curves.items():
475
+ for p in points:
476
+ writer.writerow(
477
+ [
478
+ market_slug,
479
+ p.timestamp,
480
+ p.delta_realized_pnl_usdc,
481
+ p.cumulative_realized_pnl_usdc,
482
+ ]
483
+ )
484
+ return path
485
+
486
+ def save_curve_csv(self, report: AnalysisReport, path: str | None = None) -> str:
487
+ return self.save_total_curve_csv(report, path)
488
+
489
+
490
+
491
+ def _cumulative_at(by_ts: dict[int, float], ts: int) -> float:
492
+ cumulative = 0.0
493
+ for key in sorted(by_ts.keys()):
494
+ if key > ts:
495
+ break
496
+ cumulative += by_ts[key]
497
+ return cumulative
498
+
499
+
500
+ def _market_order_key(slug: str) -> tuple[int, str]:
501
+ try:
502
+ return int(str(slug).rsplit("-", 1)[-1]), slug
503
+ except Exception: # noqa: BLE001
504
+ return 10**18, slug
505
+
506
+
507
+ def _has_market_trade_activity(market_report: MarketReport) -> bool:
508
+ return any(token.trade_count > 0 for token in market_report.tokens)
509
+
510
+
511
+ def _chunk_specs_by_timestamp(
512
+ specs: list[MarketSlugSpec], timestamps_per_chunk: int
513
+ ) -> list[list[MarketSlugSpec]]:
514
+ if not specs:
515
+ return []
516
+ chunk_size = max(1, int(timestamps_per_chunk))
517
+
518
+ ts_to_specs: dict[int, list[MarketSlugSpec]] = defaultdict(list)
519
+ ts_order: list[int] = []
520
+ for spec in specs:
521
+ if spec.timestamp not in ts_to_specs:
522
+ ts_order.append(spec.timestamp)
523
+ ts_to_specs[spec.timestamp].append(spec)
524
+
525
+ chunks: list[list[MarketSlugSpec]] = []
526
+ for start in range(0, len(ts_order), chunk_size):
527
+ ts_slice = ts_order[start : start + chunk_size]
528
+ chunk_specs: list[MarketSlugSpec] = []
529
+ for ts in ts_slice:
530
+ chunk_specs.extend(ts_to_specs[ts])
531
+ chunks.append(chunk_specs)
532
+ return chunks
533
+
534
+
535
+ def _result_to_cache_payload(result: _MarketProcessResult) -> dict:
536
+ return {
537
+ "market_slug": result.market_slug,
538
+ "market_report": result.market_report.model_dump(),
539
+ "market_report_no_fee": result.market_report_no_fee.model_dump(),
540
+ "deltas": [_delta_to_dict(d) for d in result.deltas],
541
+ "deltas_no_fee": [_delta_to_dict(d) for d in result.deltas_no_fee],
542
+ "warnings": [w.model_dump() for w in result.warnings],
543
+ }
544
+
545
+
546
+ def _result_from_cache_payload(slug: str, payload: dict) -> _MarketProcessResult | None:
547
+ try:
548
+ market_report = MarketReport.model_validate(payload["market_report"])
549
+ market_report_no_fee = MarketReport.model_validate(payload["market_report_no_fee"])
550
+ deltas = [_delta_from_dict(d) for d in payload.get("deltas", [])]
551
+ deltas_no_fee = [_delta_from_dict(d) for d in payload.get("deltas_no_fee", [])]
552
+ warnings = [WarningItem.model_validate(w) for w in payload.get("warnings", [])]
553
+ return _MarketProcessResult(
554
+ market_slug=slug,
555
+ market_report=market_report,
556
+ market_report_no_fee=market_report_no_fee,
557
+ deltas=deltas,
558
+ deltas_no_fee=deltas_no_fee,
559
+ warnings=warnings,
560
+ )
561
+ except Exception: # noqa: BLE001
562
+ return None
563
+
564
+
565
+ def _delta_to_dict(delta: PnlDelta) -> dict:
566
+ return {
567
+ "timestamp": int(delta.timestamp),
568
+ "market_slug": str(delta.market_slug),
569
+ "token_id": str(delta.token_id),
570
+ "delta_pnl_usdc": float(delta.delta_pnl_usdc),
571
+ }
572
+
573
+
574
+ def _delta_from_dict(payload: dict) -> PnlDelta:
575
+ return PnlDelta(
576
+ timestamp=int(payload["timestamp"]),
577
+ market_slug=str(payload["market_slug"]),
578
+ token_id=str(payload["token_id"]),
579
+ delta_pnl_usdc=float(payload["delta_pnl_usdc"]),
580
+ )
581
+
582
+
583
+ def _is_market_result_cache_eligible(slug: str, now_ts: int, recent_window_sec: int) -> bool:
584
+ market_ts = _market_order_key(slug)[0]
585
+ if market_ts >= 10**18:
586
+ return False
587
+ return (now_ts - market_ts) > recent_window_sec
analysis_poly/cli.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ if __package__ in (None, ""):
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
10
+ from analysis_poly.main import run
11
+ else:
12
+ from .main import run
13
+
14
+
15
+ def _build_arg_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(description="Run analysis-poly web server")
17
+ parser.add_argument("--host", default="0.0.0.0", help="Bind host for web server")
18
+ parser.add_argument("--port", type=int, default=8000, help="Bind port for web server")
19
+ return parser
20
+
21
+
22
+ def main() -> None:
23
+ args = _build_arg_parser().parse_args()
24
+ run(host=args.host, port=args.port)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from loguru import logger
6
+
7
+ _CONFIGURED = False
8
+
9
+
10
+ def configure_logging(level: str = "INFO") -> None:
11
+ global _CONFIGURED
12
+ if _CONFIGURED:
13
+ return
14
+
15
+ logger.remove()
16
+ logger.add(
17
+ sys.stderr,
18
+ level=level,
19
+ colorize=True,
20
+ backtrace=False,
21
+ diagnose=False,
22
+ format=(
23
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
24
+ "<level>{level: <8}</level> | "
25
+ "<cyan>{name}:{function}:{line}</cyan> - "
26
+ "<level>{message}</level>"
27
+ ),
28
+ )
29
+ _CONFIGURED = True
analysis_poly/main.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ if __package__ in (None, ""):
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
8
+ from analysis_poly.logging_config import configure_logging
9
+ from analysis_poly.web import app
10
+ else:
11
+ from .logging_config import configure_logging
12
+ from .web import app
13
+
14
+
15
+ def run(host: str = "0.0.0.0", port: int = 8000) -> None:
16
+ import uvicorn
17
+
18
+ configure_logging()
19
+ uvicorn.run(app, host=host, port=port)
20
+
21
+
22
+ def main() -> None:
23
+ run()
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()