📁 完整项目代码结构 text GoldAI/ ├── app/ │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/goldai/ │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── data/ │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── Tick.kt │ │ │ │ │ │ ├── Kline.kt │ │ │ │ │ │ ├── Signal.kt │ │ │ │ │ │ └── StrategyConfig.kt │ │ │ │ │ ├── network/ │ │ │ │ │ │ ├── AllTickWebSocket.kt │ │ │ │ │ │ ├── ItickApi.kt │ │ │ │ │ │ └── NetworkModule.kt │ │ │ │ │ ├── repository/ │ │ │ │ │ │ └── MarketRepository.kt │ │ │ │ │ └── database/ │ │ │ │ │ ├── AppDatabase.kt │ │ │ │ │ └── SettingDao.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── strategy/ │ │ │ │ │ │ └── StrategyCalculator.kt │ │ │ │ │ └── usecase/ │ │ │ │ │ └── GetSignalsUseCase.kt │ │ │ │ ├── presentation/ │ │ │ │ │ ├── theme/ │ │ │ │ │ │ ├── Color.kt │ │ │ │ │ │ ├── Theme.kt │ │ │ │ │ │ └── Type.kt │ │ │ │ │ ├── screens/ │ │ │ │ │ │ ├── DashboardScreen.kt │ │ │ │ │ │ ├── StrategiesScreen.kt │ │ │ │ │ │ ├── SimulationScreen.kt │ │ │ │ │ │ ├── BacktestScreen.kt │ │ │ │ │ │ ├── AnalysisScreen.kt │ │ │ │ │ │ └── SettingsScreen.kt │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── PriceCard.kt │ │ │ │ │ │ ├── SignalCard.kt │ │ │ │ │ │ └── Tooltip.kt │ │ │ │ │ └── viewmodel/ │ │ │ │ │ ├── DashboardViewModel.kt │ │ │ │ │ ├── StrategiesViewModel.kt │ │ │ │ │ └── SettingsViewModel.kt │ │ │ │ └── utils/ │ │ │ │ ├── Constants.kt │ │ │ │ └── Extensions.kt │ │ │ └── res/ │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── drawable/ (可忽略) │ │ └── AndroidManifest.xml │ └── build.gradle.kts └── build.gradle.kts
- 项目根 build.gradle.kts kotlin // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.1.0" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("com.google.dagger.hilt.android") version "2.48" apply false id("org.jetbrains.kotlin.kapt") version "1.9.0" apply false }
- app/build.gradle.kts kotlin plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.dagger.hilt.android") id("org.jetbrains.kotlin.kapt") }
android { namespace = "com.goldai" compileSdk = 34
defaultConfig {
applicationId = "com.goldai"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.activity:activity-compose:1.8.0") implementation(platform("androidx.compose:compose-bom:2023.10.01")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") implementation("androidx.navigation:navigation-compose:2.7.5")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Networking
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("io.ktor:ktor-client-core:2.3.5")
implementation("io.ktor:ktor-client-cio:2.3.5")
implementation("io.ktor:ktor-client-websockets:2.3.5")
// Dependency Injection
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Database
implementation("androidx.room:room-runtime:2.6.0")
kapt("androidx.room:room-compiler:2.6.0")
implementation("androidx.room:room-ktx:2.6.0")
// Charts
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
// Preferences DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
} 3. AndroidManifest.xml xml
<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果需要悬浮窗,后续添加 -->
<!-- <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GoldAI"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GoldAI">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
4. MainActivity.kt
kotlin
package com.goldai
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.goldai.presentation.screens.* import com.goldai.presentation.theme.GoldAITheme import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { GoldAITheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { val navController = rememberNavController() NavHost(navController = navController, startDestination = "dashboard") { composable("dashboard") { DashboardScreen(navController) } composable("strategies") { StrategiesScreen(navController) } composable("simulation") { SimulationScreen(navController) } composable("backtest") { BacktestScreen(navController) } composable("analysis") { AnalysisScreen(navController) } composable("settings") { SettingsScreen(navController) } } } } } } }
- data/model/Tick.kt kotlin package com.goldai.data.model
data class Tick( val timestamp: Long, val price: Double, val volume: Double ) 6. data/model/Kline.kt kotlin package com.goldai.data.model
data class Kline( val time: Long, val open: Double, val high: Double, val low: Double, val close: Double, val volume: Double ) 7. data/model/Signal.kt kotlin package com.goldai.data.model
enum class SignalType { MID, SHORT, ULTRA, DIVE, BOUNCE, SURGE_BUY, SURGE_SELL } enum class Action { BUY, SELL, WAIT }
data class Signal( val type: SignalType, val action: Action, val color: String, // "red", "green", "gray" val reason: String, val timestamp: Long ) 8. data/model/StrategyConfig.kt kotlin package com.goldai.data.model
data class StrategyConfig( val midMa5Period: Int = 5, val midMa20Period: Int = 20, val midRsiThreshold: Int = 50, val shortBbPeriod: Int = 20, val shortBbDev: Double = 2.0, val shortRsiOverbought: Int = 70, val shortRsiOversold: Int = 30, val ultraKdjPeriod: Int = 9, val diveDropThreshold: Double = 1.0, val diveTimeWindow: Int = 5, val diveVolumeMultiple: Double = 1.2, val bounceThreshold: Double = 0.3, val bounceRsiThreshold: Int = 35, val surgeBuyRiseThreshold: Double = 0.5, val surgeBuyTimeWindow: Int = 2, val surgeBuyVolumeMultiple: Double = 1.5, val surgeSellPullbackThreshold: Double = 0.3, val surgeSellTimeWindow: Int = 3 ) 9. data/network/AllTickWebSocket.kt kotlin package com.goldai.data.network
import android.util.Log import com.goldai.data.model.Tick import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.websocket.* import io.ktor.websocket.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.serialization.json.*
class AllTickWebSocket( private val token: String, private val onTick: (Tick) -> Unit ) { private val client = HttpClient(CIO) { install(WebSockets) } private var job: Job? = null private val _connectionState = MutableStateFlow(false) val connectionState: StateFlow = _connectionState.asStateFlow()
fun connect() {
if (_connectionState.value) return
job = CoroutineScope(Dispatchers.IO).launch {
var retryCount = 0
while (isActive) {
try {
client.webSocket(
method = HttpMethod.Get,
host = "quote.alltick.io",
path = "/quote-b-ws-api"
) {
Log.d("AllTickWS", "Connected")
_connectionState.value = true
retryCount = 0
// 发送订阅
send(Json.encodeToString(mapOf(
"cmd" to "subscribe",
"args" to listOf("XAUUSD")
)))
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
val json = Json.parseToJsonElement(text)
val data = json.jsonObject["data"]?.jsonArray?.get(0)?.jsonArray
if (data != null) {
val timestamp = data[0].jsonPrimitive.long
val price = data[4].jsonPrimitive.double
val volume = data[5].jsonPrimitive.long / 10000.0
onTick(Tick(timestamp, price, volume))
}
}
}
}
}
} catch (e: Exception) {
Log.e("AllTickWS", "Connection error: ${e.message}")
_connectionState.value = false
retryCount++
val delayTime = (5000 * retryCount).coerceAtMost(30000)
delay(delayTime.toLong())
}
}
}
}
fun disconnect() {
job?.cancel()
client.close()
_connectionState.value = false
}
} 10. data/network/ItickApi.kt kotlin package com.goldai.data.network
import com.goldai.data.model.Kline import retrofit2.http.*
interface ItickApi { @GET("forex/kline") suspend fun getKline( @Query("region") region: String = "gb", @Query("code") code: String = "XAUUSD", @Query("kType") kType: Int = 1 ): ItickResponse }
data class ItickResponse( val code: Int?, val data: List? )
data class ItickKline( val t: Long, val o: Double, val h: Double, val l: Double, val c: Double, val v: Double )
fun ItickKline.toKline(): Kline = Kline(t, o, h, l, c, v) 11. data/network/NetworkModule.kt kotlin package com.goldai.data.network
import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit import javax.inject.Singleton
@Module @InstallIn(SingletonComponent::class) object NetworkModule {
private const val BASE_URL = "https://api.itick.org/"
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
val gson = GsonBuilder()
.setLenient()
.create()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
@Provides
@Singleton
fun provideItickApi(retrofit: Retrofit): ItickApi {
return retrofit.create(ItickApi::class.java)
}
} 12. data/repository/MarketRepository.kt kotlin package com.goldai.data.repository
import android.util.Log import com.goldai.data.model.Signal import com.goldai.data.model.Tick import com.goldai.data.network.AllTickWebSocket import com.goldai.data.network.ItickApi import com.goldai.domain.strategy.StrategyCalculator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton
@Singleton class MarketRepository @Inject constructor( private val itickApi: ItickApi, private val strategyCalculator: StrategyCalculator ) { private lateinit var allTickWebSocket: AllTickWebSocket private val _tickFlow = MutableSharedFlow() val tickFlow: SharedFlow = _tickFlow.asSharedFlow()
private val _signals = MutableStateFlow<List<Signal>>(emptyList())
val signals: StateFlow<List<Signal>> = _signals.asStateFlow()
private val _priceHistory = MutableStateFlow<List<Double>>(emptyList())
val priceHistory: StateFlow<List<Double>> = _priceHistory.asStateFlow()
private val _volumeHistory = MutableStateFlow<List<Double>>(emptyList())
val volumeHistory: StateFlow<List<Double>> = _volumeHistory.asStateFlow()
private val viewModelScope = CoroutineScope(Dispatchers.Default)
fun initAllTick(token: String) {
allTickWebSocket = AllTickWebSocket(token) { tick ->
viewModelScope.launch {
_tickFlow.emit(tick)
updateHistory(tick.price, tick.volume)
calculateSignals()
}
}
}
fun startAllTick() {
if (::allTickWebSocket.isInitialized) {
allTickWebSocket.connect()
}
}
fun stopAllTick() {
if (::allTickWebSocket.isInitialized) {
allTickWebSocket.disconnect()
}
}
private fun updateHistory(price: Double, volume: Double) {
val newPrices = _priceHistory.value.toMutableList().apply {
add(price)
if (size > 100) removeAt(0)
}
val newVolumes = _volumeHistory.value.toMutableList().apply {
add(volume)
if (size > 100) removeAt(0)
}
_priceHistory.value = newPrices
_volumeHistory.value = newVolumes
}
private fun calculateSignals() {
val prices = _priceHistory.value
val volumes = _volumeHistory.value
if (prices.size < 20) return
val avgVolume = if (volumes.size >= 20) volumes.takeLast(20).average() else 1.0
val currentVolume = volumes.lastOrNull() ?: 0.0
val rsi = strategyCalculator.calculateRSI(prices)
val ma5 = strategyCalculator.calculateMA(prices, 5)
val ma10 = strategyCalculator.calculateMA(prices, 10)
val ma20 = strategyCalculator.calculateMA(prices, 20)
val ma50 = strategyCalculator.calculateMA(prices, 50)
val ma200 = strategyCalculator.calculateMA(prices, 200)
val signals = listOf(
strategyCalculator.calculateMidSignal(prices, rsi),
strategyCalculator.calculateShortSignal(prices, rsi),
strategyCalculator.calculateUltraSignal(prices, currentVolume, avgVolume),
strategyCalculator.detectDive(prices, volumes, avgVolume),
strategyCalculator.detectBounce(prices, rsi),
strategyCalculator.calculateSurgeBuySignal(prices, currentVolume, avgVolume),
strategyCalculator.calculateSurgeSellSignal(prices)
)
_signals.value = signals
}
suspend fun fetchItickKline(): List<com.goldai.data.model.Kline> {
return try {
val response = itickApi.getKline()
response.data?.map { it.toKline() } ?: emptyList()
} catch (e: Exception) {
Log.e("MarketRepository", "iTick fetch failed", e)
emptyList()
}
}
} 13. domain/strategy/StrategyCalculator.kt kotlin package com.goldai.domain.strategy
import com.goldai.data.model.* import javax.inject.Inject import javax.inject.Singleton
@Singleton class StrategyCalculator @Inject constructor() {
fun calculateMA(prices: List<Double>, period: Int): Double {
if (prices.size < period) return prices.lastOrNull() ?: 0.0
return prices.takeLast(period).average()
}
fun calculateRSI(prices: List<Double>, period: Int = 14): Double {
if (prices.size < period + 1) return 50.0
var gains = 0.0
var losses = 0.0
for (i in prices.size - period until prices.size) {
val diff = prices[i] - prices[i - 1]
if (diff > 0) gains += diff else losses -= diff
}
val avgGain = gains / period
val avgLoss = losses / period
if (avgLoss == 0.0) return 100.0
val rs = avgGain / avgLoss
return 100.0 - 100.0 / (1 + rs)
}
fun calculateMidSignal(prices: List<Double>, rsi: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < 20) return Signal(SignalType.MID, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val ma5 = calculateMA(prices, config.midMa5Period)
val ma20 = calculateMA(prices, config.midMa20Period)
return when {
ma5 > ma20 && rsi > config.midRsiThreshold -> Signal(SignalType.MID, Action.BUY, "red", "MA5(${ma5.toInt()})上穿MA20(${ma20.toInt()}),RSI偏强", System.currentTimeMillis())
ma5 < ma20 && rsi < config.midRsiThreshold -> Signal(SignalType.MID, Action.SELL, "green", "MA5下穿MA20,RSI偏弱", System.currentTimeMillis())
else -> Signal(SignalType.MID, Action.WAIT, "gray", "均线纠缠,RSI中性", System.currentTimeMillis())
}
}
fun calculateShortSignal(prices: List<Double>, rsi: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < 20) return Signal(SignalType.SHORT, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val ma5 = calculateMA(prices, 5)
val ma10 = calculateMA(prices, 10)
val ma20 = calculateMA(prices, 20)
val upper = ma20 * (1 + config.shortBbDev / 100)
val lower = ma20 * (1 - config.shortBbDev / 100)
val currentPrice = prices.last()
return when {
currentPrice > upper && rsi > config.shortRsiOverbought -> Signal(SignalType.SHORT, Action.SELL, "green", "突破布林上轨(${upper.toInt()}),RSI超买", System.currentTimeMillis())
currentPrice < lower && rsi < config.shortRsiOversold -> Signal(SignalType.SHORT, Action.BUY, "red", "跌破布林下轨(${lower.toInt()}),RSI超卖", System.currentTimeMillis())
currentPrice > ma5 && ma5 > ma10 -> Signal(SignalType.SHORT, Action.BUY, "red", "短期均线多头排列", System.currentTimeMillis())
currentPrice < ma5 && ma5 < ma10 -> Signal(SignalType.SHORT, Action.SELL, "green", "短期均线空头排列", System.currentTimeMillis())
else -> Signal(SignalType.SHORT, Action.WAIT, "gray", "价格在中轨附近", System.currentTimeMillis())
}
}
fun calculateUltraSignal(prices: List<Double>, volume: Double, avgVolume: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < 5) return Signal(SignalType.ULTRA, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val ma5 = calculateMA(prices, 5)
val currentPrice = prices.last()
return when {
currentPrice > ma5 * 1.002 && volume > avgVolume * 1.2 -> Signal(SignalType.ULTRA, Action.BUY, "red", "价格突破MA5,成交量放大", System.currentTimeMillis())
currentPrice < ma5 * 0.998 && volume > avgVolume * 1.2 -> Signal(SignalType.ULTRA, Action.SELL, "green", "价格跌破MA5,成交量放大", System.currentTimeMillis())
else -> Signal(SignalType.ULTRA, Action.WAIT, "gray", "价格在MA5附近", System.currentTimeMillis())
}
}
fun detectDive(prices: List<Double>, volumes: List<Double>, avgVolume: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < config.diveTimeWindow) return Signal(SignalType.DIVE, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val startPrice = prices[prices.size - config.diveTimeWindow]
val endPrice = prices.last()
val dropPct = (startPrice - endPrice) / startPrice * 100
val currentVolume = volumes.lastOrNull() ?: 0.0
val detected = dropPct >= config.diveDropThreshold && currentVolume > avgVolume * config.diveVolumeMultiple
val reason = if (detected) "${config.diveTimeWindow}分钟内下跌${"%.2f".format(dropPct)}%,成交量放大" else "未检测到跳水"
return Signal(SignalType.DIVE, Action.WAIT, if (detected) "yellow" else "gray", reason, System.currentTimeMillis())
}
fun detectBounce(prices: List<Double>, rsi: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < 2) return Signal(SignalType.BOUNCE, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val currentPrice = prices.last()
val prevPrice = prices[prices.size - 2]
val bouncePct = (currentPrice - prevPrice) / prevPrice * 100
val buySignal = bouncePct >= config.bounceThreshold && rsi < config.bounceRsiThreshold
val reason = if (buySignal) "反弹${"%.2f".format(bouncePct)}%,RSI超卖" else "无有效反弹"
return Signal(SignalType.BOUNCE, if (buySignal) Action.BUY else Action.WAIT, if (buySignal) "red" else "gray", reason, System.currentTimeMillis())
}
fun calculateSurgeBuySignal(prices: List<Double>, volume: Double, avgVolume: Double, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < config.surgeBuyTimeWindow + 1) return Signal(SignalType.SURGE_BUY, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val startPrice = prices[prices.size - config.surgeBuyTimeWindow - 1]
val endPrice = prices.last()
val risePct = (endPrice - startPrice) / startPrice * 100
val triggered = risePct >= config.surgeBuyRiseThreshold && volume > avgVolume * config.surgeBuyVolumeMultiple
val reason = if (triggered) "${config.surgeBuyTimeWindow}分钟内上涨${"%.2f".format(risePct)}%,成交量放大" else "未达爆拉阈值"
return Signal(SignalType.SURGE_BUY, if (triggered) Action.BUY else Action.WAIT, if (triggered) "red" else "gray", reason, System.currentTimeMillis())
}
fun calculateSurgeSellSignal(prices: List<Double>, config: StrategyConfig = StrategyConfig()): Signal {
if (prices.size < config.surgeSellTimeWindow + 1) return Signal(SignalType.SURGE_SELL, Action.WAIT, "gray", "数据不足", System.currentTimeMillis())
val recentHigh = prices.takeLast(config.surgeSellTimeWindow + 1).maxOrNull() ?: 0.0
val currentPrice = prices.last()
val pullbackPct = (recentHigh - currentPrice) / recentHigh * 100
val triggered = pullbackPct >= config.surgeSellPullbackThreshold
val reason = if (triggered) "从高点回调${"%.2f".format(pullbackPct)}%" else "未达回调阈值"
return Signal(SignalType.SURGE_SELL, if (triggered) Action.SELL else Action.WAIT, if (triggered) "green" else "gray", reason, System.currentTimeMillis())
}
fun calculateAllSignals(
prices: List<Double>,
volumes: List<Double>,
rsi: Double,
currentVolume: Double,
avgVolume: Double,
config: StrategyConfig
): List<Signal> {
return listOf(
calculateMidSignal(prices, rsi, config),
calculateShortSignal(prices, rsi, config),
calculateUltraSignal(prices, currentVolume, avgVolume, config),
detectDive(prices, volumes, avgVolume, config),
detectBounce(prices, rsi, config),
calculateSurgeBuySignal(prices, currentVolume, avgVolume, config),
calculateSurgeSellSignal(prices, config)
)
}
}
- presentation/theme/Color.kt kotlin package com.goldai.presentation.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260)
// 自定义颜色 val RedBuy = Color(0xFFEF4444) val GreenSell = Color(0xFF10B981) val GrayNeutral = Color(0xFF6B7280) 15. presentation/theme/Theme.kt kotlin package com.goldai.presentation.theme
import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80 )
private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable fun GoldAITheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme } val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window window.statusBarColor = colorScheme.primary.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme } }
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
} 16. presentation/theme/Type.kt kotlin package com.goldai.presentation.theme
import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp ) /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp ), labelSmall = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp ) */ ) 17. presentation/components/PriceCard.kt kotlin package com.goldai.presentation.components
import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.goldai.presentation.theme.GreenSell import com.goldai.presentation.theme.RedBuy
@Composable fun PriceCard( price: Double, changePercent: String, lastUpdate: String, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( text = "实时金价", style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "$${String.format("%.2f", price)}", fontSize = 32.sp, fontWeight = FontWeight.Bold ) val change = changePercent.toDoubleOrNull() ?: 0.0 val changeColor = if (change >= 0) RedBuy else GreenSell Text( text = if (change >= 0) "+changePercent%", color = changeColor, fontSize = 18.sp, fontWeight = FontWeight.Medium ) Spacer(modifier = Modifier.height(4.dp)) Text( text = "更新于: $lastUpdate", fontSize = 12.sp, color = Color.Gray ) } } } 18. presentation/components/SignalCard.kt kotlin package com.goldai.presentation.components
import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.goldai.data.model.Signal import com.goldai.presentation.theme.GreenSell import com.goldai.presentation.theme.RedBuy
@Composable fun SignalCard( signal: Signal, modifier: Modifier = Modifier, onVerifyClick: () -> Unit = {} ) { val signalColor = when (signal.color) { "red" -> RedBuy "green" -> GreenSell else -> Color.Gray }
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = signal.type.name,
style = MaterialTheme.typography.titleSmall
)
Text(
text = signal.action.name,
color = signalColor,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = signal.reason,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onVerifyClick,
modifier = Modifier.align(Alignment.End)
) {
Text("AI验证")
}
}
}
} 19. presentation/components/Tooltip.kt kotlin package com.goldai.presentation.components
import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp
@Composable fun Tooltip( term: String, definition: String, modifier: Modifier = Modifier ) { var showDialog by remember { mutableStateOf(false) }
Box(modifier = modifier) {
IconButton(onClick = { showDialog = true }) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Info",
tint = MaterialTheme.colorScheme.primary
)
}
}
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text(term) },
text = { Text(definition) },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("我知道了")
}
}
)
}
} 20. presentation/screens/DashboardScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.goldai.presentation.components.PriceCard import com.goldai.presentation.components.SignalCard import com.goldai.presentation.viewmodel.DashboardViewModel
@OptIn(ExperimentalMaterial3Api::class) @Composable fun DashboardScreen( navController: NavController, viewModel: DashboardViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("黄金AI交易系统") }
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
PriceCard(
price = uiState.currentPrice,
changePercent = uiState.changePercent,
lastUpdate = uiState.lastUpdate
)
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "市场快照",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
// 简化的市场快照,可后续扩展
Text("ETF持仓: ${uiState.etfHolding}吨")
Text("COMEX: ${uiState.comexPosition}手")
Text("伦敦金: ${uiState.londonFlow}M")
Text("交易量: ${uiState.volume}万手")
}
}
}
item {
Text(
text = "实时信号",
style = MaterialTheme.typography.titleLarge
)
}
items(uiState.signals) { signal ->
SignalCard(
signal = signal,
onVerifyClick = { /* 暂时弹窗,后续可接入AI验证 */ }
)
}
}
}
} 21. presentation/viewmodel/DashboardViewModel.kt kotlin package com.goldai.presentation.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.goldai.data.repository.MarketRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject
data class DashboardUiState( val currentPrice: Double = 0.0, val changePercent: String = "0.00", val volume: Double = 0.0, val etfHolding: Double = 0.0, val comexPosition: Long = 0, val londonFlow: Double = 0.0, val signals: List<com.goldai.data.model.Signal> = emptyList(), val lastUpdate: String = "", val isLoading: Boolean = false )
@HiltViewModel class DashboardViewModel @Inject constructor( private val repository: MarketRepository ) : ViewModel() {
private val _uiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
init {
viewModelScope.launch {
repository.tickFlow.collect { tick ->
_uiState.update { current ->
val prevClose = current.currentPrice
val change = if (prevClose > 0) (tick.price - prevClose) / prevClose * 100 else 0.0
current.copy(
currentPrice = tick.price,
changePercent = String.format("%.2f", change),
volume = tick.volume,
lastUpdate = dateFormat.format(Date(tick.timestamp))
)
}
}
}
viewModelScope.launch {
repository.signals.collect { signals ->
_uiState.update { it.copy(signals = signals) }
}
}
// 初始化数据源(假设已设置token)
repository.initAllTick("253274119b5cf2ee7cb9a375fdddca17-c-app")
repository.startAllTick()
}
override fun onCleared() {
repository.stopAllTick()
super.onCleared()
}
}
- presentation/screens/StrategiesScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.goldai.presentation.components.SignalCard import com.goldai.presentation.viewmodel.StrategiesViewModel
@OptIn(ExperimentalMaterial3Api::class) @Composable fun StrategiesScreen( navController: NavController, viewModel: StrategiesViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("策略中心") }
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(uiState.signals) { signal ->
SignalCard(
signal = signal,
onVerifyClick = { /* TODO: 接入AI验证 */ }
)
}
}
}
} 23. presentation/viewmodel/StrategiesViewModel.kt kotlin package com.goldai.presentation.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.goldai.data.model.Signal import com.goldai.data.repository.MarketRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject
data class StrategiesUiState( val signals: List = emptyList() )
@HiltViewModel class StrategiesViewModel @Inject constructor( private val repository: MarketRepository ) : ViewModel() {
private val _uiState = MutableStateFlow(StrategiesUiState())
val uiState: StateFlow<StrategiesUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
repository.signals.collect { signals ->
_uiState.update { it.copy(signals = signals) }
}
}
}
} 24. presentation/screens/SimulationScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.goldai.presentation.viewmodel.SimulationViewModel
@OptIn(ExperimentalMaterial3Api::class) @Composable fun SimulationScreen( navController: NavController, viewModel: SimulationViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("模拟交易") }
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "用户账号",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text("余额: $${String.format("%.2f", uiState.balance)}")
Text("当前金价: $${String.format("%.2f", uiState.currentPrice)}")
// 简单买入按钮(后续扩展)
Button(
onClick = { /* TODO: 买入逻辑 */ },
modifier = Modifier.align(Alignment.End)
) {
Text("买入")
}
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "当前持仓",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
if (uiState.positions.isEmpty()) {
Text("暂无持仓")
} else {
// 暂不实现持仓列表
}
}
}
}
}
}
} 25. presentation/viewmodel/SimulationViewModel.kt kotlin package com.goldai.presentation.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.goldai.data.repository.MarketRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject
data class SimulationUiState( val balance: Double = 100000.0, val currentPrice: Double = 0.0, val positions: List = emptyList() )
@HiltViewModel class SimulationViewModel @Inject constructor( private val repository: MarketRepository ) : ViewModel() {
private val _uiState = MutableStateFlow(SimulationUiState())
val uiState: StateFlow<SimulationUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
repository.tickFlow.collect { tick ->
_uiState.update { it.copy(currentPrice = tick.price) }
}
}
}
} 26. presentation/screens/BacktestScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController
@OptIn(ExperimentalMaterial3Api::class) @Composable fun BacktestScreen(navController: NavController) { Scaffold( topBar = { TopAppBar( title = { Text("回测") } ) } ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) .padding(16.dp) ) { Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( text = "回测功能", style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.height(8.dp)) Text("后续版本将提供策略回测功能") } } } } } 27. presentation/screens/AnalysisScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController
@OptIn(ExperimentalMaterial3Api::class) @Composable fun AnalysisScreen(navController: NavController) { Scaffold( topBar = { TopAppBar( title = { Text("AI分析") } ) } ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) .padding(16.dp) ) { Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( text = "AI市场分析", style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.height(8.dp)) Text("后续版本将接入DeepSeek AI进行实时分析") Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { /* TODO: 调用AI验证 */ }) { Text("AI验证") } } } } } } 28. presentation/screens/SettingsScreen.kt kotlin package com.goldai.presentation.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.goldai.presentation.viewmodel.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( navController: NavController, viewModel: SettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("设置") }
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "数据源选择",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = uiState.dataSource == "alltick",
onClick = { viewModel.setDataSource("alltick") },
label = { Text("AllTick") }
)
FilterChip(
selected = uiState.dataSource == "itick",
onClick = { viewModel.setDataSource("itick") },
label = { Text("iTick") }
)
FilterChip(
selected = uiState.dataSource == "mock",
onClick = { viewModel.setDataSource("mock") },
label = { Text("模拟") }
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "当前: ${uiState.dataSource}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "AllTick配置",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.alltickToken,
onValueChange = { viewModel.setAlltickToken(it) },
label = { Text("API密钥") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "iTick配置",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.itickToken,
onValueChange = { viewModel.setItickToken(it) },
label = { Text("API密钥") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "免费套餐有效期至2026-03-17,到期前请续期",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "交易方式",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = uiState.tradeMode == "long_only",
onClick = { viewModel.setTradeMode("long_only") },
label = { Text("只做多") }
)
FilterChip(
selected = uiState.tradeMode == "short_only",
onClick = { viewModel.setTradeMode("short_only") },
label = { Text("只做空") }
)
FilterChip(
selected = uiState.tradeMode == "both",
onClick = { viewModel.setTradeMode("both") },
label = { Text("多空都做") }
)
}
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "外观设置",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("深色模式")
Switch(
checked = uiState.darkTheme,
onCheckedChange = { viewModel.setDarkTheme(it) }
)
}
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "关于",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text("黄金AI交易系统 v2.0")
Text("数据源: AllTick / iTick / 模拟")
}
}
}
}
}
} 29. presentation/viewmodel/SettingsViewModel.kt kotlin package com.goldai.presentation.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.goldai.data.repository.MarketRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject
data class SettingsUiState( val dataSource: String = "alltick", val alltickToken: String = "253274119b5cf2ee7cb9a375fdddca17-c-app", val itickToken: String = "da9087a07eb644e9b65ae81d2bb04673c88d8b7ac63f4477b830cfc43f68f533", val tradeMode: String = "long_only", val darkTheme: Boolean = false )
@HiltViewModel class SettingsViewModel @Inject constructor( private val repository: MarketRepository ) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
fun setDataSource(source: String) {
_uiState.update { it.copy(dataSource = source) }
// 通知仓库切换数据源(需在仓库中实现切换逻辑)
// 简化起见,先不实现
}
fun setAlltickToken(token: String) {
_uiState.update { it.copy(alltickToken = token) }
}
fun setItickToken(token: String) {
_uiState.update { it.copy(itickToken = token) }
}
fun setTradeMode(mode: String) {
_uiState.update { it.copy(tradeMode = mode) }
}
fun setDarkTheme(enabled: Boolean) {
_uiState.update { it.copy(darkTheme = enabled) }
}
} 30. utils/Constants.kt kotlin package com.goldai.utils
object Constants { const val ALLTICK_WS_URL = "wss://quote.alltick.io/quote-b-ws-api" const val DEFAULT_ALLTICK_TOKEN = "253274119b5cf2ee7cb9a375fdddca17-c-app" const val DEFAULT_ITICK_TOKEN = "da9087a07eb644e9b65ae81d2bb04673c88d8b7ac63f4477b830cfc43f68f533" const val ITICK_BASE_URL = "api.itick.org/" } 31. utils/Extensions.kt kotlin package com.goldai.utils
import java.text.SimpleDateFormat import java.util.*
fun Long.formatTime(pattern: String = "yyyy-MM-dd HH:mm:ss"): String { val date = Date(this) val format = SimpleDateFormat(pattern, Locale.getDefault()) return format.format(date) }
fun Double.format(digits: Int = 2): String = String.format("%.${digits}f", this) 32. res/values/colors.xml xml
#FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF 33. res/values/strings.xml xml 黄金AI交易系统 34. res/values/themes.xml xml <item name="android:statusBarColor">@color/purple_700</item>