aidevhund commited on
Commit
cc6796a
·
verified ·
1 Parent(s): 9f244a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -213
app.py CHANGED
@@ -6,43 +6,62 @@ from functools import lru_cache
6
  from PIL import Image
7
  import cv2
8
  import numpy as np
 
 
 
 
 
 
 
 
9
 
10
- # OpenAI API Configuration
11
  openai.api_key = os.getenv("OPENAI_API_KEY")
 
 
 
 
 
12
  ANALYSIS_MODEL = "gpt-4o"
13
  MAX_TOKENS = 4096
 
14
 
15
- # Persona Definitions
16
  PERSONAS = {
17
  "Aggressive Trader": {
18
  "description": "High-risk, short-term gains, leverages volatile market movements.",
19
- "prompt": "Focus on high-risk strategies, short-term gains, and leverage opportunities. Suggest aggressive entry and exit points."
 
20
  },
21
  "Conservative Trader": {
22
  "description": "Low-risk, long-term investments, prioritizes capital preservation.",
23
- "prompt": "Focus on low-risk strategies, long-term investments, and capital preservation. Suggest safe entry points and strict stop-loss levels."
 
24
  },
25
  "Neutral Trader": {
26
  "description": "Balanced approach, combines short and long-term strategies.",
27
- "prompt": "Focus on balanced strategies, combining short and long-term opportunities. Suggest moderate risk levels and trend-following approaches."
 
28
  },
29
  "Reactive Trader": {
30
  "description": "Quick decisions based on market news and social media trends.",
31
- "prompt": "Focus on quick decision-making, momentum trading, and reacting to market news. Suggest strategies based on current trends and FOMO opportunities."
 
32
  },
33
  "Systematic Trader": {
34
  "description": "Algorithm-based, rule-driven, and emotionless trading.",
35
- "prompt": "Focus on algorithmic strategies, backtested rules, and quantitative analysis. Suggest data-driven entry and exit points."
 
36
  }
37
  }
38
 
39
- # System Prompt Configuration
40
  SYSTEM_PROMPT = """Professional Crypto Technical Analyst:
41
  1. Identify all technical patterns in the chart
42
  2. Determine key support/resistance levels
43
  3. Analyze volume and momentum indicators
44
  4. Calculate risk/reward ratios
45
- 5. Provide clear trading recommendations based on the selected persona
46
  6. Include specific price targets
47
  7. Assess market sentiment
48
  8. Evaluate trend strength
@@ -52,276 +71,221 @@ SYSTEM_PROMPT = """Professional Crypto Technical Analyst:
52
  class ChartAnalyzer:
53
  def __init__(self):
54
  self.last_analysis = ""
 
55
 
56
  def validate_image(self, image_path: str) -> bool:
57
- """Validate if the uploaded file is a valid image and looks like a chart"""
58
  try:
59
- # Check if the file is an image
60
  with Image.open(image_path) as img:
61
- img.verify() # Verify that it is a valid image
62
 
63
- # Check if the image looks like a chart (basic check)
64
  img = cv2.imread(image_path)
65
  if img is None:
66
  return False
67
 
68
- # Simple check for chart-like features (e.g., axes, grid lines)
69
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
70
  edges = cv2.Canny(gray, 50, 150)
71
- if np.sum(edges) < 1000: # Arbitrary threshold for edge detection
72
- return False
73
-
74
- return True
75
  except Exception:
76
  return False
77
 
78
  def optimize_image(self, image_path: str) -> str:
79
- """Optimize image size and quality"""
80
  try:
81
  img = Image.open(image_path)
82
- # Resize if the image is too large
83
- if img.size[0] > 1024 or img.size[1] > 1024:
84
- img = img.resize((1024, 1024), Image.ANTIALIAS)
85
- # Save optimized image
86
- optimized_path = "optimized_chart.png"
 
 
 
 
 
87
  img.save(optimized_path, "PNG", optimize=True, quality=85)
88
  return optimized_path
89
  except Exception as e:
90
  print(f"Image optimization error: {str(e)}")
91
- return image_path # Fallback to original image
92
 
93
  def encode_image(self, image_path: str) -> str:
94
- """Encode image to base64 with validation"""
95
  if not os.path.exists(image_path):
96
  raise FileNotFoundError("File not found")
97
- if not image_path.lower().endswith(('.png', '.jpg', '.jpeg')):
98
- raise ValueError("Unsupported file format")
99
  if os.path.getsize(image_path) > 5 * 1024 * 1024:
100
  raise ValueError("Maximum file size is 5MB")
101
 
102
  with open(image_path, "rb") as image_file:
