Dooratre commited on
Commit
4b00904
·
verified ·
1 Parent(s): c395c20

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +147 -112
app.py CHANGED
@@ -17,11 +17,11 @@ except ImportError:
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 ================
@@ -73,17 +73,23 @@ ACTIVE_MONITOR = {
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):
83
- """
84
- Returns parsed JSON: list or dict, or [] if empty/unparseable.
85
- """
86
- from db_signals import get_user_json_file
87
  res = get_user_json_file(authenticity_token, commit_oid)
88
  if not res.get("success"):
89
  raise RuntimeError(f"Failed to read user json file: {res}")
@@ -176,7 +182,7 @@ def send_message_to_users(message: str, max_retries=5, retry_delay=10):
176
  response = requests.post(MESSAGE_API_URL, headers=headers, data=json.dumps(payload), timeout=8)
177
  if response.status_code == 200:
178
  logger.info(f"Message sent to users successfully on attempt {attempt}")
179
- return {"success": True, "response": response.json()}
180
  else:
181
  logger.warning(f"Attempt {attempt}: Users API status {response.status_code}, body: {response.text[:200]}")
182
  except requests.exceptions.RequestException as e:
@@ -252,10 +258,10 @@ def call_analysis_now(text: str):
252
  except Exception as e:
253
  logger.warning(f"[ANALYSIS] analysis_now failed: {e}")
254
 
255
- # ================ GitHub signal writers/readers ================
256
  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]:
257
  """
258
- Always writes signals.json as a single-element array [ { active_trade } ].
259
  If preserve_timestamps provided, keep it; else compute fresh timestamps.
260
  """
261
  timestamps = preserve_timestamps if preserve_timestamps else format_zone_times(dt_utc)
