alpha-arena/macos-app/AlphaArena/Services/WebSocketClient.swift
oho 774f8b3f61 Initial commit: Alpha Arena - Polymarket Autonomous Trading Harness
A comprehensive autonomous trading system for Polymarket prediction markets
featuring multi-LLM provider support, a native macOS menu bar app, and a
web-based control dashboard.

Key features:
- Multi-agent trading system (Research, Risk, Execution, Reflection agents)
- LLM provider flexibility (Anthropic, OpenAI, Google, xAI, Local models)
- Automatic provider fallback chain for resilience
- Native Swift/SwiftUI macOS menu bar application
- FastAPI web dashboard with real-time WebSocket updates
- Risk management with kill switch
- Technical indicators and market analysis
2026-01-12 23:04:58 +01:00

244 lines
7.1 KiB
Swift

import Foundation
import Combine
/// WebSocket client for real-time updates from Alpha Arena
class WebSocketClient: NSObject, ObservableObject {
@Published var isConnected = false
@Published var latestPnL: Double = 0
@Published var dailyPnL: Double = 0
@Published var openPositions: Int = 0
@Published var isTradingActive: Bool = false
@Published var lastMessage: String = ""
private var webSocketTask: URLSessionWebSocketTask?
private var session: URLSession!
private var url: URL
private var reconnectTimer: Timer?
private var pingTimer: Timer?
private let settings: AppSettings
private var shouldReconnect = true
private var reconnectAttempts = 0
private let maxReconnectAttempts = 5
private let reconnectDelay: TimeInterval = 3.0
init(settings: AppSettings = .shared) {
self.settings = settings
self.url = URL(string: "ws://127.0.0.1:8000/ws")!
super.init()
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
}
/// Update WebSocket URL when settings change
func updateURL() {
let host = settings.networkMode.rawValue
let port = settings.serverPort
self.url = URL(string: "ws://\(host):\(port)/ws")!
}
/// Connect to WebSocket
func connect() {
guard !isConnected else { return }
shouldReconnect = true
reconnectAttempts = 0
establishConnection()
}
/// Disconnect from WebSocket
func disconnect() {
shouldReconnect = false
reconnectTimer?.invalidate()
reconnectTimer = nil
pingTimer?.invalidate()
pingTimer = nil
webSocketTask?.cancel(with: .goingAway, reason: nil)
webSocketTask = nil
DispatchQueue.main.async {
self.isConnected = false
}
}
// MARK: - Private Methods
private func establishConnection() {
webSocketTask?.cancel()
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
receiveMessage()
startPingTimer()
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let message):
self.handleMessage(message)
self.receiveMessage() // Continue listening
case .failure(let error):
print("WebSocket receive error: \(error)")
self.handleDisconnect()
}
}
}
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
switch message {
case .string(let text):
parseMessage(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
parseMessage(text)
}
@unknown default:
break
}
}
private func parseMessage(_ text: String) {
DispatchQueue.main.async {
self.lastMessage = text
}
guard let data = text.data(using: .utf8) else { return }
do {
let decoder = JSONDecoder()
let message = try decoder.decode(WebSocketMessage.self, from: data)
DispatchQueue.main.async {
self.handleParsedMessage(message)
}
} catch {
print("Failed to parse WebSocket message: \(error)")
}
}
private func handleParsedMessage(_ message: WebSocketMessage) {
guard let type = WebSocketMessageType(rawValue: message.type) else {
print("Unknown message type: \(message.type)")
return
}
switch type {
case .`init`:
// Initial connection state
if let data = message.data {
if let pnl = data.pnl {
latestPnL = pnl
}
if let positions = data.positions {
openPositions = positions
}
}
isConnected = true
reconnectAttempts = 0
case .tradingStarted:
isTradingActive = true
if let mode = message.data?.mode {
print("Trading started in \(mode) mode")
}
case .tradingStopped:
isTradingActive = false
case .killSwitch:
isTradingActive = false
if let reason = message.data?.reason {
print("Kill switch triggered: \(reason)")
}
case .cycleComplete:
if let data = message.data {
if let pnl = data.pnl {
latestPnL = pnl
}
if let positions = data.positions {
openPositions = positions
}
}
case .positionUpdate:
if let positions = message.data?.positions {
openPositions = positions
}
case .error:
if let errorMessage = message.data?.message {
print("WebSocket error: \(errorMessage)")
}
}
}
private func handleDisconnect() {
DispatchQueue.main.async {
self.isConnected = false
}
pingTimer?.invalidate()
pingTimer = nil
guard shouldReconnect else { return }
guard reconnectAttempts < maxReconnectAttempts else {
print("Max reconnect attempts reached")
return
}
reconnectAttempts += 1
let delay = reconnectDelay * Double(reconnectAttempts)
print("Attempting to reconnect in \(delay) seconds (attempt \(reconnectAttempts)/\(maxReconnectAttempts))")
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.establishConnection()
}
}
private func startPingTimer() {
pingTimer?.invalidate()
pingTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
self?.sendPing()
}
}
private func sendPing() {
webSocketTask?.sendPing { [weak self] error in
if let error = error {
print("Ping failed: \(error)")
self?.handleDisconnect()
}
}
}
}
// MARK: - URLSessionWebSocketDelegate
extension WebSocketClient: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
DispatchQueue.main.async {
self.isConnected = true
self.reconnectAttempts = 0
}
print("WebSocket connected")
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
DispatchQueue.main.async {
self.isConnected = false
}
if let reason = reason, let reasonString = String(data: reason, encoding: .utf8) {
print("WebSocket closed: \(reasonString)")
}
if shouldReconnect {
handleDisconnect()
}
}
}