103
  return base64.b64encode(image_file.read()).decode('utf-8')
104
 
105
- @lru_cache(maxsize=100) # Cache up to 100 recent analyses
106
  def analyze_chart(self, image_path: str, persona: str) -> str:
107
- """Core analysis function with caching and persona-based recommendations"""
108
  try:
109
- # Optimize image before analysis
110
  optimized_path = self.optimize_image(image_path)
111
  base64_image = self.encode_image(optimized_path)
112
 
113
- # Add persona-specific instructions to the system prompt
114
  persona_prompt = PERSONAS.get(persona, {}).get("prompt", "")
115
  full_system_prompt = f"{SYSTEM_PROMPT}\n\n{persona_prompt}"
116
 
117
  response = openai.ChatCompletion.create(
118
  model=ANALYSIS_MODEL,
119
  messages=[
120
- {
121
- "role": "system",
122
- "content": full_system_prompt
123
- },
124
- {
125
- "role": "user",
126
- "content": [
127
- {"type": "text", "text": "Perform detailed technical analysis of this chart:"},
128
- {
129
- "type": "image_url",
130
- "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
131
- }
132
- ]
133
- }
134
  ],
135
  max_tokens=MAX_TOKENS
136
  )
137
 
138
- return response.choices[0].message.content
 
 
139
 
140
- except openai.error.APIError as e:
141
- return f"OpenAI API Error: {str(e)}"
142
  except Exception as e:
143
- return f"Analysis Error: {str(e)}"
144
 
145
- # Custom CSS for optimized layout
146
- custom_css = """
147
- :root {
148
- --primary-color: #2563eb;
149
- --secondary-color: #1e40af;
150
- --background-color: #f8fafc;
151
- --text-color: #1e293b;
152
- --border-color: #e2e8f0;
153
- }
154
-
155
- body {
156
- background-color: var(--background-color);
157
- color: var(--text-color);
158
- }
 
 
 
159
 
160
- .container {
161
- max-width: 1200px;
162
- margin: 0 auto;
163
- padding: 20px;
164
- }
165
-
166
- .analysis-box {
167
- background-color: white;
168
- border-radius: 12px;
169
- padding: 24px;
170
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
171
- border: 1px solid var(--border-color);
172
- height: 400px; /* Initial height */
173
- overflow-y: auto;
174
- width: 100%;
175
- transition: height 0.3s ease;
176
- }
177
-
178
- .upload-box {
179
- background-color: white;
180
- border-radius: 12px;
181
- padding: 24px;
182
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
183
- border: 1px solid var(--border-color);
184
- width: 100%;
185
- }
186
-
187
- .primary-button {
188
- background-color: var(--primary-color) !important;
189
- color: white !important;
190
- border-radius: 8px !important;
191
- padding: 12px 24px !important;
192
- font-weight: 500 !important;
193
- width: 100%;
194
- }
195
-
196
- .primary-button:hover {
197
- background-color: var(--secondary-color) !important;
198
- }
199
-
200
- .markdown-container {
201
- max-width: 100%;
202
- margin: 0 auto;
203
- padding: 0 15px;
204
- word-wrap: break-word;
205
- }
206
-
207
- .loading-container {
208
- display: flex;
209
- justify-content: center;
210
- align-items: center;
211
- height: 100%;
212
- }
213
-
214
- .loading-text {
215
- color: var(--primary-color);
216
- font-size: 1.2rem;
217
- text-align: center;
218
- margin-top: 2rem;
219
- }
220
 
221
- .loading-spinner {
222
- border: 4px solid #f3f3f3;
223
- border-top: 4px solid var(--primary-color);
224
- border-radius: 50%;
225
- width: 40px;
226
- height: 40px;
227
- animation: spin 1s linear infinite;
228
- }
 
 
 
 
 
 
 
 
229
 
230
- @keyframes spin {
231
- 0% { transform: rotate(0deg); }
232
- 100% { transform: rotate(360deg); }
233
- }
 
 
 
 
 
 
234
 
235
- .row {
236
- display: flex;
237
- flex-wrap: wrap;
238
- gap: 20px;
239
- }
 
 
240
 
241
- .column {
242
- flex: 1;
243
- min-width: 300px;
244
- }
245
 
246
- @media (max-width: 768px) {
247
- .row {
248
- flex-direction: column;
249
- }
250
-
251
- .column {
252
- width: 100%;
253
- }
254
- }
255
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- # Loading HTML template
258
- loading_html = """
259
- <div class="loading-container">
260
- <div class="loading-spinner"></div>
261
- <div class="loading-text">Generating report...</div>
262
- </div>
 
