|
import gradio as gr |
|
import openai |
|
import os |
|
import base64 |
|
from functools import lru_cache |
|
from PIL import Image |
|
import cv2 |
|
import numpy as np |
|
import datetime |
|
import uuid |
|
import requests |
|
from reportlab.lib.pagesizes import letter |
|
from reportlab.platypus import SimpleDocTemplate, Image as RLImage, Paragraph, Spacer |
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
from reportlab.lib.enums import TA_JUSTIFY |
|
from reportlab.lib import colors |
|
|
|
|
|
openai.api_key = os.getenv("OPENAI_API_KEY") |
|
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") |
|
REPO_OWNER = os.getenv("GITHUB_REPO_OWNER") |
|
REPO_NAME = os.getenv("GITHUB_REPO_NAME") |
|
|
|
|
|
ANALYSIS_MODEL = "gpt-4o" |
|
MAX_TOKENS = 4096 |
|
PDF_DIR = "reports" |
|
os.makedirs(PDF_DIR, exist_ok=True) |
|
|
|
|
|
PERSONAS = { |
|
"Aggressive Trader": { |
|
"description": "High-risk, short-term gains, leverages volatile market movements.", |
|
"prompt": "Focus on high-risk strategies, short-term gains, and leverage opportunities. Suggest aggressive entry and exit points.", |
|
"color": colors.red |
|
}, |
|
"Conservative Trader": { |
|
"description": "Low-risk, long-term investments, prioritizes capital preservation.", |
|
"prompt": "Focus on low-risk strategies, long-term investments, and capital preservation. Suggest safe entry points and strict stop-loss levels.", |
|
"color": colors.blue |
|
}, |
|
"Neutral Trader": { |
|
"description": "Balanced approach, combines short and long-term strategies.", |
|
"prompt": "Focus on balanced strategies, combining short and long-term opportunities. Suggest moderate risk levels and trend-following approaches.", |
|
"color": colors.green |
|
}, |
|
"Reactive Trader": { |
|
"description": "Quick decisions based on market news and social media trends.", |
|
"prompt": "Focus on quick decision-making, momentum trading, and reacting to market news. Suggest strategies based on current trends and FOMO opportunities.", |
|
"color": colors.orange |
|
}, |
|
"Systematic Trader": { |
|
"description": "Algorithm-based, rule-driven, and emotionless trading.", |
|
"prompt": "Focus on algorithmic strategies, backtested rules, and quantitative analysis. Suggest data-driven entry and exit points.", |
|
"color": colors.purple |
|
} |
|
} |
|
|
|
|
|
SYSTEM_PROMPT = """Professional Crypto Technical Analyst: |
|
1. Identify all technical patterns in the chart |
|
2. Determine key support/resistance levels |
|
3. Analyze volume and momentum indicators |
|
4. Calculate risk/reward ratios |
|
5. Provide clear trading recommendations |
|
6. Include specific price targets |
|
7. Assess market sentiment |
|
8. Evaluate trend strength |
|
9. Identify potential breakout/breakdown levels |
|
10. Provide time-based projections""" |
|
|
|
class ChartAnalyzer: |
|
def __init__(self): |
|
self.last_optimized_path = "" |
|
|
|
def validate_image(self, image_path: str) -> bool: |
|
try: |
|
with Image.open(image_path) as img: |
|
img.verify() |
|
|
|
img = cv2.imread(image_path) |
|
if img is None: |
|
return False |
|
|
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
|
edges = cv2.Canny(gray, 50, 150) |
|
return np.sum(edges) >= 1000 |
|
except Exception: |
|
return False |
|
|
|
def optimize_image(self, image_path: str) -> str: |
|
try: |
|
img = Image.open(image_path) |
|
original_width, original_height = img.size |
|
max_size = 1024 |
|
|
|
if original_width > max_size or original_height > max_size: |
|
ratio = min(max_size/original_width, max_size/original_height) |
|
new_size = (int(original_width * ratio), int(original_height * ratio)) |
|
img = img.resize(new_size, Image.LANCZOS) |
|
|
|
unique_id = uuid.uuid4().hex |
|
optimized_path = f"{PDF_DIR}/optimized_chart_{unique_id}.png" |
|
img.save(optimized_path, "PNG", optimize=True, quality=85) |
|
return optimized_path |
|
except Exception as e: |
|
print(f"Image optimization error: {str(e)}") |
|
return image_path |
|
|
|
def encode_image(self, image_path: str) -> str: |
|
if not os.path.exists(image_path): |
|
raise FileNotFoundError("File not found") |
|
|
|
if os.path.getsize(image_path) > 5 * 1024 * 1024: |
|
raise ValueError("Maximum file size is 5MB") |
|
|
|
with open(image_path, "rb") as image_file: |
|
return base64.b64encode(image_file.read()).decode('utf-8') |
|
|
|
@lru_cache(maxsize=100) |
|
def analyze_chart(self, image_path: str, persona: str) -> tuple: |
|
try: |
|
if not self.validate_image(image_path): |
|
return "Error: Invalid or low-quality image", None |
|
|
|
optimized_path = self.optimize_image(image_path) |
|
base64_image = self.encode_image(optimized_path) |
|
|
|
persona_prompt = PERSONAS.get(persona, {}).get("prompt", "") |
|
full_system_prompt = f"{SYSTEM_PROMPT}\n\n{persona_prompt}" |
|
|
|
response = openai.ChatCompletion.create( |
|
model=ANALYSIS_MODEL, |
|
messages=[ |
|
{"role": "system", "content": full_system_prompt}, |
|
{"role": "user", "content": [ |
|
{"type": "text", "text": "Perform detailed technical analysis of this chart:"}, |
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"} |
|
} |
|
]} |
|
], |
|
max_tokens=MAX_TOKENS |
|
) |
|
|
|
analysis_text = response.choices[0].message.content |
|
self.last_optimized_path = optimized_path |
|
return analysis_text, optimized_path |
|
|
|
except Exception as e: |
|
return f"Error: {str(e)}", None |
|
|
|
def create_pdf_styles(): |
|
styles = getSampleStyleSheet() |
|
styles.add(ParagraphStyle( |
|
'Justify', |
|
parent=styles['BodyText'], |
|
alignment=TA_JUSTIFY, |
|
spaceAfter=6 |
|
)) |
|
styles.add(ParagraphStyle( |
|
'PersonaTitle', |
|
fontSize=14, |
|
textColor=colors.white, |
|
backColor=colors.darkblue, |
|
alignment=1, |
|
spaceAfter=12 |
|
)) |
|
return styles |
|
|
|
def generate_pdf(optimized_image_path: str, analysis_text: str, persona: str) -> str: |
|
try: |
|
|
|
if not optimized_image_path: |
|
raise ValueError("Optimized image path is missing") |
|
|
|
if not os.path.exists(optimized_image_path): |
|
raise FileNotFoundError(f"Optimized image not found at {optimized_image_path}") |
|
|
|
if not analysis_text or not analysis_text.strip(): |
|
raise ValueError("Analysis text cannot be empty") |
|
|
|
|
|
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") |
|
filename = f"{PDF_DIR}/report_{timestamp}_{uuid.uuid4().hex[:6]}.pdf" |
|
|
|
|
|
doc = SimpleDocTemplate(filename, pagesize=letter) |
|
story = [] |
|
|
|
|
|
try: |
|
img = Image.open(optimized_image_path) |
|
img_width, img_height = img.size |
|
aspect = img_height / float(img_width) |
|
target_width = 400 |
|
target_height = target_width * aspect |
|
|
|
if target_height > 600: |
|
target_height = 600 |
|
target_width = target_height / aspect |
|
|
|
story.append(RLImage(optimized_image_path, width=target_width, height=target_height)) |
|
story.append(Spacer(1, 20)) |
|
except Exception as e: |
|
print(f"PDF image error: {str(e)}") |
|
raise |
|
|
|
|
|
if persona in PERSONAS: |
|
persona_color = PERSONAS[persona]["color"] |
|
story.append(Paragraph( |
|
f"Persona: {persona}", |
|
ParagraphStyle( |
|
'PersonaTitle', |
|
fontSize=14, |
|
textColor=colors.white, |
|
backColor=persona_color, |
|
alignment=1 |
|
) |
|
)) |
|
story.append(Spacer(1, 20)) |
|
|
|
|
|
styles = create_pdf_styles() |
|
analysis_style = styles['Justify'] |
|
|
|
cleaned_text = analysis_text.replace('•', '•') |
|
for line in cleaned_text.split('\n'): |
|
if line.strip(): |
|
p = Paragraph(line, analysis_style) |
|
story.append(p) |
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
doc.build(story) |
|
|
|
if not os.path.exists(filename): |
|
raise RuntimeError("PDF file creation failed") |
|
|
|
return filename |
|
|
|
except Exception as e: |
|
print(f"[PDF Generation Error] {str(e)}") |
|
return None |
|
|
|
def upload_to_github(file_path: str) -> str: |
|
try: |
|
if not file_path or file_path == "None": |
|
return "⛔ Error: PDF generation failed in previous step" |
|
|
|
if not os.path.exists(file_path): |
|
return f"⛔ File not found: {file_path}" |
|
|
|
|
|
file_name = os.path.basename(file_path) |
|
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{PDF_DIR}/{file_name}" |
|
|
|
with open(file_path, "rb") as f: |
|
content = base64.b64encode(f.read()).decode("utf-8") |
|
|
|
headers = { |
|
"Authorization": f"token {GITHUB_TOKEN}", |
|
"Accept": "application/vnd.github.v3+json" |
|
} |
|
|
|
|
|
response = requests.get(url, headers=headers) |
|
sha = response.json().get("sha") if response.status_code == 200 else None |
|
|
|
data = { |
|
"message": f"Add report {file_name}", |
|
"content": content, |
|
"branch": "main" |
|
} |
|
if sha: |
|
data["sha"] = sha |
|
|
|
response = requests.put(url, headers=headers, json=data) |
|
if response.status_code in [200, 201]: |
|
return f"✅ Report successfully uploaded to GitHub!\nURL: {response.json()['content']['html_url']}" |
|
return f"❌ GitHub upload failed ({response.status_code}): {response.text}" |
|
|
|
except Exception as e: |
|
return f"⚠️ Upload error: {str(e)}" |
|
|
|
with gr.Blocks(theme=gr.themes.Soft()) as demo: |
|
analyzer = ChartAnalyzer() |
|
|
|
with gr.Column(): |
|
gr.Markdown("# 🚀 CryptoVision Pro") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
chart_input = gr.Image(type="filepath", label="Upload Chart", sources=["upload"]) |
|
persona_dropdown = gr.Dropdown( |
|
list(PERSONAS.keys()), |
|
label="Select Trading Persona", |
|
value="Neutral Trader", |
|
info="Choose your trading style" |
|
) |
|
analyze_btn = gr.Button("Analyze Chart", variant="primary") |
|
|
|
with gr.Column(): |
|
analysis_output = gr.Markdown("## Analysis Results\n*Your analysis will appear here*") |
|
pdf_status = gr.HTML() |
|
|
|
|
|
optimized_image_path = gr.Text(visible=False) |
|
pdf_file = gr.Text(visible=False) |
|
|
|
|
|
analyze_btn.click( |
|
lambda: [ |
|
gr.Markdown(visible=False), |
|
gr.HTML(value="<div class='loading-spinner'></div>"), |
|
None |
|
], |
|
outputs=[analysis_output, pdf_status, pdf_file], |
|
queue=False |
|
).then( |
|
analyzer.analyze_chart, |
|
inputs=[chart_input, persona_dropdown], |
|
outputs=[analysis_output, optimized_image_path] |
|
).then( |
|
generate_pdf, |
|
inputs=[optimized_image_path, analysis_output, persona_dropdown], |
|
outputs=pdf_file |
|
).then( |
|
upload_to_github, |
|
inputs=pdf_file, |
|
outputs=pdf_status |
|
) |
|
|
|
if __name__ == "__main__": |
|
demo.launch() |