@@ -289,7 +295,7 @@ def write_active_scenario_to_github(pair: str, side: str, entry: str, sl: str, t
289
 
290
  def clear_github_signals_to_empty_list() -> Dict[str, Any]:
291
  """
292
- Clears the db_signals file to [] after SL/TP hit as requested.
293
  """
294
  try:
295
  authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
@@ -304,19 +310,14 @@ def clear_github_signals_to_empty_list() -> Dict[str, Any]:
304
  logger.error(f"[GITHUB] clear exception: {e}")
305
  return {"success": False, "message": str(e)}
306
 
307
- def read_current_scenario_from_github() -> Optional[Dict[str, Any]]:
 
308
  """
309
- Reads the pre-activation scenario JSON when signals.json is an array, e.g.:
310
- [
311
- {
312
- "scenario": {
313
- "Buy": {"at": "...", "SL": "...", "TP": "..."},
314
- "Sell": {"at": "...", "SL": "...", "TP": "..."},
315
- "timestamps": {...}
316
- }
317
- }
318
- ]
319
- Returns {"scenario": {...}} or None if not found.
320
  """
321
  try:
322
  authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
@@ -344,7 +345,6 @@ def read_current_scenario_from_github() -> Optional[Dict[str, Any]]:
344
  except Exception as e:
345
  logger.warning(f"[SCENARIO] fetch failed: {e}")
346
  return None
347
-
348
 
349
  # ================ Activation helpers ================
350
  class CancelToken:
@@ -499,6 +499,7 @@ def start_sl_tp_monitor(side: str, entry: float, sl: float, tp: float, symbol: s
499
 
500
  t = threading.Thread(target=_runner, daemon=True)
501
  with ACTIVE_LOCK:
 
502
  if ACTIVE_MONITOR["cancel_token"]:
503
  ACTIVE_MONITOR["cancel_token"].cancel()
504
  ACTIVE_MONITOR["thread"] = t
@@ -509,25 +510,9 @@ def start_sl_tp_monitor(side: str, entry: float, sl: float, tp: float, symbol: s
509
  ACTIVE_MONITOR["entry"] = entry
510
  ACTIVE_MONITOR["tp"] = tp
511
  ACTIVE_MONITOR["sl"] = sl
512
- # timestamps preserved in ACTIVE_MONITOR["timestamps"] by activator
513
  t.start()
514
 
515
- # ================ Scenario fetcher ================
516
- def fetch_current_scenario_struct() -> Optional[Dict[str, Any]]:
517
- """
518
- Reads the pre-activation scenario (Buy/Sell at/SL/TP) from db_signals storage.
519
- Returns:
520
- {
521
- "scenario": {
522
- "Buy": {"at": "...", "SL": "...", "TP": "..."},
523
- "Sell": {"at": "...", "SL": "...", "TP": "..."},
524
- "timestamps": {...}
525
- }
526
- }
527
- or None if not available.
528
- """
529
- return read_current_scenario_from_github()
530
-
531
  # ================ API Endpoints ================
532
  @app.route("/track", methods=["POST"])
533
  def track_signal():
@@ -536,11 +521,11 @@ def track_signal():
536
  Behavior:
537
  - Stop analysis and clear other alert (immediately).
538
  - Cancel any existing activation/monitor (newest wins).
539
- - Start watcher for activation of Buy/Sell (gap-aware). On first hit:
540
  * Play sound
541
- * Read SL/TP from scenario in db_signals
542
  * Write active array [ {...} ] with timestamps for the start moment
543
- * Send user "BUY NOW/SELL NOW..." message
544
  * Start SL/TP monitor
545
  * Notify analysis via analysis_now
546
  - Respond 200 immediately after accepting.
@@ -571,11 +556,10 @@ def track_signal():
571
  # 1) Stop external analysis and other alert now
572
  call_stop_analysis_and_clear_others()
573
 
574
- # 2) Cancel any current operation
575
  with ACTIVE_LOCK:
576
  if ACTIVE_MONITOR["cancel_token"]:
577
  ACTIVE_MONITOR["cancel_token"].cancel()
578
- # Reset active monitor state; activation not yet started
579
  ACTIVE_MONITOR["thread"] = None
580
  ACTIVE_MONITOR["cancel_token"] = None
581
  ACTIVE_MONITOR["id"] = None
@@ -586,75 +570,118 @@ def track_signal():
586
  ACTIVE_MONITOR["sl"] = None
587
  ACTIVE_MONITOR["timestamps"] = None
588
 
589
- activator_cancel = CancelToken()
590
-
591
- def _activator():
592
- try:
593
- act = wait_until_activation_dual(
594
- buy_entry=buy_at,
595
- sell_entry=sell_at,
596
- symbol=SYMBOL_DEFAULT,
597
- cancel_token=activator_cancel,
598
- max_minutes=240
599
- )
600
- if act is None:
601
- logger.info("[ACTIVATOR] Cancelled or timed out.")
602
- return
603
-
604
- chosen_side = act["side"]
605
- entry_used = act["price"]
606
-
607
- play_alert_sound()
608
 
609
- # Read scenario SL/TP from db_signals
610
- scenario_blob = fetch_current_scenario_struct()
611
- if not scenario_blob or "scenario" not in scenario_blob:
612
- logger.error("[ACTIVATOR] Missing scenario data to read SL/TP.")
613
- send_message_to_users(f"ERROR: Missing scenario SL/TP for {chosen_side} at {entry_used}")
614
- return
615
 
616
- side_block = scenario_blob["scenario"].get(chosen_side, {})
617
  try:
618
- sl_f = float(side_block.get("SL"))
619
- tp_f = float(side_block.get("TP"))
620
- except Exception:
621
- logger.error("[ACTIVATOR] Invalid SL/TP from scenario data.")
622
- send_message_to_users(f"ERROR: Invalid SL/TP for {chosen_side} at {entry_used}")
623
- return
624
 
625
- # 3) Write active array with fresh timestamps for the start moment
626
- dt_utc = now_utc()
627
- ts_block = format_zone_times(dt_utc)
628
- wr = write_active_scenario_to_github(
629
- pair=SYMBOL_DEFAULT,
630
- side=chosen_side,
631
- entry=str(entry_used),
632
- sl=str(sl_f),
633
- tp=str(tp_f),
634
- dt_utc=dt_utc
635
- )
636
- if not wr.get("success"):
637
- logger.warning(f"[GITHUB] Write failed: {wr}")
638
-
639
- # Save timestamps in memory to preserve on /ts_points updates
640
- with ACTIVE_LOCK:
641
- ACTIVE_MONITOR["timestamps"] = ts_block
 
 
 
 
 
 
 
 
 
 
 
642
 
643
- # 4) Notify users
644
- user_msg = f"{chosen_side.upper()} NOW at {entry_used}\nSL: {sl_f}\nTP: {tp_f}\nPair: {SYMBOL_DEFAULT}"
645
- send_message_to_users(user_msg)
 
 
646
 
647
- # 5) Inform analysis
648
- call_analysis_now(f"Scenario {chosen_side} activated for {SYMBOL_DEFAULT} at {entry_used}. SL {sl_f}, TP {tp_f}.")
649
 
650
- # 6) Start SL/TP monitor
651
- start_sl_tp_monitor(side=chosen_side, entry=entry_used, sl=sl_f, tp=tp_f, symbol=SYMBOL_DEFAULT)
 
 
 
652
 
653
- except Exception as e:
654
- logger.error(f"[ACTIVATOR] Error: {e}")
 
 
 
655
 
656
- t = threading.Thread(target=_activator, daemon=True)
657
- t.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
  return jsonify({"status": "accepted", "message": f"Tracking activation for Buy at {buy_at} and/or Sell at {sell_at} started."}), 200
660
 
@@ -708,7 +735,7 @@ def ts_points():
708
  # Restart monitor with new TP/SL
709
  start_sl_tp_monitor(side=side, entry=entry, sl=sl, tp=tp, symbol=symbol)
710
 
711
- # Update GitHub record preserving timestamps (do NOT change time)
712
  wr = write_active_scenario_to_github(
713
  pair=symbol,
714
  side=side,
@@ -721,6 +748,13 @@ def ts_points():
721
  if not wr.get("success"):
722
  logger.warning(f"[GITHUB] Update after TS change failed: {wr}")
723
 
 
 
 
 
 
 
 
724
  return jsonify({"status": "ok", "message": "TP/SL updated and tracking restarted"}), 200
725
 
726
  @app.route("/", methods=["GET"])
@@ -733,10 +767,11 @@ def root():
733
  "POST /ts_points": "Payload: { 'TP': new_tp?, 'SL': new_sl? }. Updates active trade without changing start time."
734
  },
735
  "notes": [
736
- "Only one operation active at a time; newest request cancels previous (across users).",
737
- "On activation: stop analysis & clear other alert, write single-scenario array with fresh timestamps, notify user, start SL/TP monitor, notify analysis.",
738
- "On SL/TP hit: notify user first, then clear db_signals to [], stop analysis & clear other alert, and inform analysis.",
739
- "On /ts_points: do NOT change timestamps in GitHub; preserve trade start time; restart monitor with new TP/SL."
 
740
  ]
741
  }), 200
742
 
 
17
 
18
  from flask import Flask, request, jsonify
19
 
20
+ # db_signals helpers you already have
21
  from db_signals import (
22
  fetch_authenticity_token_and_commit_oid,
23
  update_user_json_file,
24
+ get_user_json_file, # we will call this directly
25
  )
26
 
27
  # ================ Configuration ================
 
73
  "entry": None,
74
  "tp": None,
75
  "sl": None,
76
+ "timestamps": None # preserved for /ts_points
77
  }
