Update app.py
Browse files
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
|
| 21 |
from db_signals import (
|
| 22 |
fetch_authenticity_token_and_commit_oid,
|
| 23 |
update_user_json_file,
|
| 24 |
-
#
|
| 25 |
)
|
| 26 |
|
| 27 |
# ================ Configuration ================
|
|
@@ -73,17 +73,23 @@ ACTIVE_MONITOR = {
|
|
| 73 |
"entry": None,
|
| 74 |
"tp": None,
|
| 75 |
"sl": None,
|
| 76 |
-
"timestamps": None #
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
|
|
|
| 308 |
"""
|
| 309 |
-
Reads the pre-activation scenario JSON when signals.json is an array
|
| 310 |
-
[
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 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
|
| 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
|
| 540 |
* Play sound
|
| 541 |
-
*
|
| 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 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 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 |
-
|
| 610 |
-
|
| 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 |
-
|
| 617 |
try:
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
|
|
|
|
|
|
| 646 |
|
| 647 |
-
|
| 648 |
-
|
| 649 |
|
| 650 |
-
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
| 652 |
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
-
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 737 |
-
"
|
| 738 |
-
"
|
| 739 |
-
"On /
|
|
|
|
| 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 |
|