Fedir-Ilina commited on
Commit
e5b3fdd
·
verified ·
1 Parent(s): 164dcce

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +12 -0
  2. app.py +342 -0
  3. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import yfinance as yf
3
+ import math
4
+ import time
5
+ import random
6
+ import threading
7
+ import requests
8
+ from requests.adapters import HTTPAdapter
9
+ from datetime import datetime, timezone
10
+ from flask import Flask, jsonify, render_template_string, request
11
+ from flask_cors import CORS
12
+ from dotenv import load_dotenv
13
+
14
+ load_dotenv()
15
+
16
+ app = Flask(__name__)
17
+
18
+ # ── Session com headers de browser para evitar rate limit ──
19
+ def make_yf_session():
20
+ session = requests.Session()
21
+ session.headers.update({
22
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
23
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
24
+ "Accept-Language": "en-US,en;q=0.5",
25
+ "Accept-Encoding": "gzip, deflate, br",
26
+ "Connection": "keep-alive",
27
+ })
28
+ adapter = HTTPAdapter(max_retries=2)
29
+ session.mount("https://", adapter)
30
+ return session
31
+ CORS(app)
32
+
33
+ assets_raw = os.getenv("ASSETS") or os.getenv("ASSET") or "MSFT,AAPL,TSLA"
34
+ TICKERS = [t.strip().upper() for t in assets_raw.split(",") if t.strip()]
35
+ RISK_FREE = 0.045
36
+ SLEEP = 60
37
+ TARGET_DAYS = [10, 15, 30, 60, 90, 120]
38
+ DELTA_SELL_RANGE = (0.01, 0.65)
39
+ DELTA_BUY_RANGE = (0.01, 0.65)
40
+ MAX_WIDTH = 14
41
+
42
+ scan_state = {
43
+ "status": "idle",
44
+ "last_update": None,
45
+ "results": {},
46
+ "errors": [],
47
+ "tickers": TICKERS,
48
+ }
49
+ scan_lock = threading.Lock()
50
+
51
+ def to_float_safe(x):
52
+ try:
53
+ if isinstance(x, (float, int)):
54
+ return float(x)
55
+ if hasattr(x, "values"):
56
+ return float(x.values[0])
57
+ return float(x)
58
+ except:
59
+ return 0.0
60
+
61
+ def mid_or_last_safe(row):
62
+ try:
63
+ bid = to_float_safe(row["bid"])
64
+ ask = to_float_safe(row["ask"])
65
+ last = to_float_safe(row["lastPrice"])
66
+ except:
67
+ return 0.0
68
+ if bid > 0 and ask > 0:
69
+ return (bid + ask) / 2.0
70
+ if last > 0:
71
+ return last
72
+ if bid > 0:
73
+ return bid
74
+ return 0.0
75
+
76
+ def N(x):
77
+ return 0.5 * (1 + math.erf(x / math.sqrt(2)))
78
+
79
+ def d1(S, K, r, sigma, T):
80
+ return (math.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma * math.sqrt(T))
81
+
82
+ def d2(S, K, r, sigma, T):
83
+ return d1(S, K, r, sigma, T) - sigma * math.sqrt(T)
84
+
85
+ def bs_call_price(S, K, r, sigma, T):
86
+ return S * N(d1(S, K, r, sigma, T)) - K * math.exp(-r*T) * N(d2(S, K, r, sigma, T))
87
+
88
+ def bs_put_price(S, K, r, sigma, T):
89
+ return K * math.exp(-r*T) * N(-d2(S, K, r, sigma, T)) - S * N(-d1(S, K, r, sigma, T))
90
+
91
+ def delta_call(S, K, r, sigma, T):
92
+ return N(d1(S, K, r, sigma, T))
93
+
94
+ def delta_put(S, K, r, sigma, T):
95
+ return N(d1(S, K, r, sigma, T)) - 1
96
+
97
+ def implied_volatility(option_price, S, K, r, T, option_type="call"):
98
+ sigma = 0.30
99
+ for _ in range(70):
100
+ if sigma <= 0:
101
+ sigma = 0.01
102
+ try:
103
+ model = bs_call_price(S, K, r, sigma, T) if option_type == "call" else bs_put_price(S, K, r, sigma, T)
104
+ except:
105
+ return sigma
106
+ d_1 = d1(S, K, r, sigma, T)
107
+ vega = S * math.sqrt(T) * (1/math.sqrt(2*math.pi)) * math.exp(-(d_1*d_1)/2)
108
+ diff = model - option_price
109
+ if abs(diff) < 1e-6 or vega < 1e-8:
110
+ return sigma
111
+ sigma -= diff / vega
112
+ return sigma
113
+
114
+ def year_fraction_to_expiry(expiry_str):
115
+ dt = datetime.strptime(expiry_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
116
+ now = datetime.now(timezone.utc)
117
+ delta = dt - now
118
+ days = delta.days + delta.seconds / 86400
119
+ return max(days / 365, 1e-6), max(days, 0)
120
+
121
+ def scan_ticker(TICKER):
122
+ result = {"ticker": TICKER, "price": None, "variations": {}, "structures": [], "error": None}
123
+ try:
124
+ session = make_yf_session()
125
+ tk = yf.Ticker(TICKER, session=session)
126
+ hist = tk.history(interval="1m", period="1d")
127
+ if hist.empty:
128
+ result["error"] = "Sem dados de preco (mercado fechado?)"
129
+ return result
130
+
131
+ S = float(hist["Close"].iloc[-1])
132
+ result["price"] = round(S, 4)
133
+
134
+ hist_daily = tk.history(interval="1d", period="90d")
135
+ today_date = hist_daily.index[-1].strftime("%Y-%m-%d") if len(hist_daily) else ""
136
+
137
+ for label, idx in [("1D", -2), ("5D", -6), ("15D", -15), ("30D", -31), ("60D", -60), ("90D", -90)]:
138
+ if len(hist_daily) >= abs(idx):
139
+ close = float(hist_daily["Close"].iloc[idx])
140
+ date = hist_daily.index[idx].strftime("%Y-%m-%d")
141
+ pct = ((S - close) / close) * 100
142
+ result["variations"][label] = {"pct": round(pct, 2), "from": date, "to": today_date}
143
+
144
+ expiries = tk.options
145
+ if not expiries:
146
+ result["error"] = "Sem opcoes disponiveis"
147
+ return result
148
+
149
+ exp_info = []
150
+ for e in expiries:
151
+ T_frac, days = year_fraction_to_expiry(e)
152
+ if days > 0:
153
+ exp_info.append((e, T_frac, days))
154
+
155
+ chosen = []
156
+ seen = set()
157
+ for target_days in TARGET_DAYS:
158
+ nearest = min(exp_info, key=lambda x: abs(x[2] - target_days))
159
+ if nearest[0] not in seen:
160
+ seen.add(nearest[0])
161
+ chosen.append(nearest)
162
+
163
+ for (exp_str, T_years, days_left) in chosen:
164
+ chain = tk.option_chain(exp_str)
165
+ for tipo in ["call", "put"]:
166
+ options = chain.calls if tipo == "call" else chain.puts
167
+ if options.empty:
168
+ continue
169
+
170
+ best_structure = None
171
+ best_score = -999999
172
+
173
+ for _, sell in options.iterrows():
174
+ K_s = float(sell["strike"])
175
+ if tipo == "call" and K_s <= S:
176
+ continue
177
+ if tipo == "put" and K_s >= S:
178
+ continue
179
+
180
+ mid_s = mid_or_last_safe(sell)
181
+ if mid_s <= 0:
182
+ continue
183
+
184
+ bid_s = to_float_safe(sell.get("bid", 0))
185
+ ask_s = to_float_safe(sell.get("ask", 0))
186
+ if bid_s <= 0 or ask_s <= 0:
187
+ continue
188
+
189
+ spread_s = ask_s - bid_s
190
+
191
+ iv_s = implied_volatility(mid_s, S, K_s, RISK_FREE, T_years, tipo)
192
+ d_s = abs(delta_call(S, K_s, RISK_FREE, iv_s, T_years) if tipo == "call"
193
+ else delta_put(S, K_s, RISK_FREE, iv_s, T_years))
194
+
195
+ if not (DELTA_SELL_RANGE[0] <= d_s <= DELTA_SELL_RANGE[1]):
196
+ continue
197
+
198
+ for _, buy in options.iterrows():
199
+ K_b = float(buy["strike"])
200
+ if tipo == "call" and K_b <= K_s:
201
+ continue
202
+ if tipo == "put" and K_b >= K_s:
203
+ continue
204
+
205
+ mid_b = mid_or_last_safe(buy)
206
+ if mid_b <= 0:
207
+ continue
208
+
209
+ bid_b = to_float_safe(buy.get("bid", 0))
210
+ ask_b = to_float_safe(buy.get("ask", 0))
211
+ if bid_b <= 0 or ask_b <= 0:
212
+ continue
213
+
214
+ spread_b = ask_b - bid_b
215
+
216
+ iv_b = implied_volatility(mid_b, S, K_b, RISK_FREE, T_years, tipo)
217
+ d_b = abs(delta_call(S, K_b, RISK_FREE, iv_b, T_years) if tipo == "call"
218
+ else delta_put(S, K_b, RISK_FREE, iv_b, T_years))
219
+
220
+ if not (DELTA_BUY_RANGE[0] <= d_b <= DELTA_BUY_RANGE[1]):
221
+ continue
222
+
223
+ credito = mid_s - (2 * mid_b)
224
+ if credito <= 0.3:
225
+ continue
226
+
227
+ width = abs(K_b - K_s)
228
+ if width > MAX_WIDTH:
229
+ continue
230
+
231
+ max_loss = width - credito
232
+ score = -max_loss if max_loss > 0 else -999
233
+ spread_total = spread_s + 2 * spread_b
234
+
235
+ if score > best_score:
236
+ best_score = score
237
+ be = (K_b + max_loss) if tipo == "call" else (K_b - max_loss)
238
+ best_structure = {
239
+ "type": tipo,
240
+ "expiry": exp_str,
241
+ "days_left": round(days_left, 1),
242
+ "strike_sell": K_s,
243
+ "mid_sell": round(mid_s, 3),
244
+ "delta_sell": round(d_s, 3),
245
+ "strike_buy": K_b,
246
+ "mid_buy": round(mid_b, 3),
247
+ "delta_buy": round(d_b, 3),
248
+ "credit": round(credito, 3),
249
+ "max_loss": round(max_loss, 3),
250
+ "spread_total": round(spread_total, 3),
251
+ "be_point": round(be, 2),
252
+ "pct_move": round((be - S) / S * 100, 2),
253
+ "iv_sell": round(iv_s * 100, 1),
254
+ "iv_buy": round(iv_b * 100, 1),
255
+ }
256
+
257
+ if best_structure:
258
+ result["structures"].append(best_structure)
259
+
260
+ except Exception as e:
261
+ result["error"] = str(e)
262
+
263
+ return result
264
+
265
+ def run_scan(tickers):
266
+ with scan_lock:
267
+ scan_state["status"] = "scanning"
268
+ scan_state["errors"] = []
269
+ scan_state["results"] = {}
270
+
271
+ results = {}
272
+ errors = []
273
+ for i, ticker in enumerate(tickers):
274
+ # Delay entre tickers para evitar rate limit (2-4s aleatório)
275
+ if i > 0:
276
+ time.sleep(random.uniform(2.5, 4.5))
277
+
278
+ # Retry automático em caso de rate limit
279
+ for attempt in range(3):
280
+ res = scan_ticker(ticker)
281
+ if res.get("error") and "Too Many Requests" in str(res.get("error", "")):
282
+ wait = 10 + attempt * 10 # 10s, 20s, 30s
283
+ time.sleep(wait)
284
+ continue
285
+ break
286
+
287
+ results[ticker] = res
288
+ # Atualiza resultados parciais para o frontend ir mostrando
289
+ with scan_lock:
290
+ scan_state["results"] = dict(results)
291
+ if res.get("error"):
292
+ errors.append(f"{ticker}: {res['error']}")
293
+
294
+ with scan_lock:
295
+ scan_state["status"] = "done"
296
+ scan_state["results"] = results
297
+ scan_state["errors"] = errors
298
+ scan_state["last_update"] = datetime.now(timezone.utc).isoformat()
299
+
300
+ @app.route("/")
301
+ def index():
302
+ with open("templates/index.html", encoding="utf-8") as f:
303
+ return render_template_string(f.read())
304
+
305
+ @app.route("/api/status")
306
+ def api_status():
307
+ with scan_lock:
308
+ return jsonify({
309
+ "status": scan_state["status"],
310
+ "last_update": scan_state["last_update"],
311
+ "tickers": scan_state["tickers"],
312
+ "errors": scan_state["errors"],
313
+ })
314
+
315
+ @app.route("/api/results")
316
+ def api_results():
317
+ with scan_lock:
318
+ return jsonify(scan_state["results"])
319
+
320
+ @app.route("/api/scan", methods=["POST"])
321
+ def api_scan():
322
+ data = request.get_json(silent=True) or {}
323
+ tickers_raw = data.get("tickers", "")
324
+ tickers = [t.strip().upper() for t in tickers_raw.split(",") if t.strip()] if tickers_raw else TICKERS
325
+ with scan_lock:
326
+ if scan_state["status"] == "scanning":
327
+ return jsonify({"error": "Scan ja em curso"}), 409
328
+ scan_state["tickers"] = tickers
329
+ threading.Thread(target=run_scan, args=(tickers,), daemon=True).start()
330
+ return jsonify({"ok": True, "tickers": tickers})
331
+
332
+ @app.route("/api/config")
333
+ def api_config():
334
+ return jsonify({
335
+ "tickers": TICKERS,
336
+ "risk_free": RISK_FREE,
337
+ "target_days": TARGET_DAYS,
338
+ "max_width": MAX_WIDTH,
339
+ })
340
+
341
+ if __name__ == "__main__":
342
+ app.run(host="0.0.0.0", port=7860, debug=False)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==3.0.3
2
+ flask-cors==4.0.1
3
+ yfinance==0.2.54
4
+ pandas==2.2.3
5
+ python-dotenv==1.0.1
6
+ requests==2.32.3