78
  ACTIVE_LOCK = threading.Lock()
79
 
80
+ # New: single activator governance
81
+ ACTIVE_ACTIVATOR = {
82
+ "thread": None,
83
+ "cancel_token": None,
84
+ "id": None
85
+ }
86
+ ACTIVATOR_LOCK = threading.Lock()
87
+
88
+ # Latch to prevent duplicate activations
89
+ ACTIVATION_LATCH = threading.Event()
90
+
91
  # ================ db_signals reader helper ================
 
92
  def read_user_json_file(authenticity_token: str, commit_oid: str):
 
 
 
 
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}")
 
182
  response = requests.post(MESSAGE_API_URL, headers=headers, data=json.dumps(payload), timeout=8)
183
  if response.status_code == 200:
184
  logger.info(f"Message sent to users successfully on attempt {attempt}")
185
+ return {"success": True, "response": response.json() if response.content else {}}
186
  else:
187
  logger.warning(f"Attempt {attempt}: Users API status {response.status_code}, body: {response.text[:200]}")
188
  except requests.exceptions.RequestException as e:
 
258
  except Exception as e:
259
  logger.warning(f"[ANALYSIS] analysis_now failed: {e}")
260
 
261
+ # ================ GitHub writers =================
262
  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]:
263
  """
264
+ Writes signals.json as a single-element array [ { active_trade } ].
265
  If preserve_timestamps provided, keep it; else compute fresh timestamps.
266
  """
