Amós e Souza Fernandes commited on
Commit
6fc4b2b
·
verified ·
1 Parent(s): 5f10e37

Update app.py

Browse files

Commit ini train

Files changed (1) hide show
  1. app.py +750 -749
app.py CHANGED
@@ -1,750 +1,751 @@
1
- # rnn/app.py
2
-
3
- import os
4
- import uuid
5
- import time
6
- import hmac
7
- import hashlib
8
- import json
9
- from datetime import datetime, timedelta
10
- from typing import Dict, Any, List, Optional
11
- from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data
12
-
13
- import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
14
- from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
15
- from fastapi.middleware.cors import CORSMiddleware
16
- from fastapi.responses import HTMLResponse, JSONResponse
17
- from fastapi.staticfiles import StaticFiles
18
- from jinja2 import Environment, FileSystemLoader
19
- from pydantic import BaseModel, Field
20
-
21
- from rnn.app.model.rnn_predictor import RNNModelPredictor
22
- from rnn.app.utils.logger import get_logger
23
-
24
-
25
-
26
- logger = get_logger()
27
-
28
- # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
29
- AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
30
- AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
31
- CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
32
-
33
- # Chaves para serviços externos
34
- MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
35
- EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
36
- EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
37
-
38
- if not AIBANK_API_KEY:
39
- logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
40
- if not AIBANK_CALLBACK_URL:
41
- logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
42
- if not CALLBACK_SHARED_SECRET:
43
- logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
44
-
45
-
46
-
47
-
48
- app = FastAPI(title="ATCoin Neural Agents - Investment API")
49
-
50
- # --- Middlewares ---
51
- app.add_middleware(
52
- CORSMiddleware,
53
- allow_origins=[
54
- "http://localhost:3000", # URL desenvolvimento local
55
- "http://aibank.app.br", # URL de produção
56
- "https://*.aibank.app.br", # subdomínios
57
- "https://*.hf.space" # HF Space
58
- ],
59
- allow_credentials=True,
60
- allow_methods=["*"],
61
- allow_headers=["*"],
62
- )
63
-
64
- # --- Simulação de Banco de Dados de Transações DEV ---
65
- # Em produção MongoDB
66
- transactions_db: Dict[str, Dict[str, Any]] = {}
67
-
68
- # --- Modelos Pydantic ---
69
- class InvestmentRequest(BaseModel):
70
- client_id: str
71
- amount: float = Field(..., gt=0) # Garante que o montante seja positivo
72
- aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
73
-
74
- class InvestmentResponse(BaseModel):
75
- status: str
76
- message: str
77
- rnn_transaction_id: str # ID da transação this.API
78
-
79
- class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
80
- rnn_transaction_id: str
81
- aibank_transaction_token: str
82
- client_id: str
83
- initial_amount: float
84
- final_amount: float
85
- profit_loss: float
86
- status: str # "completed", "failed"
87
- timestamp: datetime
88
- details: str = ""
89
-
90
-
91
- # --- Dependência de Autenticação ---
92
- async def verify_aibank_key(authorization: str = Header(None)):
93
- if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
94
- logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
95
- raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
96
-
97
- if authorization is None:
98
- logger.warning("Authorization header ausente na chamada do AIBank.")
99
- raise HTTPException(status_code=401, detail="Authorization header is missing")
100
-
101
- parts = authorization.split()
102
- if len(parts) != 2 or parts[0].lower() != 'bearer':
103
- logger.warning(f"Formato inválido do Authorization header: {authorization}")
104
- raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
105
-
106
- token_from_aibank = parts[1]
107
- if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
108
- logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
109
- raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
110
- logger.info("API Key do AIBank verificada com sucesso.")
111
- return True
112
-
113
-
114
- # --- Lógica de Negócio Principal (Simulada e em Background) ---
115
-
116
-
117
- async def execute_investment_strategy_background(
118
- rnn_tx_id: str,
119
- client_id: str,
120
- amount: float,
121
- aibank_tx_token: str
122
- ):
123
- logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
124
- transactions_db[rnn_tx_id]["status"] = "processing"
125
- transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
126
-
127
- final_status = "completed"
128
- error_details = "" # Acumula mensagens de erro de várias etapas
129
- calculated_final_amount = amount
130
-
131
- # Inicializa a exchange ccxt usando o utilitário
132
- # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream
133
- exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO
134
-
135
- if not exchange:
136
- # get_ccxt_exchange já loga o erro. Se a exchange é crucial, podemos falhar aqui.
137
- logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.")
138
- # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config.
139
- if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"):
140
- error_details += "Failed to initialize CCXT exchange despite API keys being present; "
141
- final_status = "failed_config"
142
- # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo)
143
-
144
- # =========================================================================
145
- # 1. COLETAR DADOS DE MERCADO
146
- # =========================================================================
147
- logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
148
- transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
149
- market_data_results = {"crypto": {}, "stocks": {}, "other": {}}
150
- critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados
151
-
152
- # --- Coleta de dados de Cripto via ccxt_utils ---
153
- if exchange:
154
- crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável
155
-
156
- crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data(
157
- exchange,
158
- crypto_pairs_to_fetch,
159
- logger_instance=logger
160
- )
161
- market_data_results["crypto"] = crypto_data
162
- if not crypto_fetch_ok:
163
- error_details += f"Crypto data fetch issues: {crypto_err_msg}; "
164
- # Decida se a falha na coleta de cripto é crítica
165
- # Se for, defina critical_data_fetch_failed = True
166
- if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto
167
- critical_data_fetch_failed = True
168
- logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.")
169
- else:
170
- logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
171
- if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou
172
- error_details += "CCXT exchange not initialized, crypto data skipped; "
173
- critical_data_fetch_failed = True
174
-
175
-
176
- # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) ---
177
- # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"])
178
- # try:
179
- # import yfinance as yf # Mova para o topo do app.py se for usar
180
- # # ... lógica yfinance ...
181
- # except Exception as e_yf:
182
- # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}")
183
- # error_details += f"YFinance data fetch failed: {str(e_yf)}; "
184
- # # Decida se isso é crítico: critical_data_fetch_failed = True
185
-
186
- market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação
187
-
188
- transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
189
-
190
- # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS ---
191
- if critical_data_fetch_failed:
192
- final_status = "failed_market_data"
193
- logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
194
- # Pular para a seção de callback
195
- # (A lógica de envio do callback precisa ser alcançada)
196
- else:
197
- logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.")
198
- transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
199
-
200
-
201
-
202
-
203
-
204
- # =========================================================================
205
- # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO
206
- # =========================================================================
207
- investment_decisions: List[Dict[str, Any]] = []
208
- total_usd_allocated_by_rnn = 0.0
209
- loop = asyncio.get_running_loop()
210
-
211
- if final_status == "completed":
212
- logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...")
213
- transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
214
- rnn_analysis_success = True
215
-
216
- # CORRIGIDO: Acessando app.state.rnn_predictor
217
- predictor: Optional[RNNModelPredictor] = getattr(app.state, 'rnn_predictor', None)
218
-
219
- try:
220
- crypto_data_for_rnn = market_data_results.get("crypto", {})
221
- candidate_assets = [
222
- asset_key for asset_key, data in crypto_data_for_rnn.items()
223
- if data and not data.get("error") and data.get("ohlcv_1h") # Apenas com dados válidos
224
- ]
225
-
226
- # --- Parâmetros de Gerenciamento de Risco e Alocação (AJUSTE FINO É CRUCIAL) ---
227
- # Risco total do portfólio para este ciclo (ex: não usar mais que 50% do capital total em novas posições)
228
- MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE = 0.75 # Usar até 75% do 'amount'
229
-
230
- # Risco por ativo individual (percentual do 'amount' TOTAL)
231
- MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.15 # Ex: máx 15% do capital total em UM ativo
232
- MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.02 # Ex: mín 2% do capital total para valer a pena
233
-
234
- MIN_USD_PER_ORDER = 25.00 # Mínimo de USD por ordem
235
- MAX_CONCURRENT_POSITIONS = 4 # Máximo de posições abertas simultaneamente
236
-
237
- # Limiares de Confiança da RNN
238
- CONFIDENCE_STRONG_BUY = 0.80 # Confiança para considerar uma alocação maior
239
- CONFIDENCE_MODERATE_BUY = 0.65 # Confiança mínima para considerar uma alocação base
240
- CONFIDENCE_WEAK_BUY = 0.55 # Confiança para uma alocação muito pequena ou nenhuma
241
-
242
- allocated_capital_this_cycle = 0.0
243
-
244
- # Para diversificação, podemos querer limitar a avaliação ou dar pesos
245
- # random.shuffle(candidate_assets)
246
-
247
- for asset_key in candidate_assets:
248
- if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
249
- logger.info(f"BG TASK [{rnn_tx_id}]: Limite de {MAX_CONCURRENT_POSITIONS} posições concorrentes atingido.")
250
- break
251
-
252
- # Verifica se já usamos o capital máximo para o ciclo
253
- if allocated_capital_this_cycle >= amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE:
254
- logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital para o ciclo ({MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE*100}%) atingido.")
255
- break
256
-
257
- asset_symbol = asset_key.replace("_", "/")
258
- logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
259
-
260
- signal, confidence_prob = await predictor.predict_for_asset(
261
- crypto_data_for_rnn[asset_key],
262
- loop=loop
263
- )
264
-
265
- if signal == 1 and confidence_prob is not None: # Sinal de COMPRA e confiança válida
266
- target_usd_allocation = 0.0
267
-
268
- if confidence_prob >= CONFIDENCE_STRONG_BUY:
269
- # Alocação maior para sinais fortes
270
- # Ex: entre 60% e 100% da alocação máxima permitida por ativo
271
- alloc_factor = 0.6 + 0.4 * ((confidence_prob - CONFIDENCE_STRONG_BUY) / (1.0 - CONFIDENCE_STRONG_BUY + 1e-6))
272
- target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
273
- reason = f"RNN STRONG BUY signal (Conf: {confidence_prob:.3f})"
274
- elif confidence_prob >= CONFIDENCE_MODERATE_BUY:
275
- # Alocação base para sinais moderados
276
- # Ex: entre 30% e 60% da alocação máxima permitida por ativo
277
- alloc_factor = 0.3 + 0.3 * ((confidence_prob - CONFIDENCE_MODERATE_BUY) / (CONFIDENCE_STRONG_BUY - CONFIDENCE_MODERATE_BUY + 1e-6))
278
- target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
279
- reason = f"RNN MODERATE BUY signal (Conf: {confidence_prob:.3f})"
280
- elif confidence_prob >= CONFIDENCE_WEAK_BUY:
281
- # Alocação pequena para sinais fracos (ou nenhuma)
282
- alloc_factor = 0.1 + 0.2 * ((confidence_prob - CONFIDENCE_WEAK_BUY) / (CONFIDENCE_MODERATE_BUY - CONFIDENCE_WEAK_BUY + 1e-6))
283
- target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
284
- reason = f"RNN WEAK BUY signal (Conf: {confidence_prob:.3f})"
285
- else:
286
- logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob:.3f}) abaixo do limiar WEAK_BUY ({CONFIDENCE_WEAK_BUY}). Pulando.")
287
- continue
288
-
289
- # Garantir que a alocação não seja menor que a mínima permitida (percentual do total)
290
- target_usd_allocation = max(target_usd_allocation, amount * MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL)
291
-
292
- # Garantir que não exceda o capital restante disponível neste CICLO
293
- capital_left_for_this_cycle = (amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE) - allocated_capital_this_cycle
294
- actual_usd_allocation = min(target_usd_allocation, capital_left_for_this_cycle)
295
-
296
- # Garantir que a ordem mínima em USD seja respeitada
297
- if actual_usd_allocation < MIN_USD_PER_ORDER:
298
- logger.info(f"BG TASK [{rnn_tx_id}]: Alocação final ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo de ordem ({MIN_USD_PER_ORDER}). Pulando.")
299
- continue
300
-
301
- # Adicionar à lista de decisões
302
- investment_decisions.append({
303
- "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
304
- "target_usd_amount": round(actual_usd_allocation, 2),
305
- "rnn_confidence": round(confidence_prob, 4),
306
- "reasoning": reason
307
- })
308
- allocated_capital_this_cycle += round(actual_usd_allocation, 2)
309
- logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol}. {reason}")
310
-
311
- # ... (restante da lógica para signal 0 ou None) ...
312
- except Exception as e: # Captura exceções da lógica da RNN
313
- logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
314
- rnn_analysis_success = False # Marca que a análise RNN falhou
315
- error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
316
-
317
-
318
- total_usd_allocated_by_rnn = allocated_capital_this_cycle
319
-
320
-
321
-
322
-
323
- if not predictor or not predictor.model: # Verifica se o preditor e o modelo interno existem
324
- logger.warning(f"BG TASK [{rnn_tx_id}]: Instância do preditor RNN não disponível ou modelo interno não carregado. Pulando análise RNN.")
325
- rnn_analysis_success = False
326
- error_details += "RNN model/predictor not available for prediction; "
327
- else:
328
- try:
329
- # ... (lógica de iteração sobre `candidate_assets` e chamada a `predictor.predict_for_asset` como na resposta anterior)
330
- # ... (lógica de alocação de capital como na resposta anterior)
331
- # Garantir que toda essa lógica está dentro deste bloco 'else'
332
- crypto_data_for_rnn = market_data_results.get("crypto", {})
333
- candidate_assets = [
334
- asset_key for asset_key, data in crypto_data_for_rnn.items()
335
- if data and not data.get("error") and data.get("ohlcv_1h")
336
- ]
337
-
338
- MAX_RISK_PER_ASSET_PCT = 0.05
339
- MIN_USD_PER_ORDER = 20.00
340
- MAX_CONCURRENT_POSITIONS = 5
341
- CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC = 0.85
342
- CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC = 0.60
343
- BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL = 0.10
344
-
345
- allocated_capital_this_cycle = 0.0
346
-
347
- for asset_key in candidate_assets:
348
- if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
349
- logger.info(f"BG TASK [{rnn_tx_id}]: Limite de posições concorrentes ({MAX_CONCURRENT_POSITIONS}) atingido.")
350
- break
351
- if allocated_capital_this_cycle >= amount * 0.90:
352
- logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital do ciclo atingido.")
353
- break
354
-
355
- asset_symbol = asset_key.replace("_", "/")
356
- logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
357
-
358
- signal, confidence_prob = await predictor.predict_for_asset(
359
- crypto_data_for_rnn[asset_key],
360
- loop=loop
361
- # window_size e expected_features serão os defaults de rnn_predictor.py
362
- # ou podem ser passados explicitamente se você quiser variar por ativo
363
- )
364
-
365
- if signal == 1:
366
- if confidence_prob is None or confidence_prob < CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
367
- logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob}) abaixo do mínimo {CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC}. Pulando.")
368
- continue
369
-
370
- confidence_factor = 0.5
371
- if confidence_prob >= CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC:
372
- confidence_factor = 1.0
373
- elif confidence_prob > CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
374
- confidence_factor = 0.5 + 0.5 * (
375
- (confidence_prob - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) /
376
- (CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC)
377
- )
378
-
379
- potential_usd_allocation = amount * BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL * confidence_factor
380
- potential_usd_allocation = min(potential_usd_allocation, amount * MAX_RISK_PER_ASSET_PCT)
381
- remaining_capital_for_cycle = amount - allocated_capital_this_cycle # Recalcula a cada iteração
382
- actual_usd_allocation = min(potential_usd_allocation, remaining_capital_for_cycle)
383
-
384
- if actual_usd_allocation < MIN_USD_PER_ORDER:
385
- logger.info(f"BG TASK [{rnn_tx_id}]: Alocação calculada ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo ({MIN_USD_PER_ORDER}). Pulando.")
386
- continue
387
-
388
- investment_decisions.append({
389
- "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
390
- "target_usd_amount": round(actual_usd_allocation, 2),
391
- "rnn_confidence": round(confidence_prob, 4) if confidence_prob is not None else None,
392
- "reasoning": f"RNN signal BUY for {asset_symbol} with confidence {confidence_prob:.2f}"
393
- })
394
- allocated_capital_this_cycle += round(actual_usd_allocation, 2)
395
- logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol} (Conf: {confidence_prob:.2f})")
396
-
397
- elif signal == 0:
398
- logger.info(f"BG TASK [{rnn_tx_id}]: RNN sinal NÃO COMPRAR para {asset_symbol} (Conf: {confidence_prob:.2f if confidence_prob is not None else 'N/A'})")
399
- else:
400
- logger.warning(f"BG TASK [{rnn_tx_id}]: RNN não gerou sinal para {asset_symbol}.")
401
-
402
- if not investment_decisions:
403
- logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA válidas após avaliação e alocação.")
404
-
405
- except Exception as e: # Captura exceções da lógica da RNN
406
- logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
407
- rnn_analysis_success = False # Marca que a análise RNN falhou
408
- error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
409
-
410
- if not rnn_analysis_success: # Se a flag foi setada para False
411
- final_status = "failed_rnn_analysis"
412
-
413
- transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
414
-
415
- total_usd_allocated_by_rnn = allocated_capital_this_cycle
416
- transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
- # =========================================================================
428
- # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens)
429
- # =========================================================================
430
- executed_trades_info: List[Dict[str, Any]] = []
431
- current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo
432
- cash_remaining_after_execution = amount # Começa com todo o montante
433
-
434
- if final_status == "completed" and investment_decisions and exchange:
435
- logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...")
436
- transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
437
- order_execution_overall_success = True
438
-
439
- # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER)
440
- # Esta seção precisa ser preenchida com:
441
- # 1. Iterar sobre `investment_decisions`.
442
- # 2. Para cada decisão de "BUY":
443
- # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT").
444
- # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar.
445
- # `amount_of_asset = target_usd_amount / current_price_of_asset`
446
- # c. Considerar saldo disponível na exchange (se estiver gerenciando isso).
447
- # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)`
448
- # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`.
449
- # Para ordens limite, a RNN precisaria fornecer o `limit_price`.
450
- # e. Tratar respostas da exchange (sucesso, falha, ID da ordem).
451
- # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc.
452
- # f. Armazenar detalhes da ordem em `executed_trades_info`:
453
- # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy",
454
- # "requested_usd_amount": ..., "asset_quantity_ordered": ...,
455
- # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ...,
456
- # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... }
457
- # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida.
458
- # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`.
459
- # 3. Para decisões de "SELL" (se sua RNN gerar):
460
- # a. Verificar se você possui o ativo (requer gerenciamento de portfólio).
461
- # b. Criar ordem de venda.
462
- # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`.
463
-
464
- # Simulação atual:
465
- for decision in investment_decisions:
466
- if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO":
467
- asset_symbol = decision["asset_id"]
468
- usd_to_spend = decision["target_usd_amount"]
469
-
470
- # Simular pequena chance de falha na ordem
471
- if random.random() < 0.05:
472
- logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.")
473
- executed_trades_info.append({
474
- "asset_id": asset_symbol, "status": "failed_simulated",
475
- "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection"
476
- })
477
- order_execution_overall_success = False # Marca que pelo menos uma falhou
478
- continue # Pula para a próxima decisão
479
-
480
- # Simular slippage e custo
481
- simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage
482
-
483
- # Garantir que não estamos gastando mais do que o caixa restante
484
- if simulated_cost > cash_remaining_after_execution:
485
- simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem
486
- if simulated_cost < 1: # Se não quase nada, não faz a ordem
487
- logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.")
488
- continue
489
-
490
-
491
- if simulated_cost > 0:
492
- current_portfolio_value += simulated_cost
493
- cash_remaining_after_execution -= simulated_cost
494
- executed_trades_info.append({
495
- "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}",
496
- "type": "market", "side": "buy",
497
- "requested_usd_amount": usd_to_spend,
498
- "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2),
499
- "timestamp": datetime.utcnow().isoformat()
500
- })
501
- logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.")
502
-
503
- await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1)
504
-
505
- if not order_execution_overall_success:
506
- error_details += "One or more orders failed during execution; "
507
- # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure'
508
- # final_status = "completed_with_partial_failure" # Exemplo de um novo status
509
-
510
- elif not exchange and investment_decisions:
511
- logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.")
512
- error_details += "Exchange not available for order execution; "
513
- final_status = "failed_order_execution" # Se a execução é crítica
514
- cash_remaining_after_execution = amount # Nada foi gasto
515
-
516
- transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
517
- transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2)
518
- transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2)
519
-
520
-
521
- # =========================================================================
522
- # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes)
523
- # =========================================================================
524
- value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo
525
-
526
- if final_status == "completed": # Ou "completed_with_partial_failure"
527
- transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation"
528
- logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...")
529
- await asyncio.sleep(random.uniform(3, 7))
530
-
531
- if current_portfolio_value > 0:
532
- # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO.
533
- # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado).
534
- daily_return_factor = 0.042 # A meta
535
- simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo)
536
- # Para ser mais realista, o fator de performance deveria ser algo como:
537
- # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto)
538
- # E não diretamente ligado à meta de 4.2%
539
-
540
- # Ajuste para uma simulação de retorno mais plausível (ainda agressiva)
541
- # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido
542
- actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05)
543
-
544
- profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio
545
- value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio
546
- logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, "
547
- f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}")
548
- else:
549
- logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).")
550
- value_of_investments_at_eod = 0.0
551
-
552
- # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado
553
- calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution
554
-
555
- else: # Se houve falha antes, o valor final é o que sobrou após a falha
556
- calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial
557
- logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.")
558
-
559
- transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2)
560
- transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2)
561
-
562
-
563
- # =========================================================================
564
- # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes)
565
- # =========================================================================
566
- if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar
567
- transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)"
568
- logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
569
- # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER)
570
- # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]`
571
- # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.)
572
- # 2. Se for usar blockchain:
573
- # a. Preparar os dados para um contrato inteligente.
574
- # b. Interagir com o contrato (ex: web3.py para Ethereum).
575
- # c. Armazenar o hash da transação da blockchain.
576
- # 3. Se for um registro interno avançado:
577
- # a. Assinar digitalmente os dados da transação.
578
- # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria.
579
-
580
- # Simulação atual (hash dos dados da transação):
581
- transaction_data_for_hash = {
582
- "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount,
583
- "final_amount_calculated": calculated_final_amount,
584
- # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante
585
- "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()),
586
- "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])),
587
- "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])),
588
- "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"),
589
- "timestamp": datetime.utcnow().isoformat()
590
- }
591
- ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True)
592
- proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest()
593
-
594
- transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash
595
- transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof"
596
- await asyncio.sleep(0.5) # Simula tempo de escrita/hash
597
- logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...")
598
-
599
-
600
- # =========================================================================
601
- # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
602
- # =========================================================================
603
- if exchange and hasattr(exchange, 'close'):
604
- try:
605
- await exchange.close()
606
- logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.")
607
- except Exception as e_close: # Especificar o tipo de exceção se souber
608
- logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}")
609
-
610
- if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
611
- logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.")
612
- transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical"
613
- return
614
-
615
- # Certifique-se que `final_status` reflete o estado real da operação
616
- # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o
617
- if error_details and final_status == "completed":
618
- final_status = "completed_with_warnings" # Ou um status mais apropriado
619
-
620
- callback_payload_data = InvestmentResultPayload(
621
- rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id,
622
- initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais
623
- profit_loss=round(calculated_final_amount - amount, 2),
624
- status=final_status, timestamp=datetime.utcnow(),
625
- details=error_details if error_details else "Investment cycle processed."
626
- )
627
- payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada
628
-
629
- signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest()
630
- headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
631
-
632
- logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}")
633
- transactions_db[rnn_tx_id]["callback_status"] = "sending"
634
- try:
635
- async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente
636
- response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers)
637
- response.raise_for_status()
638
- logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}")
639
- transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
640
- except httpx.RequestError as e_req:
641
- logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}")
642
- transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error"
643
- except httpx.HTTPStatusError as e_http:
644
- logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP do AIBank ao receber callback: {e_http.response.status_code} - {e_http.response.text[:200]}")
645
- transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}"
646
- except Exception as e_cb_final:
647
- logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True)
648
- transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
649
-
650
-
651
- import asyncio
652
- import random
653
-
654
-
655
-
656
- # --- Endpoints da API ---
657
- @app.post("/api/invest",
658
- response_model=InvestmentResponse,
659
- dependencies=[Depends(verify_aibank_key)])
660
- async def initiate_investment(
661
- request_data: InvestmentRequest,
662
- background_tasks: BackgroundTasks
663
- ):
664
- """
665
- Endpoint para o AIBank iniciar um ciclo de investimento.
666
- Responde rapidamente e executa a lógica pesada em background.
667
- """
668
- logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
669
- f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
670
-
671
- rnn_tx_id = str(uuid.uuid4())
672
-
673
- # Armazena informações iniciais da transação DB real para ser mais robusto
674
- transactions_db[rnn_tx_id] = {
675
- "rnn_transaction_id": rnn_tx_id,
676
- "aibank_transaction_token": request_data.aibank_transaction_token,
677
- "client_id": request_data.client_id,
678
- "initial_amount": request_data.amount,
679
- "status": "pending_background_processing",
680
- "received_at": datetime.utcnow().isoformat(),
681
- "callback_status": "not_sent_yet"
682
- }
683
-
684
- # Adiciona a tarefa de longa duração ao background
685
- background_tasks.add_task(
686
- execute_investment_strategy_background,
687
- rnn_tx_id,
688
- request_data.client_id,
689
- request_data.amount,
690
- request_data.aibank_transaction_token
691
- )
692
-
693
- logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
694
- return InvestmentResponse(
695
- status="pending",
696
- message="Investment request received and is being processed in the background. Await callback for results.",
697
- rnn_transaction_id=rnn_tx_id
698
- )
699
-
700
- @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
701
- async def get_transaction_status(rnn_tx_id: str):
702
- """ Endpoint para verificar o status de uma transação (para debug/admin) """
703
- transaction = transactions_db.get(rnn_tx_id)
704
- if not transaction:
705
- raise HTTPException(status_code=404, detail="Transaction not found")
706
- return transaction
707
-
708
-
709
- # --- Dashboard (Existente, adaptado) ---
710
- # Setup para arquivos estáticos e templates
711
-
712
- try:
713
- app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
714
- templates = Environment(loader=FileSystemLoader("rnn/templates"))
715
- except RuntimeError as e:
716
- logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
717
- templates = None # Para evitar erros se o loader falhar
718
-
719
- @app.get("/", response_class=HTMLResponse)
720
- async def index(request: Request):
721
- if not templates:
722
- return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
723
-
724
- agora = datetime.now()
725
- agentes_simulados = [
726
- # dados de agentes ...
727
- ]
728
- template = templates.get_template("index.html")
729
- # Adicionar transações recentes ao contexto do template
730
- recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
731
- return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
732
-
733
- # --- Imports para Background Task ---
734
- import asyncio
735
- import random
736
-
737
- # Função de logger dummy
738
- # class DummyLogger:
739
- # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
740
- # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
741
- # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
742
-
743
- # if __name__ == "__main__": # Para teste local
744
- # # logger = DummyLogger() # se não tiver get_logger()
745
- # # Configuração das variáveis de ambiente para teste local
746
- # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
747
- # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
748
- # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
749
- # # import uvicorn
 
