Dooratre commited on
Commit
6bd53df
·
verified ·
1 Parent(s): 92da1a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +637 -204
app.py CHANGED
@@ -4,6 +4,9 @@ import datetime
4
  import threading
5
  import sys
6
  import math
 
 
 
7
  from typing import Optional, Dict, Any
8
 
9
  try:
@@ -14,7 +17,14 @@ except ImportError:
14
 
15
  from flask import Flask, request, jsonify
16
 
17
- # ========== Configuration ==========
 
 
 
 
 
 
 
18
  API_URL = "https://research.titanfx.com/api/live-rate?group=forex"
19
  API_HEADERS = {
20
  "referer": "https://research.titanfx.com/instruments/gbpusd",
@@ -25,16 +35,76 @@ API_HEADERS = {
25
  }
26
 
27
  SYMBOL_DEFAULT = "XAUUSD"
28
- PRINT_INTERVAL_SECONDS = 5
29
- REQUEST_TIMEOUT_SECONDS = 5
 
30
  MAX_RETRIES = 5
31
- RETRY_BACKOFF_SECONDS = 1.5
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- # Fixed webhook to send alert messages to (as requested)
34
- ALERT_WEBHOOK_URL = "https://dooratre-xauusd-pro.hf.space/analysis_now"
 
35
 
 
36
  app = Flask(__name__)
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def safe_float_join(whole, decimal):
39
  try:
40
  return float(f"{whole}.{decimal}")
@@ -44,267 +114,630 @@ def safe_float_join(whole, decimal):
44
  except Exception:
45
  return None
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def get_current_price(symbol=SYMBOL_DEFAULT):
48
- """
49
- Fetch current bid/ask for the given symbol.
50
- Returns: dict {bid: float, ask: float} or None on failure.
51
- """
52
  try:
53
  r = requests.get(API_URL, headers=API_HEADERS, timeout=REQUEST_TIMEOUT_SECONDS)
54
  r.raise_for_status()
55
  data = r.json()
56
-
57
  if symbol not in data:
58
- print(f"[DATA] Symbol {symbol} not found in feed.")
59
  return None
60
-
61
  row = data[symbol]
62
  if not isinstance(row, list) or len(row) < 4:
63
- print(f"[DATA] Unexpected data format for {symbol}.")
64
  return None
65
-
66
  bid = safe_float_join(row[0], row[1])
67
  ask = safe_float_join(row[2], row[3])
68
-
69
  if bid is None or ask is None or math.isnan(bid) or math.isnan(ask):
70
- print(f"[DATA] Invalid price values for {symbol}.")
71
  return None
72
-
73
  return {"bid": bid, "ask": ask}
74
-
75
  except requests.RequestException as e:
76
- print(f"[NETWORK] {e}")
77
  return None
78
  except Exception as e:
79
- print(f"[ERROR] Unexpected: {e}")
80
  return None
81
 
82
- def play_alert_sound():
83
- def _beep():
84
- if HAS_WINSOUND:
85
- try:
86
- for _ in range(3):
87
- winsound.Beep(1000, 450)
88
- time.sleep(0.2)
89
- except Exception:
90
- for _ in range(3):
91
- sys.stdout.write('\a'); sys.stdout.flush()
92
- time.sleep(0.3)
93
- else:
94
- for _ in range(3):
95
- sys.stdout.write('\a'); sys.stdout.flush()
96
- time.sleep(0.3)
97
- threading.Thread(target=_beep, daemon=True).start()
98
 
99
- def ts(dt=None):
100
- return (dt or datetime.datetime.now()).strftime('%H:%M:%S')
 
 
 
 
 
 
 
 
101
 
102
- def send_alert_message(text: str):
 
 
 
 
 
 
 
 
 
103
  """
104
- Send alert message to fixed webhook as JSON { "message": "..." }.
105
- Fire-and-forget (non-blocking).
106
  """
107
- def _post():
108
- try:
109
- headers = {"Content-Type": "application/json"}
110
- payload = {"message": text}
111
- requests.post(ALERT_WEBHOOK_URL, headers=headers, json=payload, timeout=5)
112
- except Exception as e:
113
- print(f"[WEBHOOK] Failed to POST to {ALERT_WEBHOOK_URL}: {e}")
114
- threading.Thread(target=_post, daemon=True).start()
115
-
116
- def build_upside_message(symbol, bid, ask, target_up, target_down, elapsed_min, hi_bid, lo_ask):
117
- dist_to_down = max(0.0, ask - target_down)
118
- return (
119
- f"[EXECUTION] {symbol} Upside target hit. "
120
- f"Bid {bid} {target_up}. "
121
- f"SessionHighBid {hi_bid} | SessionLowAsk {lo_ask}. "
122
- f"AskNow {ask} vs Downside {target_down} (distance {dist_to_down:.5f}). "
123
- f"Elapsed {elapsed_min:.2f}m."
124
- )
125
-
126
- def build_downside_message(symbol, bid, ask, target_up, target_down, elapsed_min, hi_bid, lo_ask):
127
- dist_to_up = max(0.0, target_up - bid)
128
- return (
129
- f"[EXECUTION] {symbol} Downside target hit. "
130
- f"Ask {ask} {target_down}. "
131
- f"SessionHighBid {hi_bid} | SessionLowAsk {lo_ask}. "
132
- f"BidNow {bid} vs Upside {target_up} (distance {dist_to_up:.5f}). "
133
- f"Elapsed {elapsed_min:.2f}m."
134
- )
135
 
136
- def build_expired_message(symbol, hi_bid, lo_ask, target_up, target_down, duration_min):
137
- miss_up = max(0.0, target_up - hi_bid)
138
- miss_down = max(0.0, lo_ask - target_down)
139
- return (
140
- f"[EXPIRED] {symbol} window ended without execution. "
141
- f"SessionHighBid {hi_bid} | SessionLowAsk {lo_ask}. "
142
- f"MissUpside {miss_up:.5f} | MissDownside {miss_down:.5f}. "
143
- f"Duration {duration_min:.2f}m."
144
- )
 
 
 
 
 
 
 
145
 
146
- def monitor_price_dual(target_up, target_down, duration_minutes, symbol=SYMBOL_DEFAULT):
147
  """
148
- Monitor both upside and downside targets. Synchronous call.
149
- On hit/expired, sends alert message to fixed webhook.
150
- Returns a final dict, but in async usage we don't return it to client.
 
 
 
 
 
151
  """
152
- if duration_minutes <= 0:
153
- return {"status": "error", "message": "Configuration error: duration must be > 0."}
154
- if target_up <= target_down:
155
- return {"status": "error", "message": "Configuration error: target_up must be greater than target_down."}
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- # Initial fetch with retry
 
 
 
 
 
 
 
 
 
 
 
 
158
  retries, snap = 0, None
159
  while retries < MAX_RETRIES and snap is None:
160
  snap = get_current_price(symbol)
161
  if snap is None:
162
  retries += 1
163
  if retries < MAX_RETRIES:
164
- delay = RETRY_BACKOFF_SECONDS * retries
165
- time.sleep(delay)
166
  else:
167
- msg = "Unable to retrieve initial price snapshot."
168
- return {"status": "error", "message": msg}
169
-
170
- t_start = datetime.datetime.now()
171
- t_end = t_start + datetime.timedelta(minutes=duration_minutes)
172
-
173
- bid0, ask0 = snap["bid"], snap["ask"]
174
- hi_bid = bid0
175
- lo_ask = ask0
176
-
177
- # Instant checks at start
178
- if bid0 >= target_up:
179
- play_alert_sound()
180
- elapsed_min = (datetime.datetime.now() - t_start).total_seconds() / 60
181
- message = build_upside_message(symbol, bid0, ask0, target_up, target_down, elapsed_min, hi_bid, lo_ask)
182
- send_alert_message(message)
183
- return {"status": "hit", "side": "upside", "message": message}
184
-
185
- if ask0 <= target_down:
186
- play_alert_sound()
187
- elapsed_min = (datetime.datetime.now() - t_start).total_seconds() / 60
188
- message = build_downside_message(symbol, bid0, ask0, target_up, target_down, elapsed_min, hi_bid, lo_ask)
189
- send_alert_message(message)
190
- return {"status": "hit", "side": "downside", "message": message}
191
-
192
- ticks = 0
193
- try:
194
- while datetime.datetime.now() < t_end:
195
- q = get_current_price(symbol)
196
- if q is None:
197
- time.sleep(1)
198
- continue
199
-
200
- bid, ask = q["bid"], q["ask"]
201
- hi_bid = max(hi_bid, bid)
202
- lo_ask = min(lo_ask, ask)
203
-
204
- # Target checks
205
- if bid >= target_up:
206
- play_alert_sound()
207
- elapsed_min = (datetime.datetime.now() - t_start).total_seconds() / 60
208
- message = build_upside_message(symbol, bid, ask, target_up, target_down, elapsed_min, hi_bid, lo_ask)
209
- send_alert_message(message)
210
- return {"status": "hit", "side": "upside", "message": message}
211
-
212
- if ask <= target_down:
213
- play_alert_sound()
214
- elapsed_min = (datetime.datetime.now() - t_start).total_seconds() / 60
215
- message = build_downside_message(symbol, bid, ask, target_up, target_down, elapsed_min, hi_bid, lo_ask)
216
- send_alert_message(message)
217
- return {"status": "hit", "side": "downside", "message": message}
218
-
219
- # Optional: light logging cadence
220
- ticks += 1
221
- if ticks % PRINT_INTERVAL_SECONDS == 0:
222
- mins_left = max(0.0, (t_end - datetime.datetime.now()).total_seconds() / 60)
223
- print(f"[{ts()}] {symbol} Bid {bid} (hi {hi_bid}) | Ask {ask} (lo {lo_ask}) | ↑ {target_up} / ↓ {target_down} | {mins_left:.1f}m left")
224
-
225
- time.sleep(1)
226
-
227
- # Expiry
228
- message = build_expired_message(symbol, hi_bid, lo_ask, target_up, target_down, duration_minutes)
229
- send_alert_message(message)
230
- return {"status": "expired", "message": message}
231
-
232
- except Exception as e:
233
- message = f"Unexpected error: {e}"
234
- send_alert_message(f"[ERROR] {message}")
235
- return {"status": "error", "message": message}
236
 
237
- def start_monitor_async(symbol: str, target_up: float, target_down: float, duration_minutes: int):
238
- """
239
- Start monitor_price_dual in a daemon thread (non-blocking).
240
- """
241
  def _runner():
242
  try:
243
- result = monitor_price_dual(target_up, target_down, duration_minutes, symbol=symbol)
244
- # Optional: log final result
245
- print(f"[MONITOR-END] {symbol} result: {result}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  except Exception as e:
247
- print(f"[MONITOR-THREAD] Error: {e}")
 
 
 
 
 
 
248
  t = threading.Thread(target=_runner, daemon=True)
 
 
 
 
 
 
 
 
 
 
 
 
249
  t.start()
250
 
251
- # ========== API Endpoints ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- @app.route("/monitor", methods=["POST"])
254
- def start_monitor():
 
255
  """
256
- Start monitoring asynchronously and immediately return 200.
257
- Payload:
258
- {
259
- "symbol": "XAUUSD", // optional, default XAUUSD
260
- "target_up": 2490.5, // required
261
- "target_down": 2475.0, // required
262
- "duration_minutes": 15 // required
263
- }
264
- Immediate Response:
265
- { "status": "accepted", "message": "Okay your alert added succes" }
 
 
266
  """
267
  try:
268
  data = request.get_json(force=True)
269
  except Exception:
270
  return jsonify({"status": "error", "message": "Invalid JSON"}), 400
271
 
272
- symbol = data.get("symbol", SYMBOL_DEFAULT)
273
- target_up = data.get("target_up")
274
- target_down = data.get("target_down")
275
- duration_minutes = data.get("duration_minutes")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- if target_up is None or target_down is None or duration_minutes is None:
278
- return jsonify({"status": "error", "message": "target_up, target_down, duration_minutes are required"}), 400
279
 
280
- # Basic type validation before starting async work
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  try:
282
- target_up = float(target_up)
283
- target_down = float(target_down)
284
- duration_minutes = int(duration_minutes)
285
  except Exception:
286
- return jsonify({"status": "error", "message": "Invalid types: target_up/target_down must be float, duration_minutes must be int"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
- if duration_minutes <= 0:
289
- return jsonify({"status": "error", "message": "duration_minutes must be > 0"}), 400
290
- if target_up <= target_down:
291
- return jsonify({"status": "error", "message": "target_up must be greater than target_down"}), 400
292
 
293
- # Start monitoring in background thread
294
- start_monitor_async(symbol, target_up, target_down, duration_minutes)
295
 
296
- # Immediate success response
297
- return jsonify({"status": "accepted", "message": "Okay your alert added succes"}), 200
298
 
299
  @app.route("/", methods=["GET"])
300
  def root():
301
  return jsonify({
302
- "service": "Forex Trade Alert API (async monitor)",
303
- "fixed_alert_webhook": ALERT_WEBHOOK_URL,
304
- "endpoint": "POST /monitor",
305
- "behavior": "returns immediately; monitoring runs in background"
 
 
 
 
 
 
 
 
306
  }), 200
307
 
 
308
  if __name__ == "__main__":
309
- # threaded=True is fine; we also use daemon threads for monitors
310
  app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)
 
4
  import threading
5
  import sys
6
  import math
7
+ import json
8
+ import random
9
+ import logging
10
  from typing import Optional, Dict, Any
11
 
12
  try:
 
17
 
18
  from flask import Flask, request, jsonify
19
 
20
+ # db_signals helpers you already have; we add read_user_json_file below.
21
+ from db_signals import (
22
+ fetch_authenticity_token_and_commit_oid,
23
+ update_user_json_file,
24
+ # If you already have a reader, import it instead of defining here.
25
+ )
26
+
27
+ # ================ Configuration ================
28
  API_URL = "https://research.titanfx.com/api/live-rate?group=forex"
29
  API_HEADERS = {
30
  "referer": "https://research.titanfx.com/instruments/gbpusd",
 
35
  }
36
 
37
  SYMBOL_DEFAULT = "XAUUSD"
38
+
39
+ # Polling and retry behavior
40
+ REQUEST_TIMEOUT_SECONDS = 4
41
  MAX_RETRIES = 5
42
+ RETRY_BACKOFF_SECONDS = 1.2
43
+
44
+ # Tick cadence
45
+ MIN_TICK_SLEEP_SEC = 0.35
46
+ MAX_TICK_SLEEP_SEC = 0.6
47
+ PRINT_EVERY_N_TICKS = 12
48
+
49
+ # External systems
50
+ ANALYSIS_BASE = "https://dooratre-xauusd-pro.hf.space"
51
+ ANALYSIS_STOP_URL = f"{ANALYSIS_BASE}/stop_analysis"
52
+ ANALYSIS_NOW_URL = f"{ANALYSIS_BASE}/analysis_now"
53
+ OTHER_ALERT_CLEAR_URL = "https://dooratre-alert.hf.space/cancel"
54
 
55
+ # User message API
56
+ MESSAGE_API_URL = "https://aoamrnuwara.pythonanywhere.com/api/send-message"
57
+ MESSAGE_API_KEY = "Seakp0683asppoit"
58
 
59
+ # Flask app
60
  app = Flask(__name__)
61
 
62
+ # Logger
63
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
64
+ logger = logging.getLogger("signal-alert-pro")
65
+
66
+ # ================ Active State ================
67
+ ACTIVE_MONITOR = {
68
+ "thread": None,
69
+ "cancel_token": None,
70
+ "id": None,
71
+ "symbol": SYMBOL_DEFAULT,
72
+ "side": None,
73
+ "entry": None,
74
+ "tp": None,
75
+ "sl": None,
76
+ "timestamps": None # keep activation timestamps to preserve on /ts_points updates
77
+ }
78
+ ACTIVE_LOCK = threading.Lock()
79
+
80
+ # ================ db_signals reader helper ================
81
+ # If you already have an official reader, replace this function with it.
82
+ def read_user_json_file(authenticity_token: str, commit_oid: str) -> Dict[str, Any]:
83
+ """
84
+ Reads the current JSON from the GitHub file the same way update_user_json_file writes it.
85
+ Your db_signals module can implement this properly. Here we show a pattern:
86
+ - It likely needs the file URL or the API route you already use in update_user_json_file.
87
+ """
88
+ # You should implement inside db_signals a function that fetches the raw file content.
89
+ # For demonstration, we assume you can call a function db_signals.get_user_json_file()
90
+ # that returns {"success": True, "content": "..."} (JSON string).
91
+ # If you don't have it, implement it there and call it here.
92
+ from db_signals import get_user_json_file # implement this helper in your module
93
+ res = get_user_json_file(authenticity_token, commit_oid)
94
+ if not res.get("success"):
95
+ raise RuntimeError(f"Failed to read user json file: {res}")
96
+ content = res.get("content", "")
97
+ try:
98
+ return json.loads(content) if content else {}
99
+ except Exception:
100
+ # If file is a list [] or other structure, return parsed content or an empty dict
101
+ try:
102
+ parsed = json.loads(content)
103
+ return parsed
104
+ except Exception:
105
+ return {}
106
+
107
+ # ================ Utilities ================
108
  def safe_float_join(whole, decimal):
109
  try:
110
  return float(f"{whole}.{decimal}")
 
114
  except Exception:
115
  return None
116
 
117
+ def ts(dt=None):
118
+ return (dt or datetime.datetime.now()).strftime('%H:%M:%S')
119
+
120
+ def random_sleep_tick():
121
+ time.sleep(random.uniform(MIN_TICK_SLEEP_SEC, MAX_TICK_SLEEP_SEC))
122
+
123
+ def now_utc():
124
+ return datetime.datetime.now(datetime.timezone.utc)
125
+
126
+ def format_zone_times(dt_utc: datetime.datetime) -> Dict[str, Any]:
127
+ def last_sunday(year, month):
128
+ d = datetime.datetime(year, month, 31, tzinfo=datetime.timezone.utc)
129
+ while d.weekday() != 6:
130
+ d -= datetime.timedelta(days=1)
131
+ return d
132
+
133
+ def nth_weekday_of_month(year, month, weekday, n):
134
+ d = datetime.datetime(year, month, 1, tzinfo=datetime.timezone.utc)
135
+ while d.weekday() != weekday:
136
+ d += datetime.timedelta(days=1)
137
+ d += datetime.timedelta(days=7*(n-1))
138
+ return d
139
+
140
+ y = dt_utc.year
141
+
142
+ last_sun_march = last_sunday(y, 3).replace(hour=1, minute=0, second=0, microsecond=0)
143
+ last_sun_oct = last_sunday(y, 10).replace(hour=1, minute=0, second=0, microsecond=0)
144
+ london_is_bst = last_sun_march <= dt_utc < last_sun_oct
145
+ london_offset = datetime.timedelta(hours=1 if london_is_bst else 0)
146
+ london_sfx = "BST" if london_is_bst else "GMT"
147
+ london_time = (dt_utc + london_offset).replace(tzinfo=None)
148
+
149
+ second_sun_march = nth_weekday_of_month(y, 3, 6, 2).replace(hour=7)
150
+ first_sun_nov = nth_weekday_of_month(y, 11, 6, 1).replace(hour=6)
151
+ ny_is_edt = second_sun_march <= dt_utc < first_sun_nov
152
+ ny_offset = datetime.timedelta(hours=-4 if ny_is_edt else -5)
153
+ ny_sfx = "EDT" if ny_is_edt else "EST"
154
+ ny_time = (dt_utc + ny_offset).replace(tzinfo=None)
155
+
156
+ tokyo_time = (dt_utc + datetime.timedelta(hours=9)).replace(tzinfo=None)
157
+
158
+ first_sun_oct = nth_weekday_of_month(y, 10, 6, 1).replace(hour=16)
159
+ first_sun_apr = nth_weekday_of_month(y, 4, 6, 1).replace(hour=16)
160
+ syd_is_aedt = (dt_utc >= first_sun_oct) or (dt_utc < first_sun_apr)
161
+ sydney_offset = datetime.timedelta(hours=11 if syd_is_aedt else 10)
162
+ sydney_sfx = "AEDT" if syd_is_aedt else "AEST"
163
+ sydney_time = (dt_utc + sydney_offset).replace(tzinfo=None)
164
+
165
+ return {
166
+ "zones": {
167
+ "Greenwich": f"{dt_utc.replace(tzinfo=None).strftime('%Y-%m-%d %H:%M:%S')} UTC",
168
+ "London": f"{london_time.strftime('%Y-%m-%d %H:%M:%S')} {london_sfx}",
169
+ "New York": f"{ny_time.strftime('%Y-%m-%d %H:%M:%S')} {ny_sfx}",
170
+ "Tokyo": f"{tokyo_time.strftime('%Y-%m-%d %H:%M:%S')} JST",
171
+ "Sydney": f"{sydney_time.strftime('%Y-%m-%d %H:%M:%S')} {sydney_sfx}",
172
+ },
173
+ "iso_utc": dt_utc.isoformat()
174
+ }
175
+
176
+ def send_message_to_users(message: str, max_retries=5, retry_delay=10):
177
+ headers = {
178
+ "Content-Type": "application/json",
179
+ "X-API-Key": MESSAGE_API_KEY
180
+ }
181
+ payload = {"message": message}
182
+ for attempt in range(1, max_retries + 1):
183
+ try:
184
+ response = requests.post(MESSAGE_API_URL, headers=headers, data=json.dumps(payload), timeout=8)
185
+ if response.status_code == 200:
186
+ logger.info(f"Message sent to users successfully on attempt {attempt}")
187
+ return {"success": True, "response": response.json()}
188
+ else:
189
+ logger.warning(f"Attempt {attempt}: Users API status {response.status_code}, body: {response.text[:200]}")
190
+ except requests.exceptions.RequestException as e:
191
+ logger.warning(f"Attempt {attempt}: Users API request failed: {e}")
192
+ if attempt < max_retries:
193
+ time.sleep(retry_delay)
194
+ else:
195
+ logger.error("Max retries reached. Failed to send user message.")
196
+ return {"success": False, "error": "Failed after retries"}
197
+
198
+ def play_alert_sound():
199
+ def _beep():
200
+ if HAS_WINSOUND:
201
+ try:
202
+ for _ in range(3):
203
+ winsound.Beep(1000, 350)
204
+ time.sleep(0.15)
205
+ except Exception:
206
+ for _ in range(3):
207
+ sys.stdout.write('\a'); sys.stdout.flush()
208
+ time.sleep(0.2)
209
+ else:
210
+ for _ in range(3):
211
+ sys.stdout.write('\a'); sys.stdout.flush()
212
+ time.sleep(0.2)
213
+ threading.Thread(target=_beep, daemon=True).start()
214
+
215
+ # ================ Prices ================
216
  def get_current_price(symbol=SYMBOL_DEFAULT):
 
 
 
 
217
  try:
218
  r = requests.get(API_URL, headers=API_HEADERS, timeout=REQUEST_TIMEOUT_SECONDS)
219
  r.raise_for_status()
220
  data = r.json()
 
221
  if symbol not in data:
222
+ logger.warning(f"[DATA] Symbol {symbol} not found in feed.")
223
  return None
 
224
  row = data[symbol]
225
  if not isinstance(row, list) or len(row) < 4:
226
+ logger.warning(f"[DATA] Unexpected data format for {symbol}.")
227
  return None
 
228
  bid = safe_float_join(row[0], row[1])
229
  ask = safe_float_join(row[2], row[3])
 
230
  if bid is None or ask is None or math.isnan(bid) or math.isnan(ask):
231
+ logger.warning(f"[DATA] Invalid price values for {symbol}.")
232
  return None
 
233
  return {"bid": bid, "ask": ask}
 
234
  except requests.RequestException as e:
235
+ logger.warning(f"[NETWORK] {e}")
236
  return None
237
  except Exception as e:
238
+ logger.warning(f"[ERROR] Unexpected: {e}")
239
  return None
240
 
241
+ def mid_price(bid: float, ask: float) -> float:
242
+ return (bid + ask) / 2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ # ================ External orchestration ================
245
+ def call_stop_analysis_and_clear_others():
246
+ try:
247
+ requests.get(ANALYSIS_STOP_URL, timeout=5)
248
+ except Exception as e:
249
+ logger.warning(f"[ANALYSIS] stop_analysis failed: {e}")
250
+ try:
251
+ requests.get(OTHER_ALERT_CLEAR_URL, timeout=5)
252
+ except Exception as e:
253
+ logger.warning(f"[ALERT] clear failed: {e}")
254
 
255
+ def call_analysis_now(text: str):
256
+ try:
257
+ headers = {"Content-Type": "application/json"}
258
+ payload = {"message": text}
259
+ requests.post(ANALYSIS_NOW_URL, headers=headers, json=payload, timeout=8)
260
+ except Exception as e:
261
+ logger.warning(f"[ANALYSIS] analysis_now failed: {e}")
262
+
263
+ # ================ GitHub signal writers/readers ================
264
+ def write_active_scenario_to_github(pair: str, side: str, entry: str, sl: str, tp: str, dt_utc: datetime.datetime, preserve_timestamps: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
265
  """
266
+ Writes a single-element array as the active trade. If preserve_timestamps provided, use it;
267
+ otherwise compute fresh timestamps from dt_utc.
268
  """
269
+ timestamps = preserve_timestamps if preserve_timestamps else format_zone_times(dt_utc)
270
+ payload_obj = [{
271
+ "pair": pair,
272
+ "type": side,
273
+ "entry": str(entry),
274
+ "stop_loss": str(sl),
275
+ "take_profit": str(tp),
276
+ "timestamps": timestamps
277
+ }]
278
+ new_content = json.dumps(payload_obj, ensure_ascii=False)
279
+ try:
280
+ authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
281
+ if not authenticity_token or not commit_oid:
282
+ msg = "Failed to get authenticity_token/commit_oid."
283
+ logger.error(f"[GITHUB] {msg}")
284
+ return {"success": False, "message": msg}
285
+ result = update_user_json_file(authenticity_token, commit_oid, new_content)
286
+ if result.get("success"):
287
+ logger.info("[GITHUB] signals updated with active scenario.")
288
+ return {"success": True}
289
+ else:
290
+ logger.error(f"[GITHUB] Update failed: {result}")
291
+ return {"success": False, "message": result.get("message", "Unknown error")}
292
+ except Exception as e:
293
+ logger.error(f"[GITHUB] Exception: {e}")
294
+ return {"success": False, "message": str(e)}
 
 
295
 
296
+ def clear_github_signals_to_empty_list() -> Dict[str, Any]:
297
+ """
298
+ Clears the db_signals file to [] after SL/TP hit as requested.
299
+ """
300
+ try:
301
+ authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
302
+ if not authenticity_token or not commit_oid:
303
+ return {"success": False, "message": "Failed to get auth/commit"}
304
+ result = update_user_json_file(authenticity_token, commit_oid, "[]")
305
+ if result.get("success"):
306
+ logger.info("[GITHUB] signals cleared to []")
307
+ return {"success": True}
308
+ return {"success": False, "message": str(result)}
309
+ except Exception as e:
310
+ logger.error(f"[GITHUB] clear exception: {e}")
311
+ return {"success": False, "message": str(e)}
312
 
313
+ def read_current_scenario_from_github() -> Optional[Dict[str, Any]]:
314
  """
315
+ Reads the pre-activation scenario JSON structure:
316
+ {
317
+ "scenario": {
318
+ "Buy": {"at": "...", "SL": "...", "TP": "..."},
319
+ "Sell": {"at": "...", "SL": "...", "TP": "..."},
320
+ "timestamps": {...}
321
+ }
322
+ }
323
  """
324
+ try:
325
+ authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
326
+ if not authenticity_token or not commit_oid:
327
+ logger.error("[GITHUB] Missing auth tokens to read scenario")
328
+ return None
329
+ content = read_user_json_file(authenticity_token, commit_oid)
330
+ # content might be dict (scenario object) or list (active array) or empty
331
+ if isinstance(content, dict) and "scenario" in content:
332
+ return content
333
+ else:
334
+ # Not in pre-activation mode
335
+ return None
336
+ except Exception as e:
337
+ logger.warning(f"[SCENARIO] fetch failed: {e}")
338
+ return None
339
 
340
+ # ================ Activation helpers ================
341
+ class CancelToken:
342
+ def __init__(self):
343
+ self._cancel = False
344
+ self._lock = threading.Lock()
345
+ def cancel(self):
346
+ with self._lock:
347
+ self._cancel = True
348
+ def is_cancelled(self):
349
+ with self._lock:
350
+ return self._cancel
351
+
352
+ def init_price_snapshot(symbol) -> Optional[Dict[str, float]]:
353
  retries, snap = 0, None
354
  while retries < MAX_RETRIES and snap is None:
355
  snap = get_current_price(symbol)
356
  if snap is None:
357
  retries += 1
358
  if retries < MAX_RETRIES:
359
+ time.sleep(RETRY_BACKOFF_SECONDS * retries)
 
360
  else:
361
+ return None
362
+ return snap
363
+
364
+ def detect_cross(prev_price: float, curr_price: float, level: float, direction: Optional[str]) -> bool:
365
+ if direction == "up":
366
+ return prev_price < level <= curr_price
367
+ elif direction == "down":
368
+ return prev_price > level >= curr_price
369
+ else:
370
+ return (prev_price < level <= curr_price) or (prev_price > level >= curr_price)
371
+
372
+ def price_crossing_to_activate(side: str, entry: float, prev: float, curr: float) -> bool:
373
+ if side == "Buy":
374
+ return prev < entry <= curr
375
+ else:
376
+ return prev > entry >= curr
377
+
378
+ def wait_until_activation_dual(buy_entry: Optional[float],
379
+ sell_entry: Optional[float],
380
+ symbol: str,
381
+ cancel_token: "CancelToken",
382
+ max_minutes: int = 240) -> Optional[Dict[str, float]]:
383
+ if buy_entry is None and sell_entry is None:
384
+ return None
385
+ snap = init_price_snapshot(symbol)
386
+ if snap is None:
387
+ logger.error("[ACTIVATOR] Initial price fetch failed.")
388
+ return None
389
+ prev = mid_price(snap["bid"], snap["ask"])
390
+ t_end = datetime.datetime.now() + datetime.timedelta(minutes=max_minutes)
391
+ tick = 0
392
+ if buy_entry is not None and abs(prev - buy_entry) < 1e-9:
393
+ return {"side": "Buy", "price": prev}
394
+ if sell_entry is not None and abs(prev - sell_entry) < 1e-9:
395
+ return {"side": "Sell", "price": prev}
396
+ while datetime.datetime.now() < t_end and not cancel_token.is_cancelled():
397
+ q = get_current_price(symbol)
398
+ if q is None:
399
+ time.sleep(0.5)
400
+ continue
401
+ curr = mid_price(q["bid"], q["ask"])
402
+ # Sell first then Buy (stable tie-break)
403
+ if sell_entry is not None and price_crossing_to_activate("Sell", sell_entry, prev, curr):
404
+ return {"side": "Sell", "price": curr}
405
+ if buy_entry is not None and price_crossing_to_activate("Buy", buy_entry, prev, curr):
406
+ return {"side": "Buy", "price": curr}
407
+ tick += 1
408
+ if tick % PRINT_EVERY_N_TICKS == 0:
409
+ logger.info(f"[{ts()}] Waiting activation Buy@{buy_entry} Sell@{sell_entry}, now {curr:.3f}")
410
+ prev = curr
411
+ random_sleep_tick()
412
+ return None
413
+
414
+ # ================ SL/TP monitor ================
415
+ def start_sl_tp_monitor(side: str, entry: float, sl: float, tp: float, symbol: str = SYMBOL_DEFAULT):
416
+ cancel_token = CancelToken()
417
+ monitor_id = f"{int(time.time()*1000)}-{random.randint(1000,9999)}"
 
 
 
 
 
 
 
 
 
 
 
 
418
 
 
 
 
 
419
  def _runner():
420
  try:
421
+ snap = init_price_snapshot(symbol)
422
+ if snap is None:
423
+ logger.error("[MONITOR] Unable to retrieve initial price snapshot.")
424
+ return
425
+ bid0, ask0 = snap["bid"], snap["ask"]
426
+ price0 = mid_price(bid0, ask0)
427
+ prev_price = price0
428
+ hi_bid = bid0
429
+ lo_ask = ask0
430
+
431
+ if side == "Buy":
432
+ tp_dir = "up" if tp >= entry else None
433
+ sl_dir = "down" if sl <= entry else None
434
+ else:
435
+ tp_dir = "down" if tp <= entry else None
436
+ sl_dir = "up" if sl >= entry else None
437
+
438
+ tick = 0
439
+ while not cancel_token.is_cancelled():
440
+ q = get_current_price(symbol)
441
+ if q is None:
442
+ time.sleep(0.6)
443
+ continue
444
+ bid, ask = q["bid"], q["ask"]
445
+ price_now = mid_price(bid, ask)
446
+ hi_bid = max(hi_bid, bid)
447
+ lo_ask = min(lo_ask, ask)
448
+
449
+ hit = None
450
+ if detect_cross(prev_price, price_now, sl, sl_dir):
451
+ hit = ("SL", sl)
452
+ if hit is None and detect_cross(prev_price, price_now, tp, tp_dir):
453
+ hit = ("TP", tp)
454
+
455
+ if hit:
456
+ play_alert_sound()
457
+ hit_type, hit_price = hit
458
+ # 1) Notify user immediately
459
+ msg = f"{side.upper()} SIGNAL {hit_type} hit at {hit_price}\nEntry: {entry}\nTP: {tp}\nSL: {sl}\nPair: {symbol}"
460
+ send_message_to_users(msg)
461
+
462
+ # 2) Clear db_signals to []
463
+ try:
464
+ clear_github_signals_to_empty_list()
465
+ except Exception as e:
466
+ logger.warning(f"[GITHUB] clearing after hit failed: {e}")
467
+
468
+ # 3) Stop analysis and other alert system again
469
+ call_stop_analysis_and_clear_others()
470
+
471
+ # 4) Notify analysis of the result
472
+ analysis_msg = f"{symbol} {side} trade {hit_type} reached at {hit_price} (entry {entry}, TP {tp}, SL {sl})."
473
+ call_analysis_now(analysis_msg)
474
+
475
+ logger.info(f"[MONITOR-END] {side} {hit_type} hit.")
476
+ return
477
+
478
+ tick += 1
479
+ if tick % PRINT_EVERY_N_TICKS == 0:
480
+ logger.info(f"[{ts()}] {symbol} {side} Mid {price_now:.3f} | Bid {bid:.3f}(hi {hi_bid:.3f}) | Ask {ask:.3f}(lo {lo_ask:.3f}) | TP {tp} / SL {sl}")
481
+ prev_price = price_now
482
+ random_sleep_tick()
483
  except Exception as e:
484
+ logger.error(f"[MONITOR-THREAD] Error: {e}")
485
+ finally:
486
+ with ACTIVE_LOCK:
487
+ if ACTIVE_MONITOR.get("id") == monitor_id:
488
+ ACTIVE_MONITOR["thread"] = None
489
+ ACTIVE_MONITOR["cancel_token"] = None
490
+
491
  t = threading.Thread(target=_runner, daemon=True)
492
+ with ACTIVE_LOCK:
493
+ if ACTIVE_MONITOR["cancel_token"]:
494
+ ACTIVE_MONITOR["cancel_token"].cancel()
495
+ ACTIVE_MONITOR["thread"] = t
496
+ ACTIVE_MONITOR["cancel_token"] = cancel_token
497
+ ACTIVE_MONITOR["id"] = monitor_id
498
+ ACTIVE_MONITOR["symbol"] = symbol
499
+ ACTIVE_MONITOR["side"] = side
500
+ ACTIVE_MONITOR["entry"] = entry
501
+ ACTIVE_MONITOR["tp"] = tp
502
+ ACTIVE_MONITOR["sl"] = sl
503
+ # timestamps preserved in ACTIVE_MONITOR["timestamps"] by activator
504
  t.start()
505
 
506
+ # ================ Scenario fetcher ================
507
+ def fetch_current_scenario_struct() -> Optional[Dict[str, Any]]:
508
+ """
509
+ Reads the pre-activation scenario (Buy/Sell at/SL/TP) from db_signals storage.
510
+ Returns:
511
+ {
512
+ "scenario": {
513
+ "Buy": {"at": "...", "SL": "...", "TP": "..."},
514
+ "Sell": {"at": "...", "SL": "...", "TP": "..."},
515
+ "timestamps": {...}
516
+ }
517
+ }
518
+ or None if not available.
519
+ """
520
+ return read_current_scenario_from_github()
521
 
522
+ # ================ API Endpoints ================
523
+ @app.route("/track", methods=["POST"])
524
+ def track_signal():
525
  """
526
+ Accepts: {"Buy": number?, "Sell": number?}
527
+ Behavior:
528
+ - Stop analysis and clear other alert (immediately).
529
+ - Cancel any existing activation/monitor (newest wins).
530
+ - Start watcher for activation of Buy/Sell (gap-aware). On first hit:
531
+ * Play sound
532
+ * Read SL/TP from scenario in db_signals
533
+ * Write active array [ {...} ] with timestamps for the start moment
534
+ * Send user "BUY NOW/SELL NOW..." message
535
+ * Start SL/TP monitor
536
+ * Notify analysis via analysis_now
537
+ - Respond 200 immediately after accepting.
538
  """
539
  try:
540
  data = request.get_json(force=True)
541
  except Exception:
542
  return jsonify({"status": "error", "message": "Invalid JSON"}), 400
543
 
544
+ buy_at = data.get("Buy")
545
+ sell_at = data.get("Sell")
546
+
547
+ if buy_at is None and sell_at is None:
548
+ return jsonify({"status": "error", "message": "Provide Buy or Sell or both"}), 400
549
+
550
+ if buy_at is not None:
551
+ try:
552
+ buy_at = float(buy_at)
553
+ except Exception:
554
+ return jsonify({"status": "error", "message": "Buy must be numeric"}), 400
555
+
556
+ if sell_at is not None:
557
+ try:
558
+ sell_at = float(sell_at)
559
+ except Exception:
560
+ return jsonify({"status": "error", "message": "Sell must be numeric"}), 400
561
+
562
+ # 1) Stop external analysis and other alert now
563
+ call_stop_analysis_and_clear_others()
564
+
565
+ # 2) Cancel any current operation
566
+ with ACTIVE_LOCK:
567
+ if ACTIVE_MONITOR["cancel_token"]:
568
+ ACTIVE_MONITOR["cancel_token"].cancel()
569
+ # Reset active monitor state; activation not yet started
570
+ ACTIVE_MONITOR["thread"] = None
571
+ ACTIVE_MONITOR["cancel_token"] = None
572
+ ACTIVE_MONITOR["id"] = None
573
+ ACTIVE_MONITOR["symbol"] = SYMBOL_DEFAULT
574
+ ACTIVE_MONITOR["side"] = None
575
+ ACTIVE_MONITOR["entry"] = None
576
+ ACTIVE_MONITOR["tp"] = None
577
+ ACTIVE_MONITOR["sl"] = None
578
+ ACTIVE_MONITOR["timestamps"] = None
579
+
580
+ activator_cancel = CancelToken()
581
+
582
+ def _activator():
583
+ try:
584
+ act = wait_until_activation_dual(
585
+ buy_entry=buy_at,
586
+ sell_entry=sell_at,
587
+ symbol=SYMBOL_DEFAULT,
588
+ cancel_token=activator_cancel,
589
+ max_minutes=240
590
+ )
591
+ if act is None:
592
+ logger.info("[ACTIVATOR] Cancelled or timed out.")
593
+ return
594
+
595
+ chosen_side = act["side"]
596
+ entry_used = act["price"]
597
+
598
+ play_alert_sound()
599
+
600
+ # Read scenario SL/TP from db_signals
601
+ scenario_blob = fetch_current_scenario_struct()
602
+ if not scenario_blob or "scenario" not in scenario_blob:
603
+ logger.error("[ACTIVATOR] Missing scenario data to read SL/TP.")
604
+ send_message_to_users(f"ERROR: Missing scenario SL/TP for {chosen_side} at {entry_used}")
605
+ return
606
+
607
+ side_block = scenario_blob["scenario"].get(chosen_side, {})
608
+ try:
609
+ sl_f = float(side_block.get("SL"))
610
+ tp_f = float(side_block.get("TP"))
611
+ except Exception:
612
+ logger.error("[ACTIVATOR] Invalid SL/TP from scenario data.")
613
+ send_message_to_users(f"ERROR: Invalid SL/TP for {chosen_side} at {entry_used}")
614
+ return
615
+
616
+ # 3) Write active array with fresh timestamps for the start moment
617
+ dt_utc = now_utc()
618
+ ts_block = format_zone_times(dt_utc)
619
+ wr = write_active_scenario_to_github(
620
+ pair=SYMBOL_DEFAULT,
621
+ side=chosen_side,
622
+ entry=str(entry_used),
623
+ sl=str(sl_f),
624
+ tp=str(tp_f),
625
+ dt_utc=dt_utc
626
+ )
627
+ if not wr.get("success"):
628
+ logger.warning(f"[GITHUB] Write failed: {wr}")
629
+
630
+ # Save timestamps in memory to preserve on /ts_points updates
631
+ with ACTIVE_LOCK:
632
+ ACTIVE_MONITOR["timestamps"] = ts_block
633
+
634
+ # 4) Notify users
635
+ user_msg = f"{chosen_side.upper()} NOW at {entry_used}\nSL: {sl_f}\nTP: {tp_f}\nPair: {SYMBOL_DEFAULT}"
636
+ send_message_to_users(user_msg)
637
+
638
+ # 5) Inform analysis
639
+ call_analysis_now(f"Scenario {chosen_side} activated for {SYMBOL_DEFAULT} at {entry_used}. SL {sl_f}, TP {tp_f}.")
640
+
641
+ # 6) Start SL/TP monitor
642
+ start_sl_tp_monitor(side=chosen_side, entry=entry_used, sl=sl_f, tp=tp_f, symbol=SYMBOL_DEFAULT)
643
 
644
+ except Exception as e:
645
+ logger.error(f"[ACTIVATOR] Error: {e}")
646
 
647
+ t = threading.Thread(target=_activator, daemon=True)
648
+ t.start()
649
+
650
+ return jsonify({"status": "accepted", "message": f"Tracking activation for Buy at {buy_at} and/or Sell at {sell_at} started."}), 200
651
+
652
+ @app.route("/ts_points", methods=["POST"])
653
+ def ts_points():
654
+ """
655
+ Accepts: {"TP": "...", "SL": "..."} to update active trade points.
656
+ Behavior:
657
+ - Requires an active trade.
658
+ - Cancel current monitor and restart with the updated levels.
659
+ - DO NOT change the activation timestamps (trade start time).
660
+ - Rewrite GitHub active array with preserved timestamps.
661
+ - Notify users and analysis.
662
+ """
663
  try:
664
+ data = request.get_json(force=True)
 
 
665
  except Exception:
666
+ return jsonify({"status": "error", "message": "Invalid JSON"}), 400
667
+
668
+ tp_new = data.get("TP")
669
+ sl_new = data.get("SL")
670
+ if tp_new is None and sl_new is None:
671
+ return jsonify({"status": "error", "message": "Provide TP or SL"}), 400
672
+
673
+ with ACTIVE_LOCK:
674
+ if ACTIVE_MONITOR["id"] is None or ACTIVE_MONITOR["side"] is None:
675
+ return jsonify({"status": "error", "message": "No active trade to update"}), 400
676
+
677
+ # Parse and stage updates
678
+ if tp_new is not None:
679
+ try:
680
+ ACTIVE_MONITOR["tp"] = float(tp_new)
681
+ except Exception:
682
+ return jsonify({"status": "error", "message": "Invalid TP"}), 400
683
+ if sl_new is not None:
684
+ try:
685
+ ACTIVE_MONITOR["sl"] = float(sl_new)
686
+ except Exception:
687
+ return jsonify({"status": "error", "message": "Invalid SL"}), 400
688
+
689
+ side = ACTIVE_MONITOR["side"]
690
+ entry = ACTIVE_MONITOR["entry"]
691
+ symbol = ACTIVE_MONITOR["symbol"]
692
+ tp = ACTIVE_MONITOR["tp"]
693
+ sl = ACTIVE_MONITOR["sl"]
694
+ preserved_ts = ACTIVE_MONITOR["timestamps"]
695
+
696
+ if ACTIVE_MONITOR["cancel_token"]:
697
+ ACTIVE_MONITOR["cancel_token"].cancel()
698
+
699
+ # Restart monitor with new TP/SL
700
+ start_sl_tp_monitor(side=side, entry=entry, sl=sl, tp=tp, symbol=symbol)
701
+
702
+ # Update GitHub record preserving timestamps (do NOT change time)
703
+ wr = write_active_scenario_to_github(
704
+ pair=symbol,
705
+ side=side,
706
+ entry=str(entry),
707
+ sl=str(sl),
708
+ tp=str(tp),
709
+ dt_utc=now_utc(), # ignored when preserve_timestamps provided
710
+ preserve_timestamps=preserved_ts
711
+ )
712
+ if not wr.get("success"):
713
+ logger.warning(f"[GITHUB] Update after TS change failed: {wr}")
714
 
715
+ # Notify users
716
+ msg = f"{side.upper()} update: TP -> {tp if tp_new is not None else '(unchanged)'} | SL -> {sl if sl_new is not None else '(unchanged)'}"
717
+ send_message_to_users(msg)
 
718
 
719
+ # Notify analysis
720
+ call_analysis_now(f"{symbol} {side} updated: TP {tp if tp_new is not None else '(unchanged)'}, SL {sl if sl_new is not None else '(unchanged)'}.")
721
 
722
+ return jsonify({"status": "ok", "message": "TP/SL updated and tracking restarted"}), 200
 
723
 
724
  @app.route("/", methods=["GET"])
725
  def root():
726
  return jsonify({
727
+ "service": "Signal Trade Activation & SL/TP Tracking (PRO)",
728
+ "symbol": SYMBOL_DEFAULT,
729
+ "routes": {
730
+ "POST /track": "Payload: { 'Buy': price?, 'Sell': price? }. Starts activation tracking. Newest wins.",
731
+ "POST /ts_points": "Payload: { 'TP': new_tp?, 'SL': new_sl? }. Updates active trade without changing start time."
732
+ },
733
+ "notes": [
734
+ "Only one operation active at a time; newest request cancels previous (across users).",
735
+ "On activation: stop analysis & clear other alert, write single-scenario array with fresh timestamps, notify user, start SL/TP monitor, notify analysis.",
736
+ "On SL/TP hit: notify user first, then clear db_signals to [], stop analysis & clear other alert, and inform analysis.",
737
+ "On /ts_points: do NOT change timestamps in GitHub; preserve trade start time; restart monitor with new TP/SL."
738
+ ]
739
  }), 200
740
 
741
+ # ================ Main ================
742
  if __name__ == "__main__":
 
743
  app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)