Spaces:
Running
Running
Update botsignal.py
Browse files- botsignal.py +97 -23
botsignal.py
CHANGED
|
@@ -10,12 +10,13 @@ from datetime import datetime, timedelta, timezone
|
|
| 10 |
from mimetypes import guess_extension
|
| 11 |
from typing import List, Tuple, Optional, Dict
|
| 12 |
|
|
|
|
| 13 |
from rapidfuzz import fuzz
|
| 14 |
from telethon import TelegramClient, events
|
| 15 |
from telethon.sessions import StringSession, MemorySession
|
| 16 |
from telethon.errors.rpcerrorlist import FloodWaitError
|
| 17 |
|
| 18 |
-
|
| 19 |
from autotrack import setup_autotrack
|
| 20 |
|
| 21 |
|
|
@@ -180,7 +181,6 @@ RELEVANCE_THRESHOLD = float(os.environ.get("RELEVANCE_THRESHOLD", "0.6"))
|
|
| 180 |
EXCLUDE_PHRASES = [p.strip().lower() for p in os.environ.get(
|
| 181 |
"EXCLUDE_PHRASES",
|
| 182 |
"achievement unlocked,call profit:,achieving +"
|
| 183 |
-
|
| 184 |
).split(",") if p.strip()]
|
| 185 |
|
| 186 |
# ========= Client bootstrap =========
|
|
@@ -192,8 +192,10 @@ def build_client() -> TelegramClient:
|
|
| 192 |
return TelegramClient(MemorySession(), API_ID, API_HASH)
|
| 193 |
|
| 194 |
client = build_client()
|
| 195 |
-
|
|
|
|
| 196 |
setup_autotrack(client)
|
|
|
|
| 197 |
recent_hashes: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE)
|
| 198 |
recent_content_hashes: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE) # content-only dedup
|
| 199 |
recent_entity_keys: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE) # entity-based dedup
|
|
@@ -472,30 +474,101 @@ def _ca_from_key(keyword: str) -> str:
|
|
| 472 |
return keyword.split("ca:sol:", 1)[1]
|
| 473 |
return ""
|
| 474 |
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
"""
|
| 477 |
-
|
| 478 |
-
|
| 479 |
"""
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
"",
|
| 484 |
-
|
|
|
|
|
|
|
| 485 |
"",
|
| 486 |
-
|
| 487 |
-
"
|
|
|
|
| 488 |
"",
|
| 489 |
-
"
|
| 490 |
-
"
|
|
|
|
| 491 |
]
|
| 492 |
-
msg = "\n".join(
|
|
|
|
|
|
|
| 493 |
if body_snippet:
|
| 494 |
-
|
| 495 |
-
snippet = body_snippet.strip()
|
| 496 |
if snippet:
|
| 497 |
-
|
| 498 |
-
|
| 499 |
return msg
|
| 500 |
|
| 501 |
def format_body_with_spacing(body: str, tier_label: str) -> str:
|
|
@@ -559,8 +632,8 @@ async def post_or_update(keyword: str, body: str, new_tier: str, src_msg, *, upd
|
|
| 559 |
"""
|
| 560 |
Modified:
|
| 561 |
- If `keyword` represents a Contract Address (ca:evm:/ca:sol:),
|
| 562 |
-
we format with brand style
|
| 563 |
-
|
| 564 |
"""
|
| 565 |
prev = last_posted.get(keyword)
|
| 566 |
now_ts = datetime.now().timestamp()
|
|
@@ -568,7 +641,8 @@ async def post_or_update(keyword: str, body: str, new_tier: str, src_msg, *, upd
|
|
| 568 |
# Choose text based on whether this is a CA entity or not
|
| 569 |
if _is_ca_key(keyword):
|
| 570 |
ca_val = _ca_from_key(keyword)
|
| 571 |
-
|
|
|
|
| 572 |
else:
|
| 573 |
text_to_send = format_body_with_spacing(body, new_tier)
|
| 574 |
|
|
@@ -858,7 +932,7 @@ async def process_message(msg, source_chat_id: int) -> None:
|
|
| 858 |
core_u, sup_u = _unique_counts_by_role(topic_key)
|
| 859 |
if core_u < 1 and sup_u < SUPPORT_MIN_UNIQUE:
|
| 860 |
debug_log(
|
| 861 |
-
f"Support ditahan (core_u={core_u}, sup_u={
|
| 862 |
orig_text,
|
| 863 |
)
|
| 864 |
return
|
|
|
|
| 10 |
from mimetypes import guess_extension
|
| 11 |
from typing import List, Tuple, Optional, Dict
|
| 12 |
|
| 13 |
+
import aiohttp
|
| 14 |
from rapidfuzz import fuzz
|
| 15 |
from telethon import TelegramClient, events
|
| 16 |
from telethon.sessions import StringSession, MemorySession
|
| 17 |
from telethon.errors.rpcerrorlist import FloodWaitError
|
| 18 |
|
| 19 |
+
# Attach autotrack (silent start, reply >=1.5x) β pastikan autotrack.py kamu versi terbaru
|
| 20 |
from autotrack import setup_autotrack
|
| 21 |
|
| 22 |
|
|
|
|
| 181 |
EXCLUDE_PHRASES = [p.strip().lower() for p in os.environ.get(
|
| 182 |
"EXCLUDE_PHRASES",
|
| 183 |
"achievement unlocked,call profit:,achieving +"
|
|
|
|
| 184 |
).split(",") if p.strip()]
|
| 185 |
|
| 186 |
# ========= Client bootstrap =========
|
|
|
|
| 192 |
return TelegramClient(MemorySession(), API_ID, API_HASH)
|
| 193 |
|
| 194 |
client = build_client()
|
| 195 |
+
|
| 196 |
+
# Attach autotrack to the same Telethon client (silent start, reply >= 1.5x default)
|
| 197 |
setup_autotrack(client)
|
| 198 |
+
|
| 199 |
recent_hashes: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE)
|
| 200 |
recent_content_hashes: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE) # content-only dedup
|
| 201 |
recent_entity_keys: deque[str] = deque(maxlen=DEDUP_BUFFER_SIZE) # entity-based dedup
|
|
|
|
| 474 |
return keyword.split("ca:sol:", 1)[1]
|
| 475 |
return ""
|
| 476 |
|
| 477 |
+
# ===== Number format for MCAP line =====
|
| 478 |
+
def _fmt_big_usd(x):
|
| 479 |
+
if x is None:
|
| 480 |
+
return "β"
|
| 481 |
+
try:
|
| 482 |
+
x = float(x)
|
| 483 |
+
except:
|
| 484 |
+
return "β"
|
| 485 |
+
if x >= 1_000_000_000:
|
| 486 |
+
return f"${x/1_000_000_000:.2f}B"
|
| 487 |
+
if x >= 1_000_000:
|
| 488 |
+
return f"${x/1_000_000:.2f}M"
|
| 489 |
+
if x >= 1_000:
|
| 490 |
+
return f"${x/1_000:.2f}K"
|
| 491 |
+
return f"${x:.0f}"
|
| 492 |
+
|
| 493 |
+
# ===== Dexscreener fetch (MCAP/FDV) =====
|
| 494 |
+
DEXSCREENER_TOKEN_URL = "https://api.dexscreener.com/latest/dex/tokens/"
|
| 495 |
+
|
| 496 |
+
async def _fetch_initial_mcap(ca: str):
|
| 497 |
"""
|
| 498 |
+
Ambil perkiraan MCAP (marketCap atau FDV) sekali dari Dexscreener.
|
| 499 |
+
Return float atau None kalau gak ada.
|
| 500 |
"""
|
| 501 |
+
try:
|
| 502 |
+
timeout = aiohttp.ClientTimeout(total=8)
|
| 503 |
+
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
| 504 |
+
async with sess.get(DEXSCREENER_TOKEN_URL + ca) as r:
|
| 505 |
+
if r.status != 200:
|
| 506 |
+
return None
|
| 507 |
+
data = await r.json()
|
| 508 |
+
pairs = (data or {}).get("pairs") or []
|
| 509 |
+
if not pairs:
|
| 510 |
+
return None
|
| 511 |
+
# pilih pair dengan USD liquidity terbesar
|
| 512 |
+
best = max(pairs, key=lambda p: (p.get("liquidity", {}) or {}).get("usd", 0))
|
| 513 |
+
mc = best.get("marketCap")
|
| 514 |
+
fdv = best.get("fdv")
|
| 515 |
+
if isinstance(mc, (int, float)) and mc > 0:
|
| 516 |
+
return float(mc)
|
| 517 |
+
if isinstance(fdv, (int, float)) and fdv > 0:
|
| 518 |
+
return float(fdv)
|
| 519 |
+
return None
|
| 520 |
+
except:
|
| 521 |
+
return None
|
| 522 |
+
|
| 523 |
+
# ===== Milestones label (sinkron dengan env; default 1.5Γ β’ 2Γ) =====
|
| 524 |
+
_M_RAW = os.environ.get("MILESTONES", "1.5,2")
|
| 525 |
+
try:
|
| 526 |
+
_M_LIST = [x.strip() for x in _M_RAW.split(",") if x.strip()]
|
| 527 |
+
if not _M_LIST:
|
| 528 |
+
_M_LIST = ["1.5", "2"]
|
| 529 |
+
MILESTONES_LABEL = " β’ ".join(f"{m}Γ" for m in _M_LIST)
|
| 530 |
+
except Exception:
|
| 531 |
+
_M_LIST = ["1.5", "2"]
|
| 532 |
+
MILESTONES_LABEL = "1.5Γ β’ 2Γ"
|
| 533 |
+
|
| 534 |
+
# ===== Creative CA message with MCAP + links =====
|
| 535 |
+
def build_midas_message_for_ca(ca: str, tier_label: str, *, mcap_value=None, body_snippet: Optional[str] = None) -> str:
|
| 536 |
+
"""
|
| 537 |
+
Creative brand style untuk CA:
|
| 538 |
+
- Tampilkan MCAP kalau tersedia
|
| 539 |
+
- Sisipkan Dexscreener & Axiom
|
| 540 |
+
- Copy 'first alert 1.5Γ' menuju discovery (no ceilings)
|
| 541 |
+
"""
|
| 542 |
+
dexs_link = f"https://dexscreener.com/token/{ca}"
|
| 543 |
+
axiom_link = "https://axiom.trade/@1144321"
|
| 544 |
+
mcap_line = f"MCAP (est.): {_fmt_big_usd(mcap_value)}"
|
| 545 |
+
|
| 546 |
+
first_alert = _M_LIST[0] if _M_LIST else "1.5"
|
| 547 |
+
|
| 548 |
+
lines = [
|
| 549 |
+
"β¨ **MidasTouch β Fresh Alpha Drop**",
|
| 550 |
+
f"**Tier Now:** {tier_label}",
|
| 551 |
"",
|
| 552 |
+
"Hereβs a fresh contract worth a look:",
|
| 553 |
+
f"**CA**: `{ca}`",
|
| 554 |
+
mcap_line,
|
| 555 |
"",
|
| 556 |
+
"Quick actions:",
|
| 557 |
+
f"β’ π [Open on Dexscreener]({dexs_link})",
|
| 558 |
+
f"β’ π [Trade with Axiom]({axiom_link})",
|
| 559 |
"",
|
| 560 |
+
f"Auto-track armed β first alert at **{first_alert}Γ**; we hunt momentum to price discovery β no ceilings. π―",
|
| 561 |
+
"Plan the trade, trade the plan. Cut losers quick, compound the wins.",
|
| 562 |
+
"β **MidasTouch**",
|
| 563 |
]
|
| 564 |
+
msg = "\n".join(lines)
|
| 565 |
+
|
| 566 |
+
# (opsional) sisipkan snippet sumber
|
| 567 |
if body_snippet:
|
| 568 |
+
snippet = re.sub(r"\n{3,}", "\n\n", body_snippet.strip())
|
|
|
|
| 569 |
if snippet:
|
| 570 |
+
msg += "\n\n> " + snippet.replace("\n", "\n> ")
|
| 571 |
+
|
| 572 |
return msg
|
| 573 |
|
| 574 |
def format_body_with_spacing(body: str, tier_label: str) -> str:
|
|
|
|
| 632 |
"""
|
| 633 |
Modified:
|
| 634 |
- If `keyword` represents a Contract Address (ca:evm:/ca:sol:),
|
| 635 |
+
we format with brand style via build_midas_message_for_ca
|
| 636 |
+
and menambahkan MCAP (fetch sekali sebelum kirim).
|
| 637 |
"""
|
| 638 |
prev = last_posted.get(keyword)
|
| 639 |
now_ts = datetime.now().timestamp()
|
|
|
|
| 641 |
# Choose text based on whether this is a CA entity or not
|
| 642 |
if _is_ca_key(keyword):
|
| 643 |
ca_val = _ca_from_key(keyword)
|
| 644 |
+
mcap_val = await _fetch_initial_mcap(ca_val) # ambil MCAP sekali
|
| 645 |
+
text_to_send = build_midas_message_for_ca(ca_val, new_tier, mcap_value=mcap_val, body_snippet=None)
|
| 646 |
else:
|
| 647 |
text_to_send = format_body_with_spacing(body, new_tier)
|
| 648 |
|
|
|
|
| 932 |
core_u, sup_u = _unique_counts_by_role(topic_key)
|
| 933 |
if core_u < 1 and sup_u < SUPPORT_MIN_UNIQUE:
|
| 934 |
debug_log(
|
| 935 |
+
f"Support ditahan (core_u={core_u}, sup_u={SUPPORT_MIN_UNIQUE})",
|
| 936 |
orig_text,
|
| 937 |
)
|
| 938 |
return
|