analysis-poly 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/PKG-INFO +5 -5
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/README.md +4 -4
- analysis_poly-0.1.2/analysis_poly/activity_discovery.py +230 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/analyzer.py +336 -124
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/main.py +1 -1
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/models.py +40 -23
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/open_with_params.py +3 -9
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/polymarket_client.py +34 -2
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/profit_engine.py +169 -56
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/run_manager.py +140 -5
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/static/dist/app.css +1 -1
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/static/dist/app.js +96 -96
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/templates/index.html +1 -1
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/web.py +14 -3
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/PKG-INFO +5 -5
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/SOURCES.txt +4 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/pyproject.toml +2 -1
- analysis_poly-0.1.2/tests/test_analyzer_csv.py +72 -0
- analysis_poly-0.1.2/tests/test_analyzer_discovery.py +228 -0
- analysis_poly-0.1.2/tests/test_analyzer_result_cache_compat.py +56 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_models_defaults.py +10 -6
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_open_with_params.py +3 -7
- analysis_poly-0.1.2/tests/test_profit_engine.py +475 -0
- analysis_poly-0.1.2/tests/test_run_manager.py +145 -0
- analysis_poly-0.1.0/tests/test_profit_engine.py +0 -173
- analysis_poly-0.1.0/tests/test_run_manager.py +0 -75
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/__init__.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/cli.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/logging_config.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/market_cache.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/market_result_cache.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly/slugs.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/dependency_links.txt +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/entry_points.txt +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/requires.txt +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/analysis_poly.egg-info/top_level.txt +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/setup.cfg +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_chunking.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_filter_empty_market.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_analyzer_market_cache.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_market_cache.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_market_result_cache.py +0 -0
- {analysis_poly-0.1.0 → analysis_poly-0.1.2}/tests/test_slugs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analysis-poly
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Polymarket real-profit web analyzer
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -27,7 +27,8 @@ Polymarket real-profit analyzer with a web UI.
|
|
|
27
27
|
|
|
28
28
|
## Scope (Important)
|
|
29
29
|
|
|
30
|
-
- This tool is primarily designed for Polymarket crypto `updown` markets with taker fees
|
|
30
|
+
- This tool is primarily designed for Polymarket crypto `updown` markets with taker fees.
|
|
31
|
+
- The dashboard first discovers participated slugs from Polymarket user activity within the selected time range, then replays only the matched markets.
|
|
31
32
|
- It is **not** intended as a universal PnL engine for all Polymarket market types.
|
|
32
33
|
- Main purpose: quantify and visualize the impact of trading fees on real profitability (`Net PnL` vs `No-Fee PnL`).
|
|
33
34
|
|
|
@@ -48,7 +49,7 @@ After install:
|
|
|
48
49
|
|
|
49
50
|
```bash
|
|
50
51
|
analysis-poly
|
|
51
|
-
analysis-poly-open --address 0xabc --
|
|
52
|
+
analysis-poly-open --address 0xabc --keywords updown,15m
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
## Run
|
|
@@ -67,8 +68,7 @@ Use a standalone script to start server, open browser, and pass params in URL.
|
|
|
67
68
|
```bash
|
|
68
69
|
uv run -m analysis_poly.open_with_params \
|
|
69
70
|
--address 0xabc \
|
|
70
|
-
--
|
|
71
|
-
--intervals 5,15 \
|
|
71
|
+
--keywords updown,15m \
|
|
72
72
|
--start-time "2026-03-01 00:00" \
|
|
73
73
|
--end-time "2026-03-02 00:00" \
|
|
74
74
|
--concurrency 8
|
|
@@ -11,7 +11,8 @@ Polymarket real-profit analyzer with a web UI.
|
|
|
11
11
|
|
|
12
12
|
## Scope (Important)
|
|
13
13
|
|
|
14
|
-
- This tool is primarily designed for Polymarket crypto `updown` markets with taker fees
|
|
14
|
+
- This tool is primarily designed for Polymarket crypto `updown` markets with taker fees.
|
|
15
|
+
- The dashboard first discovers participated slugs from Polymarket user activity within the selected time range, then replays only the matched markets.
|
|
15
16
|
- It is **not** intended as a universal PnL engine for all Polymarket market types.
|
|
16
17
|
- Main purpose: quantify and visualize the impact of trading fees on real profitability (`Net PnL` vs `No-Fee PnL`).
|
|
17
18
|
|
|
@@ -32,7 +33,7 @@ After install:
|
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
35
|
analysis-poly
|
|
35
|
-
analysis-poly-open --address 0xabc --
|
|
36
|
+
analysis-poly-open --address 0xabc --keywords updown,15m
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
## Run
|
|
@@ -51,8 +52,7 @@ Use a standalone script to start server, open browser, and pass params in URL.
|
|
|
51
52
|
```bash
|
|
52
53
|
uv run -m analysis_poly.open_with_params \
|
|
53
54
|
--address 0xabc \
|
|
54
|
-
--
|
|
55
|
-
--intervals 5,15 \
|
|
55
|
+
--keywords updown,15m \
|
|
56
56
|
--start-time "2026-03-01 00:00" \
|
|
57
57
|
--end-time "2026-03-02 00:00" \
|
|
58
58
|
--concurrency 8
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .models import WarningItem
|
|
7
|
+
|
|
8
|
+
DISCOVERY_ACTIVITY_TYPES = ("TRADE", "SPLIT", "REDEEM")
|
|
9
|
+
DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX = 500
|
|
10
|
+
DISCOVERY_ACTIVITY_OFFSET_MAX = 10000
|
|
11
|
+
SECONDS_PER_DAY = 24 * 60 * 60
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DiscoveredMarket:
|
|
16
|
+
slug: str
|
|
17
|
+
condition_id: str
|
|
18
|
+
first_activity_ts: int
|
|
19
|
+
last_activity_ts: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def collect_user_activity(
|
|
23
|
+
client,
|
|
24
|
+
address: str,
|
|
25
|
+
start_ts: int,
|
|
26
|
+
end_ts: int,
|
|
27
|
+
page_limit: int,
|
|
28
|
+
warnings: list[WarningItem],
|
|
29
|
+
activity_types: list[str] | tuple[str, ...] | None = None,
|
|
30
|
+
) -> list:
|
|
31
|
+
records: list = []
|
|
32
|
+
offset = 0
|
|
33
|
+
reached_offset_cap = False
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
page = await client.get_user_activity_page(
|
|
37
|
+
user=address,
|
|
38
|
+
activity_types=list(activity_types or DISCOVERY_ACTIVITY_TYPES),
|
|
39
|
+
start_ts=start_ts,
|
|
40
|
+
end_ts=end_ts,
|
|
41
|
+
limit=page_limit,
|
|
42
|
+
offset=offset,
|
|
43
|
+
sort_direction="ASC",
|
|
44
|
+
)
|
|
45
|
+
if not page:
|
|
46
|
+
break
|
|
47
|
+
records.extend(page)
|
|
48
|
+
if len(page) < page_limit:
|
|
49
|
+
break
|
|
50
|
+
offset += len(page)
|
|
51
|
+
if offset > DISCOVERY_ACTIVITY_OFFSET_MAX:
|
|
52
|
+
reached_offset_cap = True
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if not reached_offset_cap:
|
|
56
|
+
return dedupe_activity_records(records)
|
|
57
|
+
|
|
58
|
+
if end_ts - start_ts <= 1:
|
|
59
|
+
warnings.append(
|
|
60
|
+
WarningItem(
|
|
61
|
+
timestamp=start_ts,
|
|
62
|
+
code="DISCOVERY_WINDOW_TRUNCATED",
|
|
63
|
+
message="user activity window is too dense to split further; discovery may be incomplete",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return dedupe_activity_records(records)
|
|
67
|
+
|
|
68
|
+
split_ts = start_ts + ((end_ts - start_ts) // 2)
|
|
69
|
+
left_records, right_records = await asyncio.gather(
|
|
70
|
+
collect_user_activity(
|
|
71
|
+
client=client,
|
|
72
|
+
address=address,
|
|
73
|
+
start_ts=start_ts,
|
|
74
|
+
end_ts=split_ts,
|
|
75
|
+
page_limit=page_limit,
|
|
76
|
+
warnings=warnings,
|
|
77
|
+
activity_types=activity_types,
|
|
78
|
+
),
|
|
79
|
+
collect_user_activity(
|
|
80
|
+
client=client,
|
|
81
|
+
address=address,
|
|
82
|
+
start_ts=split_ts + 1,
|
|
83
|
+
end_ts=end_ts,
|
|
84
|
+
page_limit=page_limit,
|
|
85
|
+
warnings=warnings,
|
|
86
|
+
activity_types=activity_types,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
return dedupe_activity_records([*left_records, *right_records])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def discover_user_markets_in_range(
|
|
93
|
+
client,
|
|
94
|
+
address: str,
|
|
95
|
+
start_ts: int,
|
|
96
|
+
end_ts: int,
|
|
97
|
+
page_limit: int,
|
|
98
|
+
warnings: list[WarningItem],
|
|
99
|
+
) -> list[DiscoveredMarket]:
|
|
100
|
+
records = await collect_user_activity(
|
|
101
|
+
client=client,
|
|
102
|
+
address=address,
|
|
103
|
+
start_ts=start_ts,
|
|
104
|
+
end_ts=end_ts,
|
|
105
|
+
page_limit=min(max(1, page_limit), DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX),
|
|
106
|
+
warnings=warnings,
|
|
107
|
+
activity_types=DISCOVERY_ACTIVITY_TYPES,
|
|
108
|
+
)
|
|
109
|
+
return summarize_discovered_markets(records, warnings)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def discover_user_markets_by_day(
|
|
113
|
+
client,
|
|
114
|
+
address: str,
|
|
115
|
+
start_ts: int,
|
|
116
|
+
end_ts: int,
|
|
117
|
+
page_limit: int,
|
|
118
|
+
warnings: list[WarningItem],
|
|
119
|
+
) -> list[DiscoveredMarket]:
|
|
120
|
+
market_by_key: dict[str, DiscoveredMarket] = {}
|
|
121
|
+
for window_start, window_end in iter_day_windows(start_ts, end_ts):
|
|
122
|
+
day_records = await collect_user_activity(
|
|
123
|
+
client=client,
|
|
124
|
+
address=address,
|
|
125
|
+
start_ts=window_start,
|
|
126
|
+
end_ts=window_end,
|
|
127
|
+
page_limit=min(max(1, page_limit), DISCOVERY_ACTIVITY_PAGE_LIMIT_MAX),
|
|
128
|
+
warnings=warnings,
|
|
129
|
+
activity_types=DISCOVERY_ACTIVITY_TYPES,
|
|
130
|
+
)
|
|
131
|
+
for discovered in summarize_discovered_markets(day_records, warnings):
|
|
132
|
+
market_key = discovered.condition_id or discovered.slug
|
|
133
|
+
existing = market_by_key.get(market_key)
|
|
134
|
+
if existing is None:
|
|
135
|
+
market_by_key[market_key] = discovered
|
|
136
|
+
continue
|
|
137
|
+
existing.first_activity_ts = min(existing.first_activity_ts, discovered.first_activity_ts)
|
|
138
|
+
existing.last_activity_ts = max(existing.last_activity_ts, discovered.last_activity_ts)
|
|
139
|
+
if not existing.slug and discovered.slug:
|
|
140
|
+
existing.slug = discovered.slug
|
|
141
|
+
|
|
142
|
+
return sorted(
|
|
143
|
+
market_by_key.values(),
|
|
144
|
+
key=lambda item: (item.first_activity_ts, item.last_activity_ts, item.slug),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def summarize_discovered_markets(records: list, warnings: list[WarningItem]) -> list[DiscoveredMarket]:
|
|
149
|
+
market_by_key: dict[str, DiscoveredMarket] = {}
|
|
150
|
+
warned_missing_slug: set[str] = set()
|
|
151
|
+
|
|
152
|
+
for record in records:
|
|
153
|
+
slug = str(record.slug or "").strip()
|
|
154
|
+
condition_id = str(record.condition_id or "").strip()
|
|
155
|
+
if not slug:
|
|
156
|
+
dedupe_key = f"{condition_id}:{record.type}"
|
|
157
|
+
if dedupe_key not in warned_missing_slug:
|
|
158
|
+
warnings.append(
|
|
159
|
+
WarningItem(
|
|
160
|
+
timestamp=record.timestamp,
|
|
161
|
+
code="DISCOVERY_SKIP_MISSING_SLUG",
|
|
162
|
+
message="skip user activity record without market slug",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
warned_missing_slug.add(dedupe_key)
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
market_key = condition_id or slug
|
|
169
|
+
existing = market_by_key.get(market_key)
|
|
170
|
+
if existing is None:
|
|
171
|
+
market_by_key[market_key] = DiscoveredMarket(
|
|
172
|
+
slug=slug,
|
|
173
|
+
condition_id=condition_id,
|
|
174
|
+
first_activity_ts=record.timestamp,
|
|
175
|
+
last_activity_ts=record.timestamp,
|
|
176
|
+
)
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
existing.first_activity_ts = min(existing.first_activity_ts, record.timestamp)
|
|
180
|
+
existing.last_activity_ts = max(existing.last_activity_ts, record.timestamp)
|
|
181
|
+
if not existing.slug and slug:
|
|
182
|
+
existing.slug = slug
|
|
183
|
+
|
|
184
|
+
return sorted(
|
|
185
|
+
market_by_key.values(),
|
|
186
|
+
key=lambda item: (item.first_activity_ts, item.last_activity_ts, item.slug),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def dedupe_activity_records(records: list) -> list:
|
|
191
|
+
deduped: dict[str, object] = {}
|
|
192
|
+
for record in records:
|
|
193
|
+
deduped[activity_key(record)] = record
|
|
194
|
+
return sorted(
|
|
195
|
+
deduped.values(),
|
|
196
|
+
key=lambda item: (
|
|
197
|
+
int(getattr(item, "timestamp", 0)),
|
|
198
|
+
str(getattr(item, "transaction_hash", "")),
|
|
199
|
+
str(getattr(item, "type", "")),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def activity_key(record) -> str:
|
|
205
|
+
return "|".join(
|
|
206
|
+
[
|
|
207
|
+
str(getattr(record, "transaction_hash", "")),
|
|
208
|
+
str(getattr(record, "condition_id", "")),
|
|
209
|
+
str(getattr(record, "type", "")),
|
|
210
|
+
str(getattr(record, "timestamp", 0)),
|
|
211
|
+
str(getattr(record, "slug", "")),
|
|
212
|
+
str(getattr(record, "size", 0)),
|
|
213
|
+
str(getattr(record, "usdc_size", 0)),
|
|
214
|
+
]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def iter_day_windows(start_ts: int, end_ts: int) -> list[tuple[int, int]]:
|
|
219
|
+
if start_ts > end_ts:
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
windows: list[tuple[int, int]] = []
|
|
223
|
+
current = start_ts
|
|
224
|
+
while current <= end_ts:
|
|
225
|
+
day_start = (current // SECONDS_PER_DAY) * SECONDS_PER_DAY
|
|
226
|
+
day_end = day_start + SECONDS_PER_DAY - 1
|
|
227
|
+
window_end = min(day_end, end_ts)
|
|
228
|
+
windows.append((current, window_end))
|
|
229
|
+
current = window_end + 1
|
|
230
|
+
return windows
|