263
  """
264
 
265
- # Gradio Interface
266
- analyzer = ChartAnalyzer()
267
-
268
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
269
- # Header Section
 
270
  with gr.Column(elem_classes=["container"]):
271
- gr.Markdown("""
272
- <div style="text-align: center; margin-bottom: 32px;">
273
- <h1 style="color: var(--primary-color); font-size: 2.5rem; margin-bottom: 16px;">
274
- 🚀 CryptoVision Pro
275
- </h1>
276
- <p style="color: var(--text-color); font-size: 1.1rem;">
277
- Advanced AI-powered cryptocurrency technical analysis
278
- </p>
279
- </div>
280
- """)
281
 
282
- # Main Content
283
  with gr.Row():
284
- # Upload Column
285
- with gr.Column(elem_classes=["column"]):
286
- with gr.Column(elem_classes=["upload-box"]):
287
- gr.Markdown("### 📤 Upload Your Chart")
288
- chart_input = gr.Image(
289
- type="filepath",
290
- label="",
291
- sources=["upload"],
292
- height=300
293
- )
294
- persona_dropdown = gr.Dropdown(
295
- choices=list(PERSONAS.keys()),
296
- label="Select Trading Persona",
297
- value="Neutral Trader"
298
- )
299
- analyze_btn = gr.Button(
300
- "Analyze Chart",
301
- variant="primary",
302
- elem_classes=["primary-button"]
303
- )
304
-
305
- # Analysis Column
306
- with gr.Column(elem_classes=["column"]):
307
- with gr.Column(elem_classes=["analysis-box"]):
308
- gr.Markdown("### 📊 Analysis Report")
309
- analysis_output = gr.Markdown(
310
- label="",
311
- value="Analysis report will appear here",
312
- elem_classes=["markdown-container"]
313
- )
314
 
315
- # Event Handling
316
  analyze_btn.click(
317
- fn=lambda: loading_html, # Show loading spinner immediately
318
- outputs=analysis_output,
319
  queue=False
320
  ).then(
321
- fn=analyzer.analyze_chart,
322
- inputs=[chart_input, persona_dropdown],
323
- outputs=analysis_output,
324
- api_name="analyze"
 
 
 
 
 
 
 
325
  )
326
 
327
  if __name__ == "__main__":
 
6
  from PIL import Image
7
  import cv2
8
  import numpy as np
9
+ import datetime
10
+ import uuid
11
+ import requests
12
+ from reportlab.lib.pagesizes import letter
13
+ from reportlab.platypus import SimpleDocTemplate, Image as RLImage, Paragraph, Spacer
14
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
15
+ from reportlab.lib.enums import TA_JUSTIFY
16
+ from reportlab.lib import colors
17
 
18
+ # OpenAI ve GitHub Konfigürasyonları
19
  openai.api_key = os.getenv("OPENAI_API_KEY")
20
+ GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
21
+ REPO_OWNER = os.getenv("GITHUB_REPO_OWNER")
22
+ REPO_NAME = os.getenv("GITHUB_REPO_NAME")
23
+
24
+ # Sabitler
25
  ANALYSIS_MODEL = "gpt-4o"
26
  MAX_TOKENS = 4096
27
+ PDF_DIR = "reports"
28
 
29
+ # Persona Tanımları
30
  PERSONAS = {
31
  "Aggressive Trader": {
32
  "description": "High-risk, short-term gains, leverages volatile market movements.",
33
+ "prompt": "Focus on high-risk strategies, short-term gains, and leverage opportunities. Suggest aggressive entry and exit points.",
34
+ "color": colors.red
35
  },
36
  "Conservative Trader": {
37
  "description": "Low-risk, long-term investments, prioritizes capital preservation.",
38
+ "prompt": "Focus on low-risk strategies, long-term investments, and capital preservation. Suggest safe entry points and strict stop-loss levels.",
39
+ "color": colors.blue
40
  },
41
  "Neutral Trader": {
42
  "description": "Balanced approach, combines short and long-term strategies.",
43
+ "prompt": "Focus on balanced strategies, combining short and long-term opportunities. Suggest moderate risk levels and trend-following approaches.",
44
+ "color": colors.green
45
  },
46
  "Reactive Trader": {
47
  "description": "Quick decisions based on market news and social media trends.",
48
+ "prompt": "Focus on quick decision-making, momentum trading, and reacting to market news. Suggest strategies based on current trends and FOMO opportunities.",
49
+ "color": colors.orange
50
  },
51
  "Systematic Trader": {
52
  "description": "Algorithm-based, rule-driven, and emotionless trading.",
53
+ "prompt": "Focus on algorithmic strategies, backtested rules, and quantitative analysis. Suggest data-driven entry and exit points.",
54
+ "color": colors.purple
55
  }
56
  }
57
 
58
+ # Sistem Prompt'u
59
  SYSTEM_PROMPT = """Professional Crypto Technical Analyst:
60
  1. Identify all technical patterns in the chart
61
  2. Determine key support/resistance levels
62
  3. Analyze volume and momentum indicators
63
  4. Calculate risk/reward ratios
64
+ 5. Provide clear trading recommendations
65
  6. Include specific price targets
66
  7. Assess market sentiment
67
  8. Evaluate trend strength
 
71
  class ChartAnalyzer:
72
  def __init__(self):
73
  self.last_analysis = ""
74
+ os.makedirs(PDF_DIR, exist_ok=True)
75
 
76
  def validate_image(self, image_path: str) -> bool:
 
77
  try:
 
78
  with Image.open(image_path) as img:
79
+ img.verify()
80
 
 
81
  img = cv2.imread(image_path)
82
  if img is None:
83
  return False
84
 
 
85
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
86
  edges = cv2.Canny(gray, 50, 150)
87
+ return np.sum(edges) >= 1000
 
 
 
88
  except Exception:
89
  return False
90
 
91
  def optimize_image(self, image_path: str) -> str:
 
92
  try:
93
  img = Image.open(image_path)
94
+ original_width, original_height = img.size
95
+ max_size = 1024
96
+
97
+ if original_width > max_size or original_height > max_size:
98
+ ratio = min(max_size/original_width, max_size/original_height)
99
+ new_size = (int(original_width * ratio), int(original_height * ratio))
100
+ img = img.resize(new_size, Image.LANCZOS)
101
+
102
+ unique_id = uuid.uuid4().hex
103
+ optimized_path = f"{PDF_DIR}/optimized_chart_{unique_id}.png"
104
  img.save(optimized_path, "PNG", optimize=True, quality=85)
105
  return optimized_path
106
  except Exception as e:
107
  print(f"Image optimization error: {str(e)}")
108
+ return image_path
109
 
110
  def encode_image(self, image_path: str) -> str:
 
111
  if not os.path.exists(image_path):
112
  raise FileNotFoundError("File not found")
113
+
 
114
  if os.path.getsize(image_path) > 5 * 1024 * 1024:
115
  raise ValueError("Maximum file size is 5MB")
116
 
117
  with open(image_path, "rb") as image_file:
118
  return base64.b64encode(image_file.read()).decode('utf-8')
119
 
120
+ @lru_cache(maxsize=100)
121
  def analyze_chart(self, image_path: str, persona: str) -> str:
 
122
  try:
 
123
  optimized_path = self.optimize_image(image_path)
124
  base64_image = self.encode_image(optimized_path)
125
 
 
126
  persona_prompt = PERSONAS.get(persona, {}).get("prompt", "")
127
  full_system_prompt = f"{SYSTEM_PROMPT}\n\n{persona_prompt}"
128
 
129
  response = openai.ChatCompletion.create(
130
  model=ANALYSIS_MODEL,
131
  messages=[
132
+ {"role": "system", "content": full_system_prompt},
133
+ {"role": "user", "content": [
134
+ {"type": "text", "text": "Perform detailed technical analysis of this chart:"},
135
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
136
+ }
137
+ ]}
 
 
 
 
 
 
 
 
138
  ],
139
  max_tokens=MAX_TOKENS
140
  )
141
 
142
+ analysis_text = response.choices[0].message.content
143
+ self.last_analysis = analysis_text
144
+ return analysis_text
145
 
 
 
146
  except Exception as e:
147
+ return f"Error: {str(e)}"
148
 
149
+ def create_pdf_styles():
150
+ styles = getSampleStyleSheet()
151
+ styles.add(ParagraphStyle(
152
+ 'Justify',
153
+ parent=styles['BodyText'],
154
+ alignment=TA_JUSTIFY,
155
+ spaceAfter=6
156
+ ))
157
+ styles.add(ParagraphStyle(
158
+ 'PersonaTitle',
159
+ fontSize=14,
160
+ textColor=colors.white,
161
+ backColor=colors.darkblue,
162
+ alignment=1,
163
+ spaceAfter=12
164
+ ))
165
+ return styles
166
 
