-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbtc_weekly_report.py
More file actions
executable file
·168 lines (132 loc) · 4.66 KB
/
btc_weekly_report.py
File metadata and controls
executable file
·168 lines (132 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python3
"""Weekly BTC DCA timing report.
Uses the existing 5am/5pm samples in state/btc-prices.jsonl.
Computes simple day-of-week stats for two candidate buy times:
- 05:00 local (hour==5)
- 17:00 local (hour==17)
Outputs:
- For each slot, which weekday had the lowest average BTC/USD and BTC/CAD
- Win-rate: how often each weekday was the lowest (within that week) for that slot
This is descriptive stats, not prediction.
"""
import json
import sys
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime
from zoneinfo import ZoneInfo
TZ = ZoneInfo("America/Edmonton")
SLOTS = {
5: "5am",
17: "5pm",
}
@dataclass
class Rec:
ts: int
dt: datetime
btc_usd: float
btc_cad: float
def load_recs(path: str) -> list[Rec]:
recs: list[Rec] = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
ts = int(obj["ts"])
dt = datetime.fromtimestamp(ts, TZ)
recs.append(
Rec(
ts=ts,
dt=dt,
btc_usd=float(obj["btc_usd"]),
btc_cad=float(obj["btc_cad"]),
)
)
recs.sort(key=lambda r: r.ts)
return recs
def week_key(dt: datetime) -> tuple[int, int]:
# ISO week
iso = dt.isocalendar()
return (iso.year, iso.week)
def weekday_name(dt: datetime) -> str:
return dt.strftime("%a") # Mon, Tue...
def pick_latest_per_day_slot(recs: list[Rec], hour: int) -> dict[tuple[int, int], dict[str, Rec]]:
"""Return mapping week -> weekday -> record for that weekday/slot.
If multiple samples exist in same weekday+slot, keep latest.
"""
out: dict[tuple[int, int], dict[str, Rec]] = defaultdict(dict)
for r in recs:
if r.dt.hour != hour:
continue
wk = week_key(r.dt)
wd = weekday_name(r.dt)
prev = out[wk].get(wd)
if prev is None or r.ts > prev.ts:
out[wk][wd] = r
return out
def summarize(slot_hour: int, by_week: dict[tuple[int, int], dict[str, Rec]]) -> str:
slot_label = SLOTS.get(slot_hour, f"{slot_hour}:00")
# Aggregate averages by weekday
sums_usd = defaultdict(float)
sums_cad = defaultdict(float)
counts = defaultdict(int)
# Win counts: within each week, which weekday had lowest price for that slot
wins_usd = defaultdict(int)
wins_cad = defaultdict(int)
total_weeks = 0
for wk, days in by_week.items():
if len(days) < 4:
# skip super sparse weeks
continue
total_weeks += 1
# pick winner within this week for that slot
usd_items = [(wd, rec.btc_usd) for wd, rec in days.items()]
cad_items = [(wd, rec.btc_cad) for wd, rec in days.items()]
usd_wd = min(usd_items, key=lambda x: x[1])[0]
cad_wd = min(cad_items, key=lambda x: x[1])[0]
wins_usd[usd_wd] += 1
wins_cad[cad_wd] += 1
for wd, rec in days.items():
sums_usd[wd] += rec.btc_usd
sums_cad[wd] += rec.btc_cad
counts[wd] += 1
if total_weeks == 0:
return f"{slot_label}: not enough data yet."
# Build sorted tables by average
rows = []
for wd in sorted(counts.keys()):
n = counts[wd]
avg_usd = sums_usd[wd] / n
avg_cad = sums_cad[wd] / n
winr_usd = wins_usd[wd] / total_weeks
winr_cad = wins_cad[wd] / total_weeks
rows.append((wd, n, avg_usd, avg_cad, winr_usd, winr_cad))
rows_usd = sorted(rows, key=lambda r: r[2])
rows_cad = sorted(rows, key=lambda r: r[3])
best_usd = rows_usd[0]
best_cad = rows_cad[0]
lines = []
lines.append(f"{slot_label} (weeks analyzed: {total_weeks})")
lines.append(
f"- Best avg BTC/USD: {best_usd[0]} (avg ${best_usd[2]:,.0f}, win-rate {best_usd[4]*100:.0f}%)"
)
lines.append(
f"- Best avg BTC/CAD: {best_cad[0]} (avg C${best_cad[3]:,.0f}, win-rate {best_cad[5]*100:.0f}%)"
)
return "\n".join(lines)
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "/home/bot/Workspace/state/btc-prices.jsonl"
recs = load_recs(path)
if len(recs) < 10:
print("Not much data yet (need a few weeks). I’ll start giving real weekday stats once we have more samples.")
return
parts = []
parts.append("Weekly DCA timing stats (descriptive, not predictive)")
for hour in (5, 17):
by_week = pick_latest_per_day_slot(recs, hour)
parts.append(summarize(hour, by_week))
print("\n".join(parts))
if __name__ == "__main__":
main()