mirror of
https://github.com/saymrwulf/alpha-arena.git
synced 2026-07-22 19:11:33 +00:00
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
511 lines
17 KiB
Swift
511 lines
17 KiB
Swift
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
/// Preferences window view
|
|
struct PreferencesView: View {
|
|
@ObservedObject var settings = AppSettings.shared
|
|
@StateObject private var llmState = LLMProviderState()
|
|
@State private var showingPathPicker = false
|
|
|
|
private let apiClient = APIClient()
|
|
|
|
var body: some View {
|
|
TabView {
|
|
generalTab
|
|
.tabItem {
|
|
Label("General", systemImage: "gear")
|
|
}
|
|
|
|
llmProvidersTab
|
|
.tabItem {
|
|
Label("LLM", systemImage: "brain")
|
|
}
|
|
|
|
networkTab
|
|
.tabItem {
|
|
Label("Network", systemImage: "network")
|
|
}
|
|
|
|
notificationsTab
|
|
.tabItem {
|
|
Label("Notifications", systemImage: "bell")
|
|
}
|
|
|
|
aboutTab
|
|
.tabItem {
|
|
Label("About", systemImage: "info.circle")
|
|
}
|
|
}
|
|
.frame(width: 500, height: 380)
|
|
.task {
|
|
await fetchLLMState()
|
|
}
|
|
}
|
|
|
|
// MARK: - General Tab
|
|
|
|
private var generalTab: some View {
|
|
Form {
|
|
Section {
|
|
HStack {
|
|
TextField("Project Path", text: $settings.projectPath)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
Button("Browse...") {
|
|
showingPathPicker = true
|
|
}
|
|
}
|
|
.fileImporter(
|
|
isPresented: $showingPathPicker,
|
|
allowedContentTypes: [.folder]
|
|
) { result in
|
|
if case .success(let url) = result {
|
|
settings.projectPath = url.path
|
|
settings.saveProjectPath()
|
|
}
|
|
}
|
|
|
|
Text("Path to the Alpha Arena project directory containing the 'alpha' script.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} header: {
|
|
Text("Project Location")
|
|
}
|
|
|
|
Section {
|
|
Picker("Default Port", selection: $settings.serverPort) {
|
|
Text("8000").tag(8000)
|
|
Text("8080").tag(8080)
|
|
Text("3000").tag(3000)
|
|
}
|
|
.pickerStyle(.menu)
|
|
} header: {
|
|
Text("Server")
|
|
}
|
|
|
|
Section {
|
|
Toggle("Auto-connect WebSocket", isOn: $settings.autoConnectWebSocket)
|
|
Text("Automatically connect to the WebSocket for real-time updates when the server starts.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} header: {
|
|
Text("Connection")
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - LLM Providers Tab
|
|
|
|
private var llmProvidersTab: some View {
|
|
Form {
|
|
Section {
|
|
if !llmState.isInitialized {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.font(.title)
|
|
.foregroundColor(.orange)
|
|
Text("Provider registry not initialized")
|
|
.font(.headline)
|
|
Text("Start the server and click Initialize to connect to LLM providers.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
Button("Initialize Providers") {
|
|
Task { await initializeProviders() }
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(llmState.isInitializing)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
} else {
|
|
HStack {
|
|
Text("Active Provider:")
|
|
.foregroundColor(.secondary)
|
|
Text(llmState.activeProvider?.displayName ?? "None")
|
|
.fontWeight(.semibold)
|
|
Spacer()
|
|
Button("Refresh") {
|
|
Task { await fetchLLMState() }
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Status")
|
|
}
|
|
|
|
if llmState.isInitialized {
|
|
Section {
|
|
ForEach(LLMProviderType.allCases) { provider in
|
|
ProviderRow(
|
|
provider: provider,
|
|
isActive: llmState.activeProvider == provider,
|
|
isConnected: llmState.connectedProviders.contains(provider),
|
|
healthStatus: llmState.getHealthStatus(for: provider)
|
|
) {
|
|
Task { await setActiveProvider(provider) }
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Providers")
|
|
}
|
|
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("If the active provider fails, the system will try providers in this order:")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
ForEach(Array(llmState.fallbackOrder.enumerated()), id: \.element) { index, provider in
|
|
HStack {
|
|
Text("\(index + 1).")
|
|
.foregroundColor(.secondary)
|
|
.frame(width: 20)
|
|
Text(provider.displayName)
|
|
Spacer()
|
|
if index > 0 {
|
|
Button("↑") {
|
|
moveFallback(index: index, direction: -1)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
if index < llmState.fallbackOrder.count - 1 {
|
|
Button("↓") {
|
|
moveFallback(index: index, direction: 1)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
|
|
Button("Save Fallback Order") {
|
|
Task { await saveFallbackOrder() }
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.padding(.top, 8)
|
|
}
|
|
} header: {
|
|
Text("Fallback Order")
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - LLM Helper Methods
|
|
|
|
private func fetchLLMState() async {
|
|
do {
|
|
let response = try await apiClient.getLLMProviders()
|
|
await MainActor.run {
|
|
llmState.update(from: response)
|
|
}
|
|
} catch {
|
|
// Server might not be running
|
|
}
|
|
}
|
|
|
|
private func initializeProviders() async {
|
|
await MainActor.run {
|
|
llmState.isInitializing = true
|
|
}
|
|
do {
|
|
let response = try await apiClient.initializeLLMProviders()
|
|
await MainActor.run {
|
|
llmState.isInitialized = true
|
|
if let active = response.activeProvider {
|
|
llmState.activeProvider = LLMProviderType(rawValue: active)
|
|
}
|
|
if let connected = response.connectedProviders {
|
|
llmState.connectedProviders = Set(connected.compactMap { LLMProviderType(rawValue: $0) })
|
|
}
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
llmState.errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
await MainActor.run {
|
|
llmState.isInitializing = false
|
|
}
|
|
}
|
|
|
|
private func setActiveProvider(_ provider: LLMProviderType) async {
|
|
guard llmState.connectedProviders.contains(provider) else { return }
|
|
do {
|
|
try await apiClient.setActiveProvider(provider.rawValue)
|
|
await MainActor.run {
|
|
llmState.activeProvider = provider
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
llmState.errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
private func moveFallback(index: Int, direction: Int) {
|
|
let newIndex = index + direction
|
|
guard newIndex >= 0 && newIndex < llmState.fallbackOrder.count else { return }
|
|
llmState.fallbackOrder.swapAt(index, newIndex)
|
|
}
|
|
|
|
private func saveFallbackOrder() async {
|
|
do {
|
|
let order = llmState.fallbackOrder.map { $0.rawValue }
|
|
try await apiClient.setFallbackOrder(order)
|
|
} catch {
|
|
await MainActor.run {
|
|
llmState.errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Network Tab
|
|
|
|
private var networkTab: some View {
|
|
Form {
|
|
Section {
|
|
Picker("Default Network Mode", selection: $settings.networkMode) {
|
|
ForEach(NetworkMode.allCases, id: \.rawValue) { mode in
|
|
VStack(alignment: .leading) {
|
|
Text(mode.displayName)
|
|
Text(mode.description)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.tag(mode)
|
|
}
|
|
}
|
|
.pickerStyle(.radioGroup)
|
|
} header: {
|
|
Text("Network Mode")
|
|
}
|
|
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Label {
|
|
VStack(alignment: .leading) {
|
|
Text("Localhost Only (127.0.0.1)")
|
|
.font(.headline)
|
|
Text("Server is only accessible from this machine. More secure.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
} icon: {
|
|
Image(systemName: "lock.shield")
|
|
.foregroundColor(.green)
|
|
}
|
|
|
|
Label {
|
|
VStack(alignment: .leading) {
|
|
Text("LAN Access (0.0.0.0)")
|
|
.font(.headline)
|
|
Text("Server is accessible from other devices on your local network.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
} icon: {
|
|
Image(systemName: "network")
|
|
.foregroundColor(.blue)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("About Network Modes")
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - Notifications Tab
|
|
|
|
private var notificationsTab: some View {
|
|
Form {
|
|
Section {
|
|
Toggle("Show Notifications", isOn: $settings.showNotifications)
|
|
Text("Show macOS notifications for server status changes and trading events.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} header: {
|
|
Text("Notifications")
|
|
}
|
|
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
NotificationExample(
|
|
title: "Alpha Arena Started",
|
|
message: "Server is running at http://127.0.0.1:8000"
|
|
)
|
|
|
|
NotificationExample(
|
|
title: "Kill Switch Activated",
|
|
message: "Trading stopped due to loss limit"
|
|
)
|
|
}
|
|
} header: {
|
|
Text("Example Notifications")
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - About Tab
|
|
|
|
private var aboutTab: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "chart.line.uptrend.xyaxis.circle.fill")
|
|
.font(.system(size: 64))
|
|
.foregroundStyle(.green, .green.opacity(0.3))
|
|
|
|
Text("Alpha Arena")
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
|
|
Text("Version 1.0.0")
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("Menu bar controller for Alpha Arena trading system")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 20) {
|
|
Button("View Logs") {
|
|
openLogs()
|
|
}
|
|
|
|
Button("Open Dashboard") {
|
|
openDashboard()
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func openLogs() {
|
|
let projectPath = settings.projectPath
|
|
guard !projectPath.isEmpty else { return }
|
|
|
|
let script = """
|
|
tell application "Terminal"
|
|
do script "cd '\(projectPath)' && ./alpha logs"
|
|
activate
|
|
end tell
|
|
"""
|
|
|
|
var error: NSDictionary?
|
|
if let scriptObject = NSAppleScript(source: script) {
|
|
scriptObject.executeAndReturnError(&error)
|
|
}
|
|
}
|
|
|
|
private func openDashboard() {
|
|
let host = settings.networkMode == .lan ? (getLocalIPAddress() ?? "127.0.0.1") : "127.0.0.1"
|
|
if let url = URL(string: "http://\(host):\(settings.serverPort)") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Notification Example View
|
|
|
|
struct NotificationExample: View {
|
|
let title: String
|
|
let message: String
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
Image(systemName: "app.badge")
|
|
.font(.title2)
|
|
.foregroundColor(.accentColor)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(title)
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
Text(message)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(8)
|
|
.background(Color(NSColor.controlBackgroundColor))
|
|
.cornerRadius(8)
|
|
}
|
|
}
|
|
|
|
// MARK: - Provider Row
|
|
|
|
struct ProviderRow: View {
|
|
let provider: LLMProviderType
|
|
let isActive: Bool
|
|
let isConnected: Bool
|
|
let healthStatus: HealthStatus
|
|
let onSelect: () -> Void
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: provider.icon)
|
|
.foregroundColor(isConnected ? .accentColor : .secondary)
|
|
.frame(width: 20)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(provider.displayName)
|
|
.fontWeight(isActive ? .semibold : .regular)
|
|
Text(provider.description)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Health indicator
|
|
Circle()
|
|
.fill(healthColor)
|
|
.frame(width: 8, height: 8)
|
|
|
|
// Status badge
|
|
if isActive {
|
|
Text("Active")
|
|
.font(.caption2)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color.accentColor.opacity(0.2))
|
|
.foregroundColor(.accentColor)
|
|
.cornerRadius(4)
|
|
} else if isConnected {
|
|
Button("Select") {
|
|
onSelect()
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.small)
|
|
} else {
|
|
Text("Not Available")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
private var healthColor: Color {
|
|
switch healthStatus {
|
|
case .healthy: return .green
|
|
case .degraded: return .yellow
|
|
case .unhealthy: return .red
|
|
case .unknown: return .gray
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
PreferencesView()
|
|
}
|