267
  timestamps = preserve_timestamps if preserve_timestamps else format_zone_times(dt_utc)
 
295
 
296
  def clear_github_signals_to_empty_list() -> Dict[str, Any]:
297
  """
298
+ Clears the db_signals file to [] after SL/TP hit.
299
  """
300
  try:
301
  authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
 
310
  logger.error(f"[GITHUB] clear exception: {e}")
311
  return {"success": False, "message": str(e)}
312
 
313
+ # ================ Scenario fetcher ================
314
+ def fetch_current_scenario_struct() -> Optional[Dict[str, Any]]:
315
  """
316
+ Reads the pre-activation scenario JSON when signals.json is an array:
317
+ [ { "scenario": { "Buy": {"at": "...", "SL": "...", "TP": "..."},
318
+ "Sell": {"at": "...", "SL": "...", "TP": "..."},
319
+ "timestamps": {...} } } ]
320
+ Returns {"scenario": {...}} or None.
 
 
 
 
 
 
321
  """
322
  try:
323
  authenticity_token, commit_oid = fetch_authenticity_token_and_commit_oid()
 
345
  except Exception as e:
346
  logger.warning(f"[SCENARIO] fetch failed: {e}")
347
  return None
 
348
 
349
  # ================ Activation helpers ================
350
  class CancelToken:
 
499
 
500
  t = threading.Thread(target=_runner, daemon=True)
501
  with ACTIVE_LOCK:
502
+ # Cancel any running monitor before starting a new one
503
  if ACTIVE_MONITOR["cancel_token"]:
504
  ACTIVE_MONITOR["cancel_token"].cancel()
505
  ACTIVE_MONITOR["thread"] = t
 
510
  ACTIVE_MONITOR["entry"] = entry
511
  ACTIVE_MONITOR["tp"] = tp
512
  ACTIVE_MONITOR["sl"] = sl
513
+ # timestamps remains as is (set by activator)
514
  t.start()
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  # ================ API Endpoints ================
517
  @app.route("/track", methods=["POST"])
518
  def track_signal():
 
521
  Behavior:
522
  - Stop analysis and clear other alert (immediately).
523
  - Cancel any existing activation/monitor (newest wins).
524
+ - Start watcher for activation of Buy/Sell. On first hit (latched):
525
  * Play sound
526
+ * Use cached SL/TP from scenario read at start
527
  * Write active array [ {...} ] with timestamps for the start moment
528
+ * Send user "BUY NOW/SELL NOW..." message (once)
529
  * Start SL/TP monitor
530
  * Notify analysis via analysis_now
531
  - Respond 200 immediately after accepting.
 
556
  # 1) Stop external analysis and other alert now
557
  call_stop_analysis_and_clear_others()
558
 
559
+ # 2) Cancel any current SL/TP monitor operation and reset active monitor state
560
  with ACTIVE_LOCK:
561
  if ACTIVE_MONITOR["cancel_token"]:
562
  ACTIVE_MONITOR["cancel_token"].cancel()
 
