1→import Foundation 2→ 3→// MARK: - API Errors 4→ 5→enum BCBAPIError: LocalizedError { 6→ case invalidURL 7→ case networkError(Error) 8→ case decodingError(Error) 9→ case serverError(Int) 10→ case noData 11→ case rateLimited 12→ 13→ var errorDescription: String? { 14→ switch self { 15→ case .invalidURL: 16→ return "URL inválida" 17→ case .networkError(let error): 18→ return "Erro de rede: \(error.localizedDescription)" 19→ case .decodingError(let error): 20→ return "Erro ao processar dados: \(error.localizedDescription)" 21→ case .serverError(let code): 22→ return "Erro do servidor: \(code)" 23→ case .noData: 24→ return "Nenhum dado disponível" 25→ case .rateLimited: 26→ return "Limite de requisições excedido" 27→ } 28→ } 29→} 30→ 31→// MARK: - API Client 32→ 33→@MainActor 34→class BCBAPIClient: ObservableObject { 35→ static let shared = BCBAPIClient() 36→ 37→ @Published var isLoading = false 38→ @Published var lastError: BCBAPIError? 39→ 40→ private let session: URLSession 41→ private let decoder: JSONDecoder 42→ 43→ // Base URLs 44→ enum BaseURL { 45→ static let olinda = "https://olinda.bcb.gov.br/olinda/servico" 46→ static let sgs = "https://api.bcb.gov.br/dados/serie/bcdata.sgs" 47→ } 48→ 49→ private init() { 50→ let config = URLSessionConfiguration.default 51→ config.timeoutIntervalForRequest = 30 52→ config.timeoutIntervalForResource = 60 53→ self.session = URLSession(configuration: config) 54→ 55→ self.decoder = JSONDecoder() 56→ } 57→ 58→ // MARK: - Generic Fetch 59→ 60→ func fetch(url: URL) async throws -> T { 61→ isLoading = true 62→ defer { isLoading = false } 63→ 64→ do { 65→ let (data, response) = try await session.data(from: url) 66→ 67→ guard let httpResponse = response as? HTTPURLResponse else { 68→ throw BCBAPIError.networkError(URLError(.badServerResponse)) 69→ } 70→ 71→ guard (200...299).contains(httpResponse.statusCode) else { 72→ if httpResponse.statusCode == 429 { 73→ throw BCBAPIError.rateLimited 74→ } 75→ throw BCBAPIError.serverError(httpResponse.statusCode) 76→ } 77→ 78→ let decoded = try decoder.decode(T.self, from: data) 79→ lastError = nil 80→ return decoded 81→ 82→ } catch let error as BCBAPIError { 83→ lastError = error 84→ throw error 85→ } catch let error as DecodingError { 86→ let apiError = BCBAPIError.decodingError(error) 87→ lastError = apiError 88→ throw apiError 89→ } catch { 90→ let apiError = BCBAPIError.networkError(error) 91→ lastError = apiError 92→ throw apiError 93→ } 94→ } 95→ 96→ // MARK: - PTAX API 97→ 98→ func fetchCurrencies() async throws -> [CurrencyInfo] { 99→ let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/Moedas?$format=json" 100→ guard let url = URL(string: urlString) else { 101→ throw BCBAPIError.invalidURL 102→ } 103→ 104→ let response: PTAXResponse = try await fetch(url: url) 105→ return response.value 106→ } 107→ 108→ func fetchDollarQuote(date: Date) async throws -> [DollarQuote] { 109→ let dateString = date.toBCBFormat() 110→ let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarDia(dataCotacao=@dataCotacao)?@dataCotacao='\(dateString)'&$format=json" 111→ guard let url = URL(string: urlString) else { 112→ throw BCBAPIError.invalidURL 113→ } 114→ 115→ let response: PTAXResponse = try await fetch(url: url) 116→ return response.value 117→ } 118→ 119→ func fetchDollarQuotePeriod(startDate: Date, endDate: Date) async throws -> [DollarQuote] { 120→ let startString = startDate.toBCBFormat() 121→ let endString = endDate.toBCBFormat() 122→ let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)?@dataInicial='\(startString)'&@dataFinalCotacao='\(endString)'&$format=json" 123→ guard let url = URL(string: urlString) else { 124→ throw BCBAPIError.invalidURL 125→ } 126→ 127→ let response: PTAXResponse = try await fetch(url: url) 128→ return response.value 129→ } 130→ 131→ func fetchCurrencyQuote(symbol: String, date: Date) async throws -> [CurrencyQuote] { 132→ let dateString = date.toBCBFormat() 133→ let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoMoedaDia(moeda=@moeda,dataCotacao=@dataCotacao)?@moeda='\(symbol)'&@dataCotacao='\(dateString)'&$format=json" 134→ guard let url = URL(string: urlString) else { 135→ throw BCBAPIError.invalidURL 136→ } 137→ 138→ let response: PTAXResponse = try await fetch(url: url) 139→ return response.value 140→ } 141→ 142→ // MARK: - SGS API 143→ 144→ func fetchTimeSeries(code: Int, lastN: Int = 10) async throws -> [TimeSeriesValue] { 145→ let urlString = "\(BaseURL.sgs).\(code)/dados/ultimos/\(lastN)?formato=json" 146→ guard let url = URL(string: urlString) else { 147→ throw BCBAPIError.invalidURL 148→ } 149→ 150→ return try await fetch(url: url) 151→ } 152→ 153→ func fetchTimeSeriesPeriod(code: Int, startDate: Date, endDate: Date) async throws -> [TimeSeriesValue] { 154→ let formatter = DateFormatter() 155→ formatter.dateFormat = "dd/MM/yyyy" 156→ let startString = formatter.string(from: startDate) 157→ let endString = formatter.string(from: endDate) 158→ 159→ let urlString = "\(BaseURL.sgs).\(code)/dados?formato=json&dataInicial=\(startString)&dataFinal=\(endString)" 160→ guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) else { 161→ throw BCBAPIError.invalidURL 162→ } 163→ 164→ return try await fetch(url: url) 165→ } 166→ 167→ // MARK: - Institutions API (New Working Endpoints) 168→ 169→ /// Fetch all banks (Bancos Múltiplos, Comerciais, etc.) 170→ func fetchBanks() async throws -> [Bank] { 171→ let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesBancoComMultCE?$format=json" 172→ guard let url = URL(string: urlString) else { 173→ throw BCBAPIError.invalidURL 174→ } 175→ 176→ let response: BanksResponse = try await fetch(url: url) 177→ return response.value 178→ } 179→ 180→ /// Fetch all credit cooperatives 181→ func fetchCooperatives() async throws -> [Cooperative] { 182→ let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesCooperativas?$format=json" 183→ guard let url = URL(string: urlString) else { 184→ throw BCBAPIError.invalidURL 185→ } 186→ 187→ let response: CooperativesResponse = try await fetch(url: url) 188→ return response.value 189→ } 190→ 191→ /// Fetch all consortium administrators 192→ func fetchConsortia() async throws -> [Consortium] { 193→ let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesConsorcios?$format=json" 194→ guard let url = URL(string: urlString) else { 195→ throw BCBAPIError.invalidURL 196→ } 197→ 198→ let response: ConsortiaResponse = try await fetch(url: url) 199→ return response.value 200→ } 201→ 202→ /// Fetch all sociedades (brokers, SCDs, payment institutions, etc.) 203→ func fetchSociedades() async throws -> [Sociedade] { 204→ let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesSociedades?$format=json" 205→ guard let url = URL(string: urlString) else { 206→ throw BCBAPIError.invalidURL 207→ } 208→ 209→ let response: SociedadesResponse = try await fetch(url: url) 210→ return response.value 211→ } 212→ 213→ /// Fetch all institution types in parallel 214→ func fetchAllInstitutions() async throws -> (banks: [Bank], cooperatives: [Cooperative], consortia: [Consortium], sociedades: [Sociedade]) { 215→ async let banksTask = fetchBanks() 216→ async let cooperativesTask = fetchCooperatives() 217→ async let consortiaTask = fetchConsortia() 218→ async let sociedadesTask = fetchSociedades() 219→ 220→ let (banks, cooperatives, consortia, sociedades) = try await (banksTask, cooperativesTask, consortiaTask, sociedadesTask) 221→ return (banks, cooperatives, consortia, sociedades) 222→ } 223→ 224→ // Legacy - kept for compatibility 225→ func fetchInstitutions(top: Int = 100) async throws -> [Institution] { 226→ // Return empty array - use new specific methods instead 227→ return [] 228→ } 229→ 230→ func searchInstitutions(query: String) async throws -> [Institution] { 231→ // Return empty array - use new specific methods instead 232→ return [] 233→ } 234→ 235→ // MARK: - PIX SPI API 236→ 237→ /// Fetch daily PIX settlement data 238→ func fetchPIXDaily() async throws -> [PIXDaily] { 239→ let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosAtual?$format=json" 240→ guard let url = URL(string: urlString) else { 241→ throw BCBAPIError.invalidURL 242→ } 243→ 244→ let response: PIXDailyResponse = try await fetch(url: url) 245→ return response.value 246→ } 247→ 248→ /// Fetch hourly PIX averages 249→ func fetchPIXIntraday() async throws -> [PIXIntraday] { 250→ let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosIntradia?$format=json" 251→ guard let url = URL(string: urlString) else { 252→ throw BCBAPIError.invalidURL 253→ } 254→ 255→ let response: PIXIntradayResponse = try await fetch(url: url) 256→ return response.value 257→ } 258→ 259→ /// Fetch PIX availability index 260→ func fetchPIXAvailability() async throws -> [PIXAvailability] { 261→ let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixDisponibilidadeSPI?$format=json&$orderby=DataBase%20desc&$top=12" 262→ guard let url = URL(string: urlString) else { 263→ throw BCBAPIError.invalidURL 264→ } 265→ 266→ let response: PIXAvailabilityResponse = try await fetch(url: url) 267→ return response.value 268→ } 269→ 270→ /// Fetch PIX account remuneration 271→ func fetchPIXRemuneration() async throws -> [PIXRemuneration] { 272→ let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixRemuneracaoContaPI?$format=json&$orderby=DataBase%20desc&$top=30" 273→ guard let url = URL(string: urlString) else { 274→ throw BCBAPIError.invalidURL 275→ } 276→ 277→ let response: PIXRemunerationResponse = try await fetch(url: url) 278→ return response.value 279→ } 280→ 281→ /// Fetch all PIX statistics (with graceful error handling for unstable endpoints) 282→ func fetchAllPIXStats() async throws -> PIXStats { 283→ // These endpoints are more stable 284→ async let dailyTask = fetchPIXDaily() 285→ async let intradayTask = fetchPIXIntraday() 286→ 287→ // Get the stable data first 288→ let (daily, intraday) = try await (dailyTask, intradayTask) 289→ 290→ // Try to get availability and remuneration data, but don't fail if they error 291→ var availability: [PIXAvailability] = [] 292→ var remuneration: [PIXRemuneration] = [] 293→ 294→ // Availability endpoint (may return 500 - BCB server issue) 295→ do { 296→ availability = try await fetchPIXAvailability() 297→ } catch { 298→ // Silently handle - BCB endpoint is known to be unstable 299→ } 300→ 301→ // Remuneration endpoint (may return 500 - BCB server issue) 302→ do { 303→ remuneration = try await fetchPIXRemuneration() 304→ } catch { 305→ // Silently handle - BCB endpoint is known to be unstable 306→ } 307→ 308→ return PIXStats( 309→ dailyData: daily, 310→ intradayData: intraday, 311→ availabilityData: availability, 312→ remunerationData: remuneration 313→ ) 314→ } 315→ 316→ // MARK: - Expectations API (Focus) 317→ 318→ /// Fetch SELIC expectations by COPOM meeting 319→ func fetchSelicExpectationsByMeeting() async throws -> [SelicExpectation] { 320→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoSelic?$format=json&$top=100&$orderby=Data%20desc,Reuniao%20desc" 321→ guard let url = URL(string: urlString) else { 322→ throw BCBAPIError.invalidURL 323→ } 324→ 325→ let response: SelicExpectationResponse = try await fetch(url: url) 326→ return response.value 327→ } 328→ 329→ /// Fetch IPCA 12-month inflation expectations 330→ func fetchIPCA12MonthExpectations() async throws -> [InflationExpectation] { 331→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc" 332→ guard let url = URL(string: urlString) else { 333→ throw BCBAPIError.invalidURL 334→ } 335→ 336→ let response: InflationExpectationResponse = try await fetch(url: url) 337→ return response.value 338→ } 339→ 340→ /// Fetch IGP-M 12-month inflation expectations 341→ func fetchIGPM12MonthExpectations() async throws -> [InflationExpectation] { 342→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc" 343→ guard let url = URL(string: urlString) else { 344→ throw BCBAPIError.invalidURL 345→ } 346→ 347→ let response: InflationExpectationResponse = try await fetch(url: url) 348→ return response.value 349→ } 350→ 351→ /// Fetch annual IGP-M expectations 352→ func fetchAnnualIGPMExpectations() async throws -> [AnnualExpectation] { 353→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 354→ guard let url = URL(string: urlString) else { 355→ throw BCBAPIError.invalidURL 356→ } 357→ 358→ let response: AnnualExpectationResponse = try await fetch(url: url) 359→ return response.value 360→ } 361→ 362→ /// Fetch annual IPCA expectations 363→ func fetchAnnualIPCAExpectations() async throws -> [AnnualExpectation] { 364→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 365→ guard let url = URL(string: urlString) else { 366→ throw BCBAPIError.invalidURL 367→ } 368→ 369→ let response: AnnualExpectationResponse = try await fetch(url: url) 370→ return response.value 371→ } 372→ 373→ /// Fetch annual PIB expectations 374→ func fetchAnnualPIBExpectations() async throws -> [AnnualExpectation] { 375→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'PIB%20Total'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 376→ guard let url = URL(string: urlString) else { 377→ throw BCBAPIError.invalidURL 378→ } 379→ 380→ let response: AnnualExpectationResponse = try await fetch(url: url) 381→ return response.value 382→ } 383→ 384→ /// Fetch annual Câmbio expectations 385→ func fetchAnnualCambioExpectations() async throws -> [AnnualExpectation] { 386→ // Note: Câmbio must be URL encoded as C%C3%A2mbio 387→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'C%C3%A2mbio'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 388→ guard let url = URL(string: urlString) else { 389→ throw BCBAPIError.invalidURL 390→ } 391→ 392→ let response: AnnualExpectationResponse = try await fetch(url: url) 393→ return response.value 394→ } 395→ 396→ /// Fetch all Focus stats in parallel 397→ func fetchAllFocusStats() async throws -> FocusStats { 398→ async let selicTask = fetchSelicExpectationsByMeeting() 399→ async let ipcaTask = fetchIPCA12MonthExpectations() 400→ async let igpmTask = fetchIGPM12MonthExpectations() 401→ async let annualIPCATask = fetchAnnualIPCAExpectations() 402→ async let annualIGPMTask = fetchAnnualIGPMExpectations() 403→ async let annualPIBTask = fetchAnnualPIBExpectations() 404→ async let annualCambioTask = fetchAnnualCambioExpectations() 405→ 406→ let (selic, ipca, igpm, annualIPCA, annualIGPM, annualPIB, annualCambio) = try await (selicTask, ipcaTask, igpmTask, annualIPCATask, annualIGPMTask, annualPIBTask, annualCambioTask) 407→ 408→ return FocusStats( 409→ selicExpectations: selic, 410→ ipcaExpectations: ipca, 411→ igpmExpectations: igpm, 412→ annualIPCA: annualIPCA, 413→ annualIGPM: annualIGPM, 414→ annualPIB: annualPIB, 415→ annualCambio: annualCambio 416→ ) 417→ } 418→ 419→ // Legacy compatibility methods 420→ func fetchInflationExpectations() async throws -> [MarketExpectation] { 421→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 422→ guard let url = URL(string: urlString) else { 423→ throw BCBAPIError.invalidURL 424→ } 425→ 426→ let response: ExpectationsResponse = try await fetch(url: url) 427→ return response.value 428→ } 429→ 430→ func fetchSelicExpectations() async throws -> [MarketExpectation] { 431→ let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'Selic'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc" 432→ guard let url = URL(string: urlString) else { 433→ throw BCBAPIError.invalidURL 434→ } 435→ 436→ let response: ExpectationsResponse = try await fetch(url: url) 437→ return response.value 438→ } 439→} 440→ 441→// MARK: - Cache Manager 442→ 443→@MainActor 444→class CacheManager: ObservableObject { 445→ static let shared = CacheManager() 446→ 447→ private let defaults = UserDefaults.standard 448→ private let encoder = JSONEncoder() 449→ private let decoder = JSONDecoder() 450→ 451→ private let cacheExpirationMinutes: Double = 5 452→ 453→ private init() {} 454→ 455→ func save(_ data: T, forKey key: String) { 456→ if let encoded = try? encoder.encode(data) { 457→ defaults.set(encoded, forKey: key) 458→ defaults.set(Date(), forKey: "\(key)_timestamp") 459→ } 460→ } 461→ 462→ func load(forKey key: String) -> T? { 463→ guard let data = defaults.data(forKey: key), 464→ let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else { 465→ return nil 466→ } 467→ 468→ // Check if cache is expired 469→ let elapsed = Date().timeIntervalSince(timestamp) / 60 470→ if elapsed > cacheExpirationMinutes { 471→ return nil 472→ } 473→ 474→ return try? decoder.decode(T.self, from: data) 475→ } 476→ 477→ func clearAll() { 478→ let domain = Bundle.main.bundleIdentifier ?? "" 479→ defaults.removePersistentDomain(forName: domain) 480→ } 481→ 482→ func isCacheValid(forKey key: String) -> Bool { 483→ guard let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else { 484→ return false 485→ } 486→ let elapsed = Date().timeIntervalSince(timestamp) / 60 487→ return elapsed <= cacheExpirationMinutes 488→ } 489→} 490→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.