-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame_engine.py
More file actions
240 lines (205 loc) · 8.68 KB
/
Copy pathgame_engine.py
File metadata and controls
240 lines (205 loc) · 8.68 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import random
import time
from stock_data import fetch_daily_ohlcv
# Difficulty maps to a (min_days, max_days) range.
# Window length is randomized within this range each run.
DIFFICULTY_RANGES = {
"beginner": (60, 100), # 12–20 weeks
"intermediate": (40, 70), # 8–14 weeks
"expert": (30, 50), # 6–10 weeks
}
FEATURED_SCENARIOS = [
{
"ticker": "AAPL",
"label": "AAPL",
"teaser": "Something changed forever in consumer technology...",
"tag": "TECH",
},
{
"ticker": "TSLA",
"label": "TSLA",
"teaser": "The market had very strong opinions about this company.",
"tag": "EV",
},
{
"ticker": "NVDA",
"label": "NVDA",
"teaser": "The world suddenly decided it needed this chip.",
"tag": "AI CHIPS",
},
{
"ticker": "JPM",
"label": "JPM",
"teaser": "When the financial system held its breath.",
"tag": "BANKING",
},
{
"ticker": "NFLX",
"label": "NFLX",
"teaser": "A content war was brewing. Someone was winning.",
"tag": "STREAMING",
},
{
"ticker": "AMZN",
"label": "AMZN",
"teaser": "A pivotal moment in how the world shops and computes.",
"tag": "E-COMMERCE",
},
{
"ticker": "MSFT",
"label": "MSFT",
"teaser": "A software empire quietly became the most valuable company on Earth.",
"tag": "CLOUD",
},
{
"ticker": "GME",
"label": "GME",
"teaser": "The internet decided a struggling retailer was worth fighting for.",
"tag": "MEME",
},
]
def pick_scenario(ticker: str, difficulty: str = "intermediate"):
"""
Pick a random historical window from the ticker's data.
Window length is randomized within difficulty-based range (not fixed).
Seed: timestamp-ms XOR ticker hash — unique per run, stored for
reproducibility. Use a local random.Random instance so we don't
affect global random state.
Returns a scenario dict, or None if data is insufficient.
"""
data = fetch_daily_ohlcv(ticker)
if not data or len(data) < 60:
return None
all_dates = sorted(data.keys())
min_days, max_days = DIFFICULTY_RANGES.get(difficulty, (40, 70))
# Unique seed per run: millisecond timestamp XOR ticker hash
seed = int(time.time() * 1000) ^ (hash(ticker) & 0x7FFF_FFFF)
rng = random.Random(seed)
trading_days = rng.randint(min_days, max_days)
if len(all_dates) < trading_days + 10:
trading_days = len(all_dates) - 10
trading_days = max(trading_days, 10)
max_start = len(all_dates) - trading_days - 1
if max_start < 1:
return None
start_idx = rng.randint(0, max_start)
end_idx = start_idx + trading_days
window_dates = all_dates[start_idx:end_idx]
window_data = {d: data[d] for d in window_dates}
start_price = window_data[window_dates[0]]["open"]
end_price = window_data[window_dates[-1]]["close"]
pct_change = ((end_price - start_price) / start_price) * 100
briefing = _generate_briefing(window_data, pct_change, window_dates)
return {
"ticker": ticker,
"difficulty": difficulty,
"dates": window_dates,
"data": window_data,
"start_price": start_price,
"end_price": end_price,
"pct_change": round(pct_change, 2),
"briefing": briefing,
"real_start_date": window_dates[0],
"real_end_date": window_dates[-1],
"total_days": len(window_dates),
"seed": seed,
}
def _generate_briefing(window_data: dict, pct_change: float, dates: list) -> list:
"""
Generate 3 vague market sentiment bullets derived from the actual
price trend of the selected window.
"""
prices = [window_data[d]["close"] for d in dates]
# Volatility
if len(prices) > 5:
changes = [abs((prices[i] - prices[i-1]) / prices[i-1]) * 100
for i in range(1, min(20, len(prices)))]
avg_daily_change = sum(changes) / len(changes)
else:
avg_daily_change = 1.0
early_end = max(1, len(prices) // 5)
early_trend = prices[early_end] - prices[0]
# Mid-period momentum
mid = len(prices) // 2
mid_trend = prices[mid] - prices[mid // 2] if mid > 0 else 0
bullets = []
if avg_daily_change > 3:
bullets.append("Markets are exhibiting unusual volatility — analyst desks are on high alert.")
elif avg_daily_change > 1.5:
bullets.append("Trading sessions have been eventful, with notable intraday swings across sectors.")
else:
bullets.append("Markets are moving quietly — the calm before or after a storm, it's hard to say.")
hints = [
"Institutional investors appear to be repositioning their portfolios significantly.",
"Retail investor activity has been elevated in recent sessions.",
"Options market activity suggests traders are hedging for a major move.",
"Analyst coverage of this sector has increased noticeably.",
"Earnings expectations have been a topic of debate on Wall Street.",
"Macroeconomic data releases have added uncertainty to near-term forecasts.",
"Supply chain narratives continue to shape investor sentiment.",
"Regulatory chatter has been circulating, though nothing confirmed.",
"Short interest in this name has been climbing according to recent filings.",
"Large block trades have been spotted in the options chain this week.",
]
# Use a stable choice from the window's start price (deterministic per scenario)
hint_idx = int(prices[0] * 7) % len(hints)
bullets.append(hints[hint_idx])
if early_trend > 0:
bullets.append(
"Early trading in this period started with a cautiously optimistic tone — "
"buyers appear to be testing the waters."
)
else:
bullets.append(
"The opening days of this period saw some selling pressure — "
"bears were active early on."
)
return bullets
def get_performance_rating(profit_pct: float) -> dict:
if profit_pct >= 30:
return {"title": "Warren Buffett Would Be Proud", "emoji": "",
"color": "#00d4aa", "desc": "Exceptional returns. You clearly have a gift for reading the market."}
elif profit_pct >= 10:
return {"title": "Decent Trader", "emoji": "",
"color": "#00b894", "desc": "Solid performance. You made smart moves and came out ahead."}
elif profit_pct >= 0:
return {"title": "Broke Even — Barely", "emoji": "",
"color": "#f59e0b", "desc": "You survived. Not exactly the stuff of legend, but no serious damage."}
elif profit_pct >= -10:
return {"title": "Bought High, Sold Low", "emoji": "",
"color": "#ff8800", "desc": "Classic retail investor move. The market humbled you this time."}
else:
return {"title": "You Owe the Market an Apology", "emoji": "",
"color": "#ff4757", "desc": "That was rough. The market took you to school and kept your lunch money."}
def build_chart_data(scenario: dict, trades: list) -> dict:
dates = scenario["dates"]
data = scenario["data"]
candles = {
"x": dates,
"open": [data[d]["open"] for d in dates if d in data],
"high": [data[d]["high"] for d in dates if d in data],
"low": [data[d]["low"] for d in dates if d in data],
"close": [data[d]["close"] for d in dates if d in data],
}
valid_dates = [d for d in dates if d in data]
candles["x"] = valid_dates
# Issue #11 — build date→index map once (O(1) lookup per trade)
date_idx = {d: i for i, d in enumerate(valid_dates)}
buy_points = {"x": [], "y": [], "text": []}
sell_points = {"x": [], "y": [], "text": []}
for trade in trades:
day_idx = trade.get("day_index", 0)
if day_idx < len(dates):
real_date = dates[day_idx]
price = trade.get("price", 0)
shares = trade.get("shares", 0)
if trade.get("action") == "buy":
buy_points["x"].append(real_date)
buy_points["y"].append(price)
buy_points["text"].append(f"BUY {shares} @ ${price:.2f}")
elif trade.get("action") == "sell":
sell_points["x"].append(real_date)
sell_points["y"].append(price)
sell_points["text"].append(f"SELL {shares} @ ${price:.2f}")
return {"candles": candles, "buy_points": buy_points,
"sell_points": sell_points, "date_idx": date_idx}