563
  ACTIVE_MONITOR["thread"] = None
564
  ACTIVE_MONITOR["cancel_token"] = None
565
  ACTIVE_MONITOR["id"] = None
 
570
  ACTIVE_MONITOR["sl"] = None
571
  ACTIVE_MONITOR["timestamps"] = None
572
 
573
+ # 3) Cancel any existing activator and set up new one
574
+ with ACTIVATOR_LOCK:
575
+ if ACTIVE_ACTIVATOR["cancel_token"]:
576
+ ACTIVE_ACTIVATOR["cancel_token"].cancel()
577
+ ACTIVATION_LATCH.clear() # new cycle, no activation yet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
+ activator_cancel = CancelToken()
580
+ activator_id = f"act-{int(time.time()*1000)}-{random.randint(1000,9999)}"
 
 
 
 
581
 
582
+ def _activator():
583
  try:
584
+ # Read scenario once and cache SL/TP locally to avoid race
585
+ scenario_blob = fetch_current_scenario_struct()
586
+ if not scenario_blob or "scenario" not in scenario_blob:
587
+ logger.error("[ACTIVATOR] Missing scenario data before waiting.")
588
+ send_message_to_users("ERROR: Missing scenario structure before activation. Please set scenario and retry.")
589
+ return
590
 
591
+ scen = scenario_blob["scenario"]
592
+ # Extract SL/TP for both sides if present
593
+ buy_sl = buy_tp = sell_sl = sell_tp = None
594
+ if "Buy" in scen:
595
+ try:
596
+ buy_sl = float(scen["Buy"].get("SL")) if scen["Buy"].get("SL") is not None else None
597
+ buy_tp = float(scen["Buy"].get("TP")) if scen["Buy"].get("TP") is not None else None
598
+ except Exception:
599
+ logger.error("[ACTIVATOR] Invalid Buy SL/TP in scenario.")
600
+ if "Sell" in scen:
601
+ try:
602
+ sell_sl = float(scen["Sell"].get("SL")) if scen["Sell"].get("SL") is not None else None
603
+ sell_tp = float(scen["Sell"].get("TP")) if scen["Sell"].get("TP") is not None else None
604
+ except Exception:
605
+ logger.error("[ACTIVATOR] Invalid Sell SL/TP in scenario.")
606
+
607
+ # Optional timestamps from scenario pre-activation are not used here; we create fresh on activate
608
+
609
+ act = wait_until_activation_dual(
610
+ buy_entry=buy_at,
611
+ sell_entry=sell_at,
612
+ symbol=SYMBOL_DEFAULT,
613
+ cancel_token=activator_cancel,
614
+ max_minutes=240
615
+ )
616
+ if act is None:
617
+ logger.info("[ACTIVATOR] Cancelled or timed out.")
618
+ return
619
 
620
+ # Latch: only the first activator proceeds
621
+ if ACTIVATION_LATCH.is_set():
622
+ logger.info("[ACTIVATOR] Activation already handled by another thread. Exiting.")
623
+ return
624
+ ACTIVATION_LATCH.set()
625
 
626
+ chosen_side = act["side"]
627
+ entry_used = act["price"]
628
 
629
+ # Choose SL/TP from cached scenario
630
+ if chosen_side == "Buy":
631
+ sl_f, tp_f = buy_sl, buy_tp
632
+ else:
633
+ sl_f, tp_f = sell_sl, sell_tp
634
 
635
+ if sl_f is None or tp_f is None:
636
+ # Cannot proceed without points
637
+ logger.error(f"[ACTIVATOR] Missing SL/TP for {chosen_side}.")
638
+ send_message_to_users(f"ERROR: Missing scenario SL/TP for {chosen_side} at {entry_used}")
639
+ return
640
 
