# -*- coding: utf-8 -*- import os import sys import yaml import argparse import requests import pymysql.cursors import random import re import datetime from openai import OpenAI from dotenv import load_dotenv from PIL import Image, ImageDraw, ImageFont # ============================================================================== # 1. CONFIGURATION & ENVIRONNEMENT # ============================================================================== def log(msg): print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) load_dotenv(os.path.join(SCRIPT_DIR, '.env')) def get_env(key, default=None): return os.getenv(key, default) parser = argparse.ArgumentParser() parser.add_argument('--wp-path', type=str, required=True, help='Root path of WP') parser.add_argument('--action_type', type=str, default='feed_morning', help='Action to perform') parser.add_argument('--status', type=str, default='publish', choices=['publish', 'draft']) args, unknown = parser.parse_known_args() log(f"🚀 Booting Master Bot (Action: {args.action_type} | Status: {args.status})...") # --- LECTURE DIRECTE DEPUIS .ENV --- BOT_SECRET = get_env('BOT_SECRET_KEY') BOT_AUTHOR_ID = int(get_env('BOT_AUTHOR_ID', 1)) if not BOT_SECRET: log("CRITICAL FAILURE: BOT_SECRET_KEY missing in .env") sys.exit(1) SITE_DOMAIN = get_env('WP_SITE_DOMAIN', 'https://gapandhold.com').rstrip('/') LLM_MODEL = get_env('LLM_MODEL', 'gpt-4o') WP_REST_FEED = f"{SITE_DOMAIN}/wp-json/etea/v1/bot-activity/" WP_REST_INTERACT = f"{SITE_DOMAIN}/wp-json/etea/v1/bot-interact/" openai_api_key = get_env('LLM_API_KEY') if not openai_api_key: log("CRITICAL FAILURE: openai_api_key missing in .env") sys.exit(1) client = OpenAI( api_key=openai_api_key, base_url=get_env('LLM_BASE_URL') ) # ============================================================================== # 2. AUTO-DÉTECTION DB SÉCURISÉE (pymysql) # ============================================================================== def get_wp_config_creds(wp_path): creds = {} current = wp_path while len(current) > 4: cfg = os.path.join(current, 'wp-config.php') if os.path.exists(cfg): with open(cfg, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() for k in ['DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST']: m = re.search(rf"define\(\s*['\"]{k}['\"]\s*,\s*['\"](.*?)['\"]\s*\);", content) if m: creds[k] = m.group(1) m_pref = re.search(r"\$table_prefix\s*=\s*['\"](.*?)['\"];", content) creds['table_prefix'] = m_pref.group(1) if m_pref else 'wp_' return creds current = os.path.dirname(current) return None wp_creds = get_wp_config_creds(args.wp_path) if not wp_creds: log("CRITICAL FAILURE: wp-config.php not found.") sys.exit(1) DB_PREFIX = wp_creds.get('table_prefix', 'wp_') def get_db_connection(): try: return pymysql.connect( host=wp_creds.get('DB_HOST'), user=wp_creds.get('DB_USER'), password=wp_creds.get('DB_PASSWORD'), database=wp_creds.get('DB_NAME'), cursorclass=pymysql.cursors.DictCursor ) except Exception as e: log(f"CRITICAL FAILURE: DB Connection failed: {e}") sys.exit(1) db = get_db_connection() try: with db.cursor() as cursor: # ============================================================================== # 3. ROUTAGE DES ACTIONS # ============================================================================== # --------------------------------------------------------- # ACTION A : VOTER SUR UN TICKER # --------------------------------------------------------- if args.action_type == 'vote': log("Executing Vote logic...") cursor.execute(f""" SELECT ticker, direction_anticipee FROM {DB_PREFIX}etea_earnings_analysis WHERE earnings_date >= CURDATE() AND direction_anticipee != 'Neutral' ORDER BY RAND() LIMIT 1 """) s = cursor.fetchone() if not s: log("No signal available for voting.") sys.exit(0) vote_val = 'agree' if random.random() > 0.1 else 'disagree' url = f"{WP_REST_INTERACT}?bot_key={BOT_SECRET}&action_type=vote&author_id={BOT_AUTHOR_ID}&target={s['ticker']}&vote_value={vote_val}" try: res = requests.post(url, verify=False) log(f"Voted '{vote_val}' on ${s['ticker']}. WP Response: {res.text}") except Exception as e: log(f"Vote Failed: {e}") sys.exit(0) # --------------------------------------------------------- # ACTION B : COMMENTER SUR LA PAGE D'UN TICKER # --------------------------------------------------------- elif args.action_type == 'ticker_comment': log("Executing Ticker Comment logic...") cursor.execute(f""" SELECT ticker, direction_anticipee, confidence_score, anticipated_post_gap_strategy FROM {DB_PREFIX}etea_earnings_analysis WHERE earnings_date >= CURDATE() AND direction_anticipee != 'Neutral' ORDER BY RAND() LIMIT 1 """) s = cursor.fetchone() if not s: log("No signal available to comment on.") sys.exit(0) prompt = f"Write a professional 2-sentence quantitative trader comment about ${s['ticker']}. The AI model indicates a {s['direction_anticipee']} bias (Confidence: {float(s['confidence_score'])*100:.1f}%). Strategy to execute: {s['anticipated_post_gap_strategy']}. Do NOT use markdown (*). Use plain text." try: comp = client.chat.completions.create(model=LLM_MODEL, messages=[{"role":"user", "content": prompt}], temperature=0.7) comment_text = re.sub(r'.*?', '', comp.choices[0].message.content, flags=re.DOTALL).strip() comment_text = comment_text.replace('**', '') url = f"{WP_REST_INTERACT}?bot_key={BOT_SECRET}&action_type=ticker_comment&author_id={BOT_AUTHOR_ID}&target={s['ticker']}" res = requests.post(url, json={'content': comment_text}, verify=False) log(f"Commented on ${s['ticker']}. WP Response: {res.text}") except Exception as e: log(f"Comment Failed: {e}") sys.exit(0) # --------------------------------------------------------- # ACTION C : CRÉER UN POST DANS LE GLOBAL ACTIVITY FEED # --------------------------------------------------------- else: log(f"Executing Global Feed Post logic (Type: {args.action_type})...") is_evening = 'evening' in args.action_type query = f""" SELECT T1.ticker, T1.security, T1.direction_anticipee, T1.confidence_score, T1.earnings_date, T1.earnings_timing, T1.actual_gap_percent, T1.gap_anticipation_correct, T2.logo_url, T2.security as full_name FROM {DB_PREFIX}etea_earnings_analysis T1 LEFT JOIN {DB_PREFIX}etea_tickers_list T2 ON T1.ticker = T2.ticker """ if is_evening: query += " WHERE T1.earnings_date = CURDATE() AND T1.actual_gap_percent IS NOT NULL ORDER BY ABS(T1.actual_gap_percent) DESC LIMIT 3" else: query += " WHERE T1.earnings_date >= CURDATE() AND T1.direction_anticipee IN ('Bullish', 'Bearish') ORDER BY T1.earnings_date ASC, T1.confidence_score DESC LIMIT 3" cursor.execute(query) signals = cursor.fetchall() if not signals: log("⚠️ No signals found for this time period. Aborting post.") sys.exit(0) # 1. PRÉPARATION DU TEXTE ctx_lines = [] for s in signals: if is_evening: res = "WIN" if s['gap_anticipation_correct'] else "LOSS" ctx_lines.append(f"- ${s['ticker']}: Actual Gap {s['actual_gap_percent']}% ({res})") else: ctx_lines.append(f"- ${s['ticker']}: {s['direction_anticipee']} (Conf: {float(s['confidence_score'])*100:.1f}%)") context_text = "\n".join(ctx_lines) prompts = { 'feed_morning': f"Write a brief, punchy morning pre-market update for a trading community feed. Introduce the 'Gap & Hold' strategy context. Highlight these upcoming AI earnings signals using an HTML list (