750
  # # uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ # rnn/app.py
2
+
3
+ import agents.train_rl_portfolio_agent
4
+ import os
5
+ import uuid
6
+ import time
7
+ import hmac
8
+ import hashlib
9
+ import json
10
+ from datetime import datetime, timedelta
11
+ from typing import Dict, Any, List, Optional
12
+ from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data
13
+
14
+ import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
15
+ from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import HTMLResponse, JSONResponse
18
+ from fastapi.staticfiles import StaticFiles
19
+ from jinja2 import Environment, FileSystemLoader
20
+ from pydantic import BaseModel, Field
21
+
22
+ from rnn.app.model.rnn_predictor import RNNModelPredictor
23
+ from rnn.app.utils.logger import get_logger
24
+
25
+
26
+
27
+ logger = get_logger()
28
+
29
+ # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
30
+ AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
31
+ AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
32
+ CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
33
+
34
+ # Chaves para serviços externos
35
+ MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
36
+ EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
37
+ EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
38
+
39
+ if not AIBANK_API_KEY:
40
+ logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
41
+ if not AIBANK_CALLBACK_URL:
42
+ logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
43
+ if not CALLBACK_SHARED_SECRET:
44
+ logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
45
+
46
+
47
+
48
+
49
+ app = FastAPI(title="ATCoin Neural Agents - Investment API")
50
+
51
+ # --- Middlewares ---
52
+ app.add_middleware(
53
+ CORSMiddleware,
54
+ allow_origins=[
55
+ "http://localhost:3000", # URL desenvolvimento local
56
+ "http://aibank.app.br", # URL de produção
57
+ "https://*.aibank.app.br", # subdomínios
58
+ "https://*.hf.space" # HF Space
59
+ ],
60
+ allow_credentials=True,
61
+ allow_methods=["*"],
62
+ allow_headers=["*"],
63
+ )
64
+
65
+ # --- Simulação de Banco de Dados de Transações DEV ---
66
+ # Em produção MongoDB
67
+ transactions_db: Dict[str, Dict[str, Any]] = {}
68
+
69
+ # --- Modelos Pydantic ---
70
+ class InvestmentRequest(BaseModel):
71
+ client_id: str
72
+ amount: float = Field(..., gt=0) # Garante que o montante seja positivo
73
+ aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
74
+
75
+ class InvestmentResponse(BaseModel):
76
+ status: str
77
+ message: str
78
+ rnn_transaction_id: str # ID da transação this.API
79
+
80
+ class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
81
+ rnn_transaction_id: str
82
+ aibank_transaction_token: str
83
+ client_id: str
84
+ initial_amount: float
85
+ final_amount: float
86
+ profit_loss: float
87
+ status: str # "completed", "failed"
88
+ timestamp: datetime
89
+ details: str = ""
90
+
91
+
92
+ # --- Dependência de Autenticação ---
93
+ async def verify_aibank_key(authorization: str = Header(None)):
94
+ if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
95
+ logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
96
+ raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
97
+
98
+ if authorization is None:
99
+ logger.warning("Authorization header ausente na chamada do AIBank.")
100
+ raise HTTPException(status_code=401, detail="Authorization header is missing")
101
+
102
+ parts = authorization.split()
103
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
104
+ logger.warning(f"Formato inválido do Authorization header: {authorization}")
105
+ raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
106
+
107
+ token_from_aibank = parts[1]
108
+ if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
109
+ logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
110
+ raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
111
+ logger.info("API Key do AIBank verificada com sucesso.")
112
+ return True
113
+
114
+
115
+ # --- Lógica de Negócio Principal (Simulada e em Background) ---
116
+
117
+
118
+ async def execute_investment_strategy_background(
119
+ rnn_tx_id: str,
120
+ client_id: str,
121
+ amount: float,
122
+ aibank_tx_token: str
123
+ ):
124
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
125
+ transactions_db[rnn_tx_id]["status"] = "processing"
126
+ transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
127
+
128
+ final_status = "completed"
129
+ error_details = "" # Acumula mensagens de erro de várias etapas
130
+ calculated_final_amount = amount
131
+
132
+ # Inicializa a exchange ccxt usando o utilitário
133
+ # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream
134
+ exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO
135
+
136
+ if not exchange:
137
+ # get_ccxt_exchange loga o erro. Se a exchange é crucial, podemos falhar aqui.
138
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.")
139
+ # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config.
140
+ if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"):
141
+ error_details += "Failed to initialize CCXT exchange despite API keys being present; "
142
+ final_status = "failed_config"
143
+ # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo)
144
+
145
+ # =========================================================================
146
+ # 1. COLETAR DADOS DE MERCADO
147
+ # =========================================================================
148
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
149
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
150
+ market_data_results = {"crypto": {}, "stocks": {}, "other": {}}
151
+ critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados
152
+
153
+ # --- Coleta de dados de Cripto via ccxt_utils ---
154
+ if exchange:
155
+ crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável
156
+
157
+ crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data(
158
+ exchange,
159
+ crypto_pairs_to_fetch,
160
+ logger_instance=logger
161
+ )
162
+ market_data_results["crypto"] = crypto_data
163
+ if not crypto_fetch_ok:
164
+ error_details += f"Crypto data fetch issues: {crypto_err_msg}; "
165
+ # Decida se a falha na coleta de cripto é crítica
166
+ # Se for, defina critical_data_fetch_failed = True
167
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto
168
+ critical_data_fetch_failed = True
169
+ logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.")
170
+ else:
171
+ logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
172
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou
173
+ error_details += "CCXT exchange not initialized, crypto data skipped; "
174
+ critical_data_fetch_failed = True
175
+
176
+
177
+ # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) ---
178
+ # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"])
179
+ # try:
180
+ # import yfinance as yf # Mova para o topo do app.py se for usar
181
+ # # ... lógica yfinance ...
182
+ # except Exception as e_yf:
183
+ # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}")
184
+ # error_details += f"YFinance data fetch failed: {str(e_yf)}; "
185
+ # # Decida se isso é crítico: critical_data_fetch_failed = True
186
+
187
+ market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação
188
+
189
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
190
+
191
+ # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS ---
192
+ if critical_data_fetch_failed:
193
+ final_status = "failed_market_data"
194
+ logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
195
+ # Pular para a seção de callback
196
+ # (A lógica de envio do callback precisa ser alcançada)
197
+ else:
198
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.")
199
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
200
+
201
+
202
+
203
+
204
+
205
+ # =========================================================================
206
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO
207
+ # =========================================================================
208
+ investment_decisions: List[Dict[str, Any]] = []
209
+ total_usd_allocated_by_rnn = 0.0
210
+ loop = asyncio.get_running_loop()
211
+
212
+ if final_status == "completed":
213
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...")
214
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
215
+ rnn_analysis_success = True
216
+
217
+ # CORRIGIDO: Acessando app.state.rnn_predictor
218
+ predictor: Optional[RNNModelPredictor] = getattr(app.state, 'rnn_predictor', None)
219
+
220
+ try:
221
+ crypto_data_for_rnn = market_data_results.get("crypto", {})
222
+ candidate_assets = [
223
+ asset_key for asset_key, data in crypto_data_for_rnn.items()
224
+ if data and not data.get("error") and data.get("ohlcv_1h") # Apenas com dados válidos
225
+ ]
226
+
227
+ # --- Parâmetros de Gerenciamento de Risco e Alocação (AJUSTE FINO É CRUCIAL) ---
228
+ # Risco total do portfólio para este ciclo (ex: não usar mais que 50% do capital total em novas posições)
229
+ MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE = 0.75 # Usar até 75% do 'amount'
230
+
231
+ # Risco por ativo individual (percentual do 'amount' TOTAL)
232
+ MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.15 # Ex: máx 15% do capital total em UM ativo
233
+ MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.02 # Ex: mín 2% do capital total para valer a pena
234
+
235
+ MIN_USD_PER_ORDER = 25.00 # Mínimo de USD por ordem
236
+ MAX_CONCURRENT_POSITIONS = 4 # Máximo de posições abertas simultaneamente
237
+
238
+ # Limiares de Confiança da RNN
239
+ CONFIDENCE_STRONG_BUY = 0.80 # Confiança para considerar uma alocação maior
240
+ CONFIDENCE_MODERATE_BUY = 0.65 # Confiança mínima para considerar uma alocação base
241
+ CONFIDENCE_WEAK_BUY = 0.55 # Confiança para uma alocação muito pequena ou nenhuma
242
+
243
+ allocated_capital_this_cycle = 0.0
244
+
245
+ # Para diversificação, podemos querer limitar a avaliação ou dar pesos
246
+ # random.shuffle(candidate_assets)
247
+
248
+ for asset_key in candidate_assets:
249
+ if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
250
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de {MAX_CONCURRENT_POSITIONS} posições concorrentes atingido.")
251
+ break
252
+
253
+ # Verifica se usamos o capital máximo para o ciclo
254
+ if allocated_capital_this_cycle >= amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE:
255
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital para o ciclo ({MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE*100}%) atingido.")
256
+ break
257
+
258
+ asset_symbol = asset_key.replace("_", "/")
259
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
260
+
261
+ signal, confidence_prob = await predictor.predict_for_asset(
262
+ crypto_data_for_rnn[asset_key],
263
+ loop=loop
264
+ )
265
+
266
+ if signal == 1 and confidence_prob is not None: # Sinal de COMPRA e confiança válida
267
+ target_usd_allocation = 0.0
268
+
269
+ if confidence_prob >= CONFIDENCE_STRONG_BUY:
270
+ # Alocação maior para sinais fortes
271
+ # Ex: entre 60% e 100% da alocação máxima permitida por ativo
272
+ alloc_factor = 0.6 + 0.4 * ((confidence_prob - CONFIDENCE_STRONG_BUY) / (1.0 - CONFIDENCE_STRONG_BUY + 1e-6))
273
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
274
+ reason = f"RNN STRONG BUY signal (Conf: {confidence_prob:.3f})"
275
+ elif confidence_prob >= CONFIDENCE_MODERATE_BUY:
276
+ # Alocação base para sinais moderados
277
+ # Ex: entre 30% e 60% da alocação máxima permitida por ativo
278
+ alloc_factor = 0.3 + 0.3 * ((confidence_prob - CONFIDENCE_MODERATE_BUY) / (CONFIDENCE_STRONG_BUY - CONFIDENCE_MODERATE_BUY + 1e-6))
279
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
280
+ reason = f"RNN MODERATE BUY signal (Conf: {confidence_prob:.3f})"
281
+ elif confidence_prob >= CONFIDENCE_WEAK_BUY:
282
+ # Alocação pequena para sinais fracos (ou nenhuma)
283
+ alloc_factor = 0.1 + 0.2 * ((confidence_prob - CONFIDENCE_WEAK_BUY) / (CONFIDENCE_MODERATE_BUY - CONFIDENCE_WEAK_BUY + 1e-6))
284
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
285
+ reason = f"RNN WEAK BUY signal (Conf: {confidence_prob:.3f})"
286
+ else:
287
+ logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob:.3f}) abaixo do limiar WEAK_BUY ({CONFIDENCE_WEAK_BUY}). Pulando.")
288
+ continue
289
+
290
+ # Garantir que a alocação não seja menor que a mínima permitida (percentual do total)
291
+ target_usd_allocation = max(target_usd_allocation, amount * MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL)
292
+
293
+ # Garantir que não exceda o capital restante disponível neste CICLO
294
+ capital_left_for_this_cycle = (amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE) - allocated_capital_this_cycle
295
+ actual_usd_allocation = min(target_usd_allocation, capital_left_for_this_cycle)
296
+
297
+ # Garantir que a ordem mínima em USD seja respeitada
298
+ if actual_usd_allocation < MIN_USD_PER_ORDER:
299
+ logger.info(f"BG TASK [{rnn_tx_id}]: Alocação final ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo de ordem ({MIN_USD_PER_ORDER}). Pulando.")
300
+ continue
301
+
302
+ # Adicionar à lista de decisões
303
+ investment_decisions.append({
304
+ "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
305
+ "target_usd_amount": round(actual_usd_allocation, 2),
306
+ "rnn_confidence": round(confidence_prob, 4),
307
+ "reasoning": reason
308
+ })
309
+ allocated_capital_this_cycle += round(actual_usd_allocation, 2)
310
+ logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol}. {reason}")
311
+
312
+ # ... (restante da lógica para signal 0 ou None) ...
313
+ except Exception as e: # Captura exceções da lógica da RNN
314
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
315
+ rnn_analysis_success = False # Marca que a análise RNN falhou
316
+ error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
317
+
318
+
319
+ total_usd_allocated_by_rnn = allocated_capital_this_cycle
320
+
321
+
322
+
323
+
324
+ if not predictor or not predictor.model: # Verifica se o preditor e o modelo interno existem
325
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Instância do preditor RNN não disponível ou modelo interno não carregado. Pulando análise RNN.")
326
+ rnn_analysis_success = False
327
+ error_details += "RNN model/predictor not available for prediction; "
328
+ else:
329
+ try:
330
+ # ... (lógica de iteração sobre `candidate_assets` e chamada a `predictor.predict_for_asset` como na resposta anterior)
331
+ # ... (lógica de alocação de capital como na resposta anterior)
332
+ # Garantir que toda essa lógica está dentro deste bloco 'else'
333
+ crypto_data_for_rnn = market_data_results.get("crypto", {})
334
+ candidate_assets = [
335
+ asset_key for asset_key, data in crypto_data_for_rnn.items()
336
+ if data and not data.get("error") and data.get("ohlcv_1h")
337
+ ]
338
+
339
+ MAX_RISK_PER_ASSET_PCT = 0.05
340
+ MIN_USD_PER_ORDER = 20.00
341
+ MAX_CONCURRENT_POSITIONS = 5
342
+ CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC = 0.85
343
+ CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC = 0.60
344
+ BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL = 0.10
345
+
346
+ allocated_capital_this_cycle = 0.0
347
+
348
+ for asset_key in candidate_assets:
349
+ if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
350
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de posições concorrentes ({MAX_CONCURRENT_POSITIONS}) atingido.")
351
+ break
352
+ if allocated_capital_this_cycle >= amount * 0.90:
353
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital do ciclo atingido.")
354
+ break
355
+
356
+ asset_symbol = asset_key.replace("_", "/")
357
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
358
+
359
+ signal, confidence_prob = await predictor.predict_for_asset(
360
+ crypto_data_for_rnn[asset_key],
361
+ loop=loop
362
+ # window_size e expected_features serão os defaults de rnn_predictor.py
363
+ # ou podem ser passados explicitamente se você quiser variar por ativo
364
+ )
365
+
366
+ if signal == 1:
367
+ if confidence_prob is None or confidence_prob < CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
368
+ logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob}) abaixo do mínimo {CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC}. Pulando.")
369
+ continue
370
+
371
+ confidence_factor = 0.5
372
+ if confidence_prob >= CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC:
373
+ confidence_factor = 1.0
374
+ elif confidence_prob > CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
375
+ confidence_factor = 0.5 + 0.5 * (
376
+ (confidence_prob - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) /
377
+ (CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC)
378
+ )
379
+
380
+ potential_usd_allocation = amount * BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL * confidence_factor
381
+ potential_usd_allocation = min(potential_usd_allocation, amount * MAX_RISK_PER_ASSET_PCT)
382
+ remaining_capital_for_cycle = amount - allocated_capital_this_cycle # Recalcula a cada iteração
383
+ actual_usd_allocation = min(potential_usd_allocation, remaining_capital_for_cycle)
384
+
385
+ if actual_usd_allocation < MIN_USD_PER_ORDER:
386
+ logger.info(f"BG TASK [{rnn_tx_id}]: Alocação calculada ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo ({MIN_USD_PER_ORDER}). Pulando.")
387
+ continue
388
+
389
+ investment_decisions.append({
390
+ "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
391
+ "target_usd_amount": round(actual_usd_allocation, 2),
392
+ "rnn_confidence": round(confidence_prob, 4) if confidence_prob is not None else None,
393
+ "reasoning": f"RNN signal BUY for {asset_symbol} with confidence {confidence_prob:.2f}"
394
+ })
395
+ allocated_capital_this_cycle += round(actual_usd_allocation, 2)
396
+ logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol} (Conf: {confidence_prob:.2f})")
397
+
398
+ elif signal == 0:
399
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN sinal NÃO COMPRAR para {asset_symbol} (Conf: {confidence_prob:.2f if confidence_prob is not None else 'N/A'})")
400
+ else:
401
+ logger.warning(f"BG TASK [{rnn_tx_id}]: RNN não gerou sinal para {asset_symbol}.")
402
+
403
+ if not investment_decisions:
404
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA válidas após avaliação e alocação.")
405
+
406
+ except Exception as e: # Captura exceções da lógica da RNN
407
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
408
+ rnn_analysis_success = False # Marca que a análise RNN falhou
409
+ error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
410
+
411
+ if not rnn_analysis_success: # Se a flag foi setada para False
412
+ final_status = "failed_rnn_analysis"
413
+
414
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
415
+
416
+ total_usd_allocated_by_rnn = allocated_capital_this_cycle
417
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
418
+
419
+
420
+
421
+
422
+
423
+
424
+
425
+
426
+
427
+
428
+ # =========================================================================
429
+ # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens)
430
+ # =========================================================================
431
+ executed_trades_info: List[Dict[str, Any]] = []
432
+ current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo
433
+ cash_remaining_after_execution = amount # Começa com todo o montante
434
+
435
+ if final_status == "completed" and investment_decisions and exchange:
436
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...")
437
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
438
+ order_execution_overall_success = True
439
+
440
+ # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER)
441
+ # Esta seção precisa ser preenchida com:
442
+ # 1. Iterar sobre `investment_decisions`.
443
+ # 2. Para cada decisão de "BUY":
444
+ # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT").
445
+ # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar.
446
+ # `amount_of_asset = target_usd_amount / current_price_of_asset`
447
+ # c. Considerar saldo disponível na exchange (se estiver gerenciando isso).
448
+ # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)`
449
+ # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`.
450
+ # Para ordens limite, a RNN precisaria fornecer o `limit_price`.
451
+ # e. Tratar respostas da exchange (sucesso, falha, ID da ordem).
452
+ # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc.
453
+ # f. Armazenar detalhes da ordem em `executed_trades_info`:
454
+ # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy",
455
+ # "requested_usd_amount": ..., "asset_quantity_ordered": ...,
456
+ # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ...,
457
+ # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... }
458
+ # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida.
459
+ # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`.
460
+ # 3. Para decisões de "SELL" (se sua RNN gerar):
461
+ # a. Verificar se você possui o ativo (requer gerenciamento de portfólio).
462
+ # b. Criar ordem de venda.
463
+ # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`.
464
+
465
+ # Simulação atual:
466
+ for decision in investment_decisions:
467
+ if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO":
468
+ asset_symbol = decision["asset_id"]
469
+ usd_to_spend = decision["target_usd_amount"]
470
+
471
+ # Simular pequena chance de falha na ordem
472
+ if random.random() < 0.05:
473
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.")
474
+ executed_trades_info.append({
475
+ "asset_id": asset_symbol, "status": "failed_simulated",
476
+ "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection"
477
+ })
478
+ order_execution_overall_success = False # Marca que pelo menos uma falhou
479
+ continue # Pula para a próxima decisão
480
+
481
+ # Simular slippage e custo
482
+ simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage
483
+
484
+ # Garantir que não estamos gastando mais do que o caixa restante
485
+ if simulated_cost > cash_remaining_after_execution:
486
+ simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem
487
+ if simulated_cost < 1: # Se não quase nada, não faz a ordem
488
+ logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.")
489
+ continue
490
+
491
+
492
+ if simulated_cost > 0:
493
+ current_portfolio_value += simulated_cost
494
+ cash_remaining_after_execution -= simulated_cost
495
+ executed_trades_info.append({
496
+ "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}",
497
+ "type": "market", "side": "buy",
498
+ "requested_usd_amount": usd_to_spend,
499
+ "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2),
500
+ "timestamp": datetime.utcnow().isoformat()
501
+ })
502
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.")
503
+
504
+ await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1)
505
+
506
+ if not order_execution_overall_success:
507
+ error_details += "One or more orders failed during execution; "
508
+ # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure'
509
+ # final_status = "completed_with_partial_failure" # Exemplo de um novo status
510
+
511
+ elif not exchange and investment_decisions:
512
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.")
513
+ error_details += "Exchange not available for order execution; "
514
+ final_status = "failed_order_execution" # Se a execução é crítica
515
+ cash_remaining_after_execution = amount # Nada foi gasto
516
+
517
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
518
+ transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2)
519
+ transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2)
520
+
521
+
522
+ # =========================================================================
523
+ # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes)
524
+ # =========================================================================
525
+ value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo
526
+
527
+ if final_status == "completed": # Ou "completed_with_partial_failure"
528
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation"
529
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...")
530
+ await asyncio.sleep(random.uniform(3, 7))
531
+
532
+ if current_portfolio_value > 0:
533
+ # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO.
534
+ # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado).
535
+ daily_return_factor = 0.042 # A meta
536
+ simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo)
537
+ # Para ser mais realista, o fator de performance deveria ser algo como:
538
+ # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto)
539
+ # E não diretamente ligado à meta de 4.2%
540
+
541
+ # Ajuste para uma simulação de retorno mais plausível (ainda agressiva)
542
+ # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido
543
+ actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05)
544
+
545
+ profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio
546
+ value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio
547
+ logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, "
548
+ f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}")
549
+ else:
550
+ logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).")
551
+ value_of_investments_at_eod = 0.0
552
+
553
+ # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado
554
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution
555
+
556
+ else: # Se houve falha antes, o valor final é o que sobrou após a falha
557
+ calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial
558
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.")
559
+
560
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2)
561
+ transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2)
562
+
563
+
564
+ # =========================================================================
565
+ # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes)
566
+ # =========================================================================
567
+ if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar
568
+ transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)"
569
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
570
+ # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER)
571
+ # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]`
572
+ # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.)
573
+ # 2. Se for usar blockchain:
574
+ # a. Preparar os dados para um contrato inteligente.
575
+ # b. Interagir com o contrato (ex: web3.py para Ethereum).
576
+ # c. Armazenar o hash da transação da blockchain.
577
+ # 3. Se for um registro interno avançado:
578
+ # a. Assinar digitalmente os dados da transação.
579
+ # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria.
580
+
581
+ # Simulação atual (hash dos dados da transação):
582
+ transaction_data_for_hash = {
583
+ "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount,
584
+ "final_amount_calculated": calculated_final_amount,
585
+ # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante
586
+ "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()),
587
+ "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])),
588
+ "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])),
589
+ "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"),
590
+ "timestamp": datetime.utcnow().isoformat()
591
+ }
592
+ ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True)
593
+ proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest()
594
+
595
+ transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash
596
+ transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof"
597
+ await asyncio.sleep(0.5) # Simula tempo de escrita/hash
598
+ logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...")
599
+
600
+
601
+ # =========================================================================
602
+ # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
603
+ # =========================================================================
604
+ if exchange and hasattr(exchange, 'close'):
605
+ try:
606
+ await exchange.close()
607
+ logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.")
608
+ except Exception as e_close: # Especificar o tipo de exceção se souber
609
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}")
610
+
611
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
612
+ logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.")
613
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical"
614
+ return
615
+
616
+ # Certifique-se que `final_status` reflete o estado real da operação
617
+ # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o
618
+ if error_details and final_status == "completed":
619
+ final_status = "completed_with_warnings" # Ou um status mais apropriado
620
+
621
+ callback_payload_data = InvestmentResultPayload(
622
+ rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id,
623
+ initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais
624
+ profit_loss=round(calculated_final_amount - amount, 2),
625
+ status=final_status, timestamp=datetime.utcnow(),
626
+ details=error_details if error_details else "Investment cycle processed."
627
+ )
628
+ payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada
629
+
630
+ signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest()
631
+ headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
632
+
633
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}")
634
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
635
+ try:
636
+ async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente
637
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers)
638
+ response.raise_for_status()
639
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}")
640
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
641
+ except httpx.RequestError as e_req:
642
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}")
643
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error"
644
+ except httpx.HTTPStatusError as e_http:
645
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP do AIBank ao receber callback: {e_http.response.status_code} - {e_http.response.text[:200]}")
646
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}"
647
+ except Exception as e_cb_final:
648
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True)
649
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
650
+
651
+
652
+ import asyncio
653
+ import random
654
+
655
+
656
+
657
+ # --- Endpoints da API ---
658
+ @app.post("/api/invest",
659
+ response_model=InvestmentResponse,
660
+ dependencies=[Depends(verify_aibank_key)])
661
+ async def initiate_investment(
662
+ request_data: InvestmentRequest,
663
+ background_tasks: BackgroundTasks
664
+ ):
665
+ """
666
+ Endpoint para o AIBank iniciar um ciclo de investimento.
667
+ Responde rapidamente e executa a lógica pesada em background.
668
+ """
669
+ logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
670
+ f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
671
+
672
+ rnn_tx_id = str(uuid.uuid4())
673
+
674
+ # Armazena informações iniciais da transação DB real para ser mais robusto
675
+ transactions_db[rnn_tx_id] = {
676
+ "rnn_transaction_id": rnn_tx_id,
677
+ "aibank_transaction_token": request_data.aibank_transaction_token,
678
+ "client_id": request_data.client_id,
679
+ "initial_amount": request_data.amount,
680
+ "status": "pending_background_processing",
681
+ "received_at": datetime.utcnow().isoformat(),
682
+ "callback_status": "not_sent_yet"
683
+ }
684
+
685
+ # Adiciona a tarefa de longa duração ao background
686
+ background_tasks.add_task(
687
+ execute_investment_strategy_background,
688
+ rnn_tx_id,
689
+ request_data.client_id,
690
+ request_data.amount,
691
+ request_data.aibank_transaction_token
692
+ )
693
+
694
+ logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
695
+ return InvestmentResponse(
696
+ status="pending",
697
+ message="Investment request received and is being processed in the background. Await callback for results.",
698
+ rnn_transaction_id=rnn_tx_id
699
+ )
700
+
701
+ @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
702
+ async def get_transaction_status(rnn_tx_id: str):
703
+ """ Endpoint para verificar o status de uma transação (para debug/admin) """
704
+ transaction = transactions_db.get(rnn_tx_id)
705
+ if not transaction:
706
+ raise HTTPException(status_code=404, detail="Transaction not found")
707
+ return transaction
708
+
709
+
710
+ # --- Dashboard (Existente, adaptado) ---
711
+ # Setup para arquivos estáticos e templates
712
+
713
+ try:
714
+ app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
715
+ templates = Environment(loader=FileSystemLoader("rnn/templates"))
716
+ except RuntimeError as e:
717
+ logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
718
+ templates = None # Para evitar erros se o loader falhar
719
+
720
+ @app.get("/", response_class=HTMLResponse)
721
+ async def index(request: Request):
722
+ if not templates:
723
+ return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
724
+
725
+ agora = datetime.now()
726
+ agentes_simulados = [
727
+ # dados de agentes ...
728
+ ]
729
+ template = templates.get_template("index.html")
730
+ # Adicionar transações recentes ao contexto do template
731
+ recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
732
+ return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
733
+
734
+ # --- Imports para Background Task ---
735
+ import asyncio
736
+ import random
737
+
738
+ # Função de logger dummy
739
+ # class DummyLogger:
740
+ # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
741
+ # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
742
+ # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
743
+
744
+ # if __name__ == "__main__": # Para teste local
745
+ # # logger = DummyLogger() # se não tiver get_logger()
746
+ # # Configuração das variáveis de ambiente para teste local
747
+ # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
748
+ # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
749
+ # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
750
+ # # import uvicorn
751
  # # uvicorn.run(app, host="0.0.0.0", port=8000)