641
+ play_alert_sound()
642
+
643
+ # 3) Write active array with fresh timestamps
644
+ dt_utc = now_utc()
645
+ ts_block = format_zone_times(dt_utc)
646
+ wr = write_active_scenario_to_github(
647
+ pair=SYMBOL_DEFAULT,
648
+ side=chosen_side,
649
+ entry=str(entry_used),
650
+ sl=str(sl_f),
651
+ tp=str(tp_f),
652
+ dt_utc=dt_utc
653
+ )
654
+ if not wr.get("success"):
655
+ logger.warning(f"[GITHUB] Write failed: {wr}")
656
+
657
+ # Save timestamps in memory to preserve on /ts_points updates
658
+ with ACTIVE_LOCK:
659
+ ACTIVE_MONITOR["timestamps"] = ts_block
660
+
661
+ # 4) Notify users (once)
662
+ user_msg = f"{chosen_side.upper()} NOW at {entry_used}\nSL: {sl_f}\nTP: {tp_f}\nPair: {SYMBOL_DEFAULT}"
663
+ send_message_to_users(user_msg)
664
+
665
+ # 5) Inform analysis
666
+ call_analysis_now(f"Scenario {chosen_side} activated for {SYMBOL_DEFAULT} at {entry_used}. SL {sl_f}, TP {tp_f}.")
667
+
668
+ # 6) Start SL/TP monitor
669
+ start_sl_tp_monitor(side=chosen_side, entry=entry_used, sl=sl_f, tp=tp_f, symbol=SYMBOL_DEFAULT)
670
+
671
+ except Exception as e:
672
+ logger.error(f"[ACTIVATOR] Error: {e}")
673
+ finally:
674
+ with ACTIVATOR_LOCK:
675
+ if ACTIVE_ACTIVATOR["id"] == activator_id:
676
+ ACTIVE_ACTIVATOR["thread"] = None
677
+ ACTIVE_ACTIVATOR["cancel_token"] = None
678
+ ACTIVE_ACTIVATOR["id"] = None
679
+
680
+ t = threading.Thread(target=_activator, daemon=True)
681
+ ACTIVE_ACTIVATOR["thread"] = t
682
+ ACTIVE_ACTIVATOR["cancel_token"] = activator_cancel
683
+ ACTIVE_ACTIVATOR["id"] = activator_id
684
+ t.start()
685
 
686
  return jsonify({"status": "accepted", "message": f"Tracking activation for Buy at {buy_at} and/or Sell at {sell_at} started."}), 200
687
 
 
735
  # Restart monitor with new TP/SL
736
  start_sl_tp_monitor(side=side, entry=entry, sl=sl, tp=tp, symbol=symbol)
737
 
738
+ # Update GitHub record preserving timestamps
739
  wr = write_active_scenario_to_github(
740
  pair=symbol,
741
  side=side,
 
748
  if not wr.get("success"):
749
  logger.warning(f"[GITHUB] Update after TS change failed: {wr}")
750
 
751
+ # Notify users and analysis about the update (optional; you can enable if needed)
752
+ try:
753
+ send_message_to_users(f"{side.upper()} TP/SL updated\nEntry: {entry}\nTP: {tp}\nSL: {sl}\nPair: {symbol}")
754
+ call_analysis_now(f"{symbol} {side} TP/SL updated. Entry {entry}, TP {tp}, SL {sl}.")
755
+ except Exception:
756
+ pass
757
+
758
  return jsonify({"status": "ok", "message": "TP/SL updated and tracking restarted"}), 200
759
 
760
  @app.route("/", methods=["GET"])
 
767
  "POST /ts_points": "Payload: { 'TP': new_tp?, 'SL': new_sl? }. Updates active trade without changing start time."
768
  },
769
  "notes": [
770
+ "Only one activator and one monitor run at a time; newest /track cancels previous.",
771
+ "Activation uses a latch so only one activation proceeds even under concurrency.",
772
+ "Scenario SL/TP are cached at activator start; no GitHub reads at activation moment.",
773
+ "On SL/TP hit: notify user, clear db_signals to [], stop analysis & clear other alert, and inform analysis.",
774
+ "On /ts_points: timestamps in GitHub are preserved (trade start time)."
775
  ]
776
  }), 200
777