167
+ def generate_pdf(image_path: str, analysis_text: str, persona: str) -> str:
168
+ styles = create_pdf_styles()
169
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
170
+ filename = f"{PDF_DIR}/report_{timestamp}_{uuid.uuid4().hex[:6]}.pdf"
171
+
172
+ doc = SimpleDocTemplate(filename, pagesize=letter)
173
+ story = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ # Resim ekleme
176
+ try:
177
+ img = Image.open(image_path)
178
+ img_width, img_height = img.size
179
+ aspect = img_height / float(img_width)
180
+ target_width = 400
181
+ target_height = target_width * aspect
182
+
183
+ if target_height > 600:
184
+ target_height = 600
185
+ target_width = target_height / aspect
186
+
187
+ story.append(RLImage(image_path, width=target_width, height=target_height))
188
+ story.append(Spacer(1, 20))
189
+ except Exception as e:
190
+ print(f"PDF image error: {str(e)}")
191
 
192
+ # Persona bilgisi
193
+ persona_color = PERSONAS.get(persona, {}).get("color", colors.black)
194
+ story.append(Paragraph(f"Persona: {persona}", ParagraphStyle(
195
+ 'PersonaTitle',
196
+ fontSize=14,
197
+ textColor=colors.white,
198
+ backColor=persona_color,
199
+ alignment=1
200
+ )))
201
+ story.append(Spacer(1, 20))
202
 
203
+ # Analiz metni
204
+ analysis_style = styles['Justify']
205
+ for line in analysis_text.split('\n'):
206
+ if line.strip():
207
+ p = Paragraph(line.replace('•', '&#8226;'), analysis_style)
208
+ story.append(p)
209
+ story.append(Spacer(1, 12))
210
 
211
+ doc.build(story)
212
+ return filename
 
 
213
 
214
+ def upload_to_github(file_path: str) -> bool:
215
+ try:
216
+ with open(file_path, "rb") as f:
217
+ content = f.read()
218
+
219
+ file_name = os.path.basename(file_path)
220
+ url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{PDF_DIR}/{file_name}"
221
+
222
+ headers = {
223
+ "Authorization": f"token {GITHUB_TOKEN}",
224
+ "Accept": "application/vnd.github.v3+json"
225
+ }
226
+
227
+ # Check if file exists
228
+ response = requests.get(url, headers=headers)
229
+ sha = response.json().get("sha") if response.status_code == 200 else None
230
+
231
+ data = {
232
+ "message": f"Add report {file_name}",
233
+ "content": base64.b64encode(content).decode("utf-8"),
234
+ "branch": "main"
235
+ }
236
+
237
+ if sha:
238
+ data["sha"] = sha
239
+
240
+ response = requests.put(url, headers=headers, json=data)
241
+ return response.status_code in [200, 201]
242
+ except Exception as e:
243
+ print(f"GitHub upload error: {str(e)}")
244
+ return False
245
 
246
+ # Gradio Arayüzü
247
+ custom_css = """
248
+ :root { --primary-color: #2563eb; --secondary-color: #1e40af; }
249
+ .container { max-width: 1200px; margin: 0 auto; }
250
+ .analysis-box { background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
251
+ .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); }
252
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
253
  """
254
 
 
 
 
255
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
256
+ analyzer = ChartAnalyzer()
257
+
258
  with gr.Column(elem_classes=["container"]):
259
+ gr.Markdown("""<div style="text-align: center;"><h1>🚀 CryptoVision Pro</h1></div>""")
 
 
 
 
 
 
 
 
 
260
 
 
261
  with gr.Row():
262
+ with gr.Column():
263
+ with gr.Box(elem_classes=["analysis-box"]):
264
+ chart_input = gr.Image(type="filepath", label="Chart", sources=["upload"])
265
+ persona_dropdown = gr.Dropdown(list(PERSONAS.keys()), label="Trading Persona", value="Neutral Trader")
266
+ analyze_btn = gr.Button("Analyze", variant="primary")
267
+
268
+ with gr.Column():
269
+ with gr.Box(elem_classes=["analysis-box"]):
270
+ analysis_output = gr.Markdown("Analysis will appear here...")
271
+ pdf_status = gr.HTML()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
 
273
  analyze_btn.click(
274
+ lambda: (gr.update(visible=False), gr.update(value="<div class='loading-spinner'></div>")),
275
+ outputs=[analysis_output, pdf_status],
276
  queue=False
277
  ).then(
278
+ analyzer.analyze_chart,
279
+ [chart_input, persona_dropdown],
280
+ analysis_output
281
+ ).then(
282
+ lambda img, text, persona: generate_pdf(img, text, persona),
283
+ [chart_input, analysis_output, persona_dropdown],
284
+ None
285
+ ).then(
286
+ lambda path: upload_to_github(path) if all([GITHUB_TOKEN, REPO_OWNER, REPO_NAME]) else None,
287
+ None,
288
+ pdf_status
289
  )
290
 
291
  if __name__ == "__main__":