Spaces:
Sleeping
Sleeping
| import sqlite3 | |
| from services.database import get_connection | |
| import datetime | |
| def init_testimonials_table(): | |
| """Crée la table des témoignages""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS testimonials ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_email TEXT NOT NULL, | |
| user_name TEXT NOT NULL, | |
| content TEXT NOT NULL, | |
| rating INTEGER DEFAULT 5, | |
| investment REAL, | |
| gain REAL, | |
| period TEXT, | |
| status TEXT DEFAULT 'pending', | |
| created_at DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| approved_at DATETIME, | |
| FOREIGN KEY (user_email) REFERENCES users(email) | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| print("✅ Table testimonials initialisée") | |
| def add_testimonial(user_email, user_name, content, rating=5, investment=None, gain=None, period=None): | |
| """Ajoute un témoignage en attente de validation""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| print(f"📝 Ajout témoignage pour {user_email} avec statut 'pending'") | |
| cursor.execute(""" | |
| INSERT INTO testimonials | |
| (user_email, user_name, content, rating, investment, gain, period, status) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') | |
| """, (user_email, user_name, content, rating, investment, gain, period)) | |
| conn.commit() | |
| # Vérification immédiate | |
| cursor.execute("SELECT id, status FROM testimonials WHERE user_email = ? ORDER BY created_at DESC LIMIT 1", (user_email,)) | |
| result = cursor.fetchone() | |
| print(f"✅ Témoignage ajouté: ID {result[0]}, statut: {result[1]}") | |
| return True | |
| except Exception as e: | |
| print(f"❌ Erreur ajout témoignage: {e}") | |
| return False | |
| finally: | |
| conn.close() | |
| def get_approved_testimonials(limit=10): | |
| """Récupère les témoignages approuvés""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| SELECT user_name, content, rating, investment, gain, period, created_at | |
| FROM testimonials | |
| WHERE status = 'approved' | |
| ORDER BY created_at DESC | |
| LIMIT ? | |
| """, (limit,)) | |
| results = cursor.fetchall() | |
| return results | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_approved_testimonials: {e}") | |
| return [] | |
| finally: | |
| conn.close() | |
| def get_pending_testimonials(): | |
| """Récupère les témoignages en attente (pour admin)""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| print("🔍 Recherche des témoignages en attente...") | |
| cursor.execute(""" | |
| SELECT id, user_email, user_name, content, rating, investment, gain, period, created_at | |
| FROM testimonials | |
| WHERE status = 'pending' | |
| ORDER BY created_at DESC | |
| """) | |
| results = cursor.fetchall() | |
| print(f"📊 {len(results)} témoignage(s) en attente trouvé(s)") | |
| # Affiche les IDs pour debug | |
| for r in results: | |
| print(f" - ID: {r[0]}, Utilisateur: {r[2]}, Statut: en attente") | |
| return results | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_pending_testimonials: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return [] | |
| finally: | |
| conn.close() | |
| def approve_testimonial(testimonial_id): | |
| """Approuve un témoignage""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| UPDATE testimonials | |
| SET status = 'approved', approved_at = CURRENT_TIMESTAMP | |
| WHERE id = ? | |
| """, (testimonial_id,)) | |
| conn.commit() | |
| return True | |
| except Exception as e: | |
| print(f"❌ Erreur approve_testimonial: {e}") | |
| return False | |
| finally: | |
| conn.close() | |
| def reject_testimonial(testimonial_id): | |
| """Rejette un témoignage""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| UPDATE testimonials | |
| SET status = 'rejected' | |
| WHERE id = ? | |
| """, (testimonial_id,)) | |
| conn.commit() | |
| return True | |
| except Exception as e: | |
| print(f"❌ Erreur reject_testimonial: {e}") | |
| return False | |
| finally: | |
| conn.close() | |
| def get_user_testimonial(user_email): | |
| """Vérifie si l'utilisateur a déjà un témoignage""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| SELECT id, status FROM testimonials | |
| WHERE user_email = ? | |
| ORDER BY created_at DESC | |
| LIMIT 1 | |
| """, (user_email,)) | |
| result = cursor.fetchone() | |
| return result | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_user_testimonial: {e}") | |
| return None | |
| finally: | |
| conn.close() | |
| def get_global_stats(): | |
| """Statistiques globales pour la page témoignages""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| # Nombre d'utilisateurs non-admin | |
| cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = 0") | |
| user_count = cursor.fetchone()[0] or 0 | |
| # Stats globales des trades | |
| cursor.execute(""" | |
| SELECT | |
| COUNT(*) as total_trades, | |
| SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed_trades, | |
| SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) as winning_trades, | |
| COALESCE(SUM(pnl), 0) as total_pnl, | |
| COALESCE(AVG(pnl_percentage), 0) as avg_return | |
| FROM user_trades | |
| """) | |
| trade_stats = cursor.fetchone() | |
| closed = trade_stats[1] or 0 | |
| winning = trade_stats[2] or 0 | |
| win_rate = round((winning / closed * 100) if closed > 0 else 0, 1) | |
| # Note moyenne des témoignages approuvés | |
| cursor.execute("SELECT COALESCE(AVG(rating), 4.9) FROM testimonials WHERE status = 'approved'") | |
| avg_rating = round(cursor.fetchone()[0] or 4.9, 1) | |
| # Nombre de témoignages approuvés | |
| cursor.execute("SELECT COUNT(*) FROM testimonials WHERE status = 'approved'") | |
| testimonials_count = cursor.fetchone()[0] or 0 | |
| return { | |
| 'user_count': user_count, | |
| 'total_trades': trade_stats[0] or 0, | |
| 'closed_trades': closed, | |
| 'winning_trades': winning, | |
| 'total_pnl': trade_stats[3] or 0, | |
| 'avg_return': trade_stats[4] or 0, | |
| 'win_rate': win_rate, | |
| 'avg_rating': avg_rating, | |
| 'testimonials_count': testimonials_count, | |
| } | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_global_stats: {e}") | |
| return { | |
| 'user_count': 0, 'total_trades': 0, 'closed_trades': 0, | |
| 'winning_trades': 0, 'total_pnl': 0, 'avg_return': 0, | |
| 'win_rate': 0, 'avg_rating': 4.9, 'testimonials_count': 0, | |
| } | |
| finally: | |
| conn.close() | |
| def get_rich_testimonials(limit=10): | |
| """Témoignages approuvés avec vraies stats utilisateurs (trades, gains, win rate)""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| SELECT t.user_email, t.user_name, t.content, t.rating, | |
| t.investment, t.gain, t.period, t.created_at, | |
| u.prenom, u.nom, u.created_at AS member_since, | |
| COALESCE(u.public_stats, 0) AS public_stats, | |
| COALESCE(u.is_online, 0) AS is_online | |
| FROM testimonials t | |
| LEFT JOIN users u ON u.email = t.user_email | |
| WHERE t.status = 'approved' | |
| ORDER BY t.created_at DESC | |
| LIMIT ? | |
| """, (limit,)) | |
| rows = cursor.fetchall() | |
| result = [] | |
| for row in rows: | |
| email, user_name, content, rating, investment, gain, period, created_at, prenom, nom, member_since, public_stats, is_online = row | |
| # Stats réelles uniquement si l'utilisateur a activé la visibilité publique | |
| trades, wins, real_pnl, win_rate = 0, 0, 0.0, 0.0 | |
| if public_stats: | |
| cursor.execute(""" | |
| SELECT COUNT(*), | |
| COALESCE(SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END), 0), | |
| COALESCE(SUM(pnl), 0) | |
| FROM user_trades | |
| WHERE user_email = ? AND status = 'closed' | |
| """, (email,)) | |
| ts = cursor.fetchone() | |
| trades = ts[0] or 0 | |
| wins = ts[1] or 0 | |
| real_pnl = ts[2] or 0.0 | |
| win_rate = round((wins / trades * 100) if trades > 0 else 0.0, 1) | |
| recent_trades = [] | |
| if public_stats: | |
| cursor.execute(""" | |
| SELECT symbol, prediction_direction, quantity, pnl, pnl_percentage, entry_date | |
| FROM user_trades | |
| WHERE user_email = ? AND status = 'closed' | |
| ORDER BY entry_date DESC | |
| LIMIT 3 | |
| """, (email,)) | |
| recent_trades = [ | |
| { | |
| 'symbol': r[0], | |
| 'signal': r[1], | |
| 'amount': r[2] or 0, | |
| 'pnl': r[3] or 0, | |
| 'pnl_pct': r[4] or 0, | |
| 'date': str(r[5])[:10] if r[5] else '', | |
| } | |
| for r in cursor.fetchall() | |
| ] | |
| # Nom affiché : prénom + nom > user_name > email prefix | |
| full = " ".join(filter(None, [prenom, nom])).strip() | |
| display_name = full or user_name or email.split('@')[0].capitalize() | |
| parts = display_name.split() | |
| initials = (parts[0][0] + (parts[-1][0] if len(parts) > 1 else parts[0][-1])).upper() | |
| # Gain affiché : PnL réel si public (même si 0), sinon champ du formulaire | |
| if public_stats: | |
| display_gain = real_pnl | |
| else: | |
| display_gain = float(gain) if gain else 0.0 | |
| # Ancienneté en mois (0 = moins d'un mois) | |
| member_months = 0 | |
| if member_since: | |
| try: | |
| from datetime import datetime as _dt | |
| since = _dt.fromisoformat(str(member_since).split('.')[0]) | |
| member_months = max(0, (_dt.now() - since).days // 30) | |
| except Exception: | |
| pass | |
| result.append({ | |
| 'email': email, | |
| 'name': display_name, | |
| 'initials': initials, | |
| 'content': content or '', | |
| 'rating': int(rating or 5), | |
| 'gain': display_gain, | |
| 'trades': trades if public_stats else None, | |
| 'win_rate': win_rate if public_stats else None, | |
| 'period': period or '', | |
| 'member_months': member_months, | |
| 'public_stats': bool(public_stats), | |
| 'is_online': bool(is_online), | |
| }) | |
| return result | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_rich_testimonials: {e}") | |
| return [] | |
| finally: | |
| conn.close() | |
| def get_user_stats(user_email): | |
| """Calcule les statistiques d'un utilisateur""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| # Trades fermés | |
| cursor.execute(""" | |
| SELECT | |
| COUNT(*), | |
| COALESCE(SUM(pnl), 0), | |
| COALESCE(AVG(pnl_percentage), 0), | |
| COALESCE(SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END), 0) as wins, | |
| COALESCE(SUM(CASE WHEN pnl < 0 THEN 1 ELSE 0 END), 0) as losses | |
| FROM user_trades | |
| WHERE user_email = ? AND status = 'closed' | |
| """, (user_email,)) | |
| stats = cursor.fetchone() | |
| # Trade en cours | |
| cursor.execute(""" | |
| SELECT COUNT(*) FROM user_trades | |
| WHERE user_email = ? AND status = 'open' | |
| """, (user_email,)) | |
| open_trades = cursor.fetchone()[0] | |
| total_trades = stats[0] if stats[0] else 0 | |
| total_pnl = stats[1] if stats[1] else 0 | |
| avg_pnl = stats[2] if stats[2] else 0 | |
| wins = stats[3] if stats[3] else 0 | |
| losses = stats[4] if stats[4] else 0 | |
| win_rate = (wins / total_trades * 100) if total_trades > 0 else 0 | |
| return { | |
| 'total_trades': total_trades, | |
| 'total_pnl': total_pnl, | |
| 'avg_pnl': avg_pnl, | |
| 'wins': wins, | |
| 'losses': losses, | |
| 'win_rate': win_rate, | |
| 'open_trades': open_trades | |
| } | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_user_stats: {e}") | |
| return { | |
| 'total_trades': 0, | |
| 'total_pnl': 0, | |
| 'avg_pnl': 0, | |
| 'wins': 0, | |
| 'losses': 0, | |
| 'win_rate': 0, | |
| 'open_trades': 0 | |
| } | |
| finally: | |
| conn.close() | |
| def get_user_trades(user_email, limit=50): | |
| """Récupère les trades d'un utilisateur""" | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute(""" | |
| SELECT * FROM user_trades | |
| WHERE user_email = ? | |
| ORDER BY entry_date DESC | |
| LIMIT ? | |
| """, (user_email, limit)) | |
| return cursor.fetchall() | |
| except Exception as e: | |
| print(f"⚠️ Erreur get_user_trades: {e}") | |
| return [] | |
| finally: | |
| conn.close() | |
| def save_followed_trade(user_email, symbol, signal, entry_price, current_price, amount, action='ACHAT', model_type='sentiment'): | |
| """Sauvegarde un trade que l'utilisateur confirme avoir suivi. | |
| action : 'ACHAT' (position longue) ou 'VENTE' (position courte/vente à découvert) | |
| model_type : 'sentiment' | 'lstm' | |
| """ | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| try: | |
| sig_to_dir = {'HAUSSIER': 'up', 'BAISSIER': 'down', 'NEUTRE': 'neutral'} | |
| pred_dir = sig_to_dir.get(signal, 'neutral') | |
| if action == 'ACHAT': | |
| pnl = (current_price - entry_price) / entry_price * amount | |
| actual_dir = 'up' | |
| elif action == 'VENTE': | |
| pnl = (entry_price - current_price) / entry_price * amount | |
| actual_dir = 'down' | |
| else: | |
| pnl = 0.0 | |
| actual_dir = 'neutral' | |
| pnl_pct = (pnl / amount * 100) if amount > 0 else 0.0 | |
| model_type = model_type or 'sentiment' | |
| cursor.execute(""" | |
| INSERT INTO user_trades | |
| (user_email, symbol, entry_price, exit_price, quantity, prediction_direction, actual_direction, pnl, pnl_percentage, status, model_type) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'closed', ?) | |
| """, (user_email, symbol, round(entry_price, 2), round(current_price, 2), | |
| round(amount, 2), pred_dir, actual_dir, round(pnl, 2), round(pnl_pct, 2), model_type)) | |
| conn.commit() | |
| print(f"✅ Trade sauvegardé: {symbol} | {signal} | PnL: {pnl:+.2f}€") | |
| return True | |
| except Exception as e: | |
| print(f"❌ Erreur save_followed_trade: {e}") | |
| return False | |
| finally: | |
| conn.close() | |