|
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" |
|
|
|
|
|
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_analysis = "" |
|
os.makedirs(PDF_DIR, exist_ok=True) |
|
|
|
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) -> str: |
|
try: |
|
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_analysis = analysis_text |
|
return analysis_text |
|
|
|
except Exception as e: |
|
return f"Error: {str(e)}" |
|
|
|
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(image_path: str, analysis_text: str, persona: str) -> str: |
|
styles = create_pdf_styles() |
|
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(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(image_path, width=target_width, height=target_height)) |
|
story.append(Spacer(1, 20)) |
|
except Exception as e: |
|
print(f"PDF image error: {str(e)}") |
|
|
|
|
|
persona_color = PERSONAS.get(persona, {}).get("color", colors.black) |
|
story.append(Paragraph(f"Persona: {persona}", ParagraphStyle( |
|
'PersonaTitle', |
|
fontSize=14, |
|
textColor=colors.white, |
|
backColor=persona_color, |
|
alignment=1 |
|
))) |
|
story.append(Spacer(1, 20)) |
|
|
|
|
|
analysis_style = styles['Justify'] |
|
for line in analysis_text.split('\n'): |
|
if line.strip(): |
|
p = Paragraph(line.replace('•', '•'), analysis_style) |
|
story.append(p) |
|
story.append(Spacer(1, 12)) |
|
|
|
doc.build(story) |
|
return filename |
|
|
|
def upload_to_github(file_path: str) -> bool: |
|
try: |
|
with open(file_path, "rb") as f: |
|
content = f.read() |
|
|
|
file_name = os.path.basename(file_path) |
|
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{PDF_DIR}/{file_name}" |
|
|
|
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": base64.b64encode(content).decode("utf-8"), |
|
"branch": "main" |
|
} |
|
|
|
if sha: |
|
data["sha"] = sha |
|
|
|
response = requests.put(url, headers=headers, json=data) |
|
return response.status_code in [200, 201] |
|
except Exception as e: |
|
print(f"GitHub upload error: {str(e)}") |
|
return False |
|
|
|
|
|
custom_css = """ |
|
:root { --primary-color: #2563eb; --secondary-color: #1e40af; } |
|
.container { max-width: 1200px; margin: 0 auto; } |
|
.analysis-box { background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } |
|
.loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); } |
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
|
""" |
|
|
|
with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo: |
|
analyzer = ChartAnalyzer() |
|
|
|
with gr.Column(elem_classes=["container"]): |
|
gr.Markdown("""<div style="text-align: center;"><h1>🚀 CryptoVision Pro</h1></div>""") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
with gr.Box(elem_classes=["analysis-box"]): |
|
chart_input = gr.Image(type="filepath", label="Chart", sources=["upload"]) |
|
persona_dropdown = gr.Dropdown(list(PERSONAS.keys()), label="Trading Persona", value="Neutral Trader") |
|
analyze_btn = gr.Button("Analyze", variant="primary") |
|
|
|
with gr.Column(): |
|
with gr.Box(elem_classes=["analysis-box"]): |
|
analysis_output = gr.Markdown("Analysis will appear here...") |
|
pdf_status = gr.HTML() |
|
|
|
analyze_btn.click( |
|
lambda: (gr.update(visible=False), gr.update(value="<div class='loading-spinner'></div>")), |
|
outputs=[analysis_output, pdf_status], |
|
queue=False |
|
).then( |
|
analyzer.analyze_chart, |
|
[chart_input, persona_dropdown], |
|
analysis_output |
|
).then( |
|
lambda img, text, persona: generate_pdf(img, text, persona), |
|
[chart_input, analysis_output, persona_dropdown], |
|
None |
|
).then( |
|
lambda path: upload_to_github(path) if all([GITHUB_TOKEN, REPO_OWNER, REPO_NAME]) else None, |
|
None, |
|
pdf_status |
|
) |
|
|
|
if __name__ == "__main__": |
|
demo.launch() |