kmp openharmony 滑动平均与趋势平滑分析
本文介绍了一个基于Kotlin Multiplatform和OpenHarmony实现的滑动平均与趋势平滑分析工具。该工具通过配置短周期和长周期窗口,对数值序列计算移动平均线,检测短期与长期均线的交叉点来识别趋势拐点。文章阐述了在AIOps和业务分析中常见的应用场景,如接口耗时、QPS、资源利用率等指标的趋势分析。工具支持文本输入配置,包含窗口参数和数值序列,并输出原始数据、移动平均结果及趋势拐点

在实际运维和业务分析中,原始指标曲线(QPS、RT、CPU、订单量等)往往“抖动”非常明显:
既包含真实的趋势变化,也夹杂大量短期噪声。如果只盯着原始曲线,很难快速判断:
当前指标是在“整体上升 / 下降”,还是只是短暂波动?
短期波动是否已经反转长期趋势?
拐点大概出现在什么位置?
本案例基于 Kotlin Multiplatform(KMP)与 OpenHarmony,实现了一个滑动平均与趋势平滑分析器:
- 支持配置短周期窗口
shortWindow和长周期窗口longWindow; - 对同一数值序列计算短期与长期移动平均线;
- 检测“短期均线与长期均线的交叉点”,给出可能的趋势拐点提示;
- 通过 ArkTS 单页面展示原始序列 + 两条移动平均序列 + 文本化拐点说明。
一、问题背景与典型场景
在 AIOps 与业务分析中,常见的趋势问题包括:
-
接口耗时趋势分析
观察某接口在过去一小时/一天中耗时的变化趋势,判断是不是“整体在变慢”,还是“偶尔抖一下”。 -
QPS / 订单量趋势平滑
高并发场景下每秒的 QPS/订单量波动很大,需要通过移动平均来看到真正的“增长/下滑趋势”。 -
资源利用率趋势分析
CPU / 内存 / 磁盘使用率短时间内可能出现尖峰,通过长周期移动平均可以判断整体负载是否持续走高。 -
业务指标健康度监控
对 DAU、转化率等核心指标做滑动平均,避免被短期营销活动或偶发事件误导整体判断。
这些场景可以统一抽象为:
给定一条时间序列 \( x_0, x_1, \dots, x_{n-1} \),
以及两个窗口大小 \( w_s < w_l \),
分别计算短期移动平均 \( MA_{w_s} \) 与长期移动平均 \( MA_{w_l} \),并分析两者的交叉关系。
二、Kotlin 滑动平均分析引擎
1. 输入格式设计
沿用本系列案例的文本配置风格,本案例使用如下输入格式:
shortWindow=3
longWindow=5
series=120,95,180,210,160,300,240,260,220
shortWindow:短周期窗口大小(例如 3 个点);longWindow:长周期窗口大小(例如 5 个点);series:一串以,/ 空格 /;/ 换行分隔的数值样本。
解析逻辑也支持追加多行裸样本,例如:
shortWindow=3
longWindow=5
120,95,180,210,160
300,240,260,220
2. Kotlin 分析主入口
在 App.kt 中,我们定义了对外暴露的分析函数,并通过 @JsExport 让 OpenHarmony 端可以直接调用:
@JsExport
fun movingAverageTrendAnalyzer(inputData: String): String {
val sanitized = inputData.trim()
if (sanitized.isEmpty()) {
return "❌ 输入为空,请按 shortWindow=3\\nlongWindow=5\\nseries=120,95,180,... 形式提供数据"
}
val lines = sanitized.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
var shortWindow: Int? = null
var longWindow: Int? = null
val values = mutableListOf<Double>()
for (line in lines) {
when {
line.startsWith("shortWindow=", ignoreCase = true) -> {
shortWindow = line.substringAfter("=").trim().toIntOrNull()
}
line.startsWith("longWindow=", ignoreCase = true) -> {
longWindow = line.substringAfter("=").trim().toIntOrNull()
}
line.startsWith("series=", ignoreCase = true) -> {
val parsed = line.substringAfter("=")
.split(",", " ", ";", "\n")
.mapNotNull { it.trim().takeIf { s -> s.isNotEmpty() }?.toDoubleOrNull() }
values += parsed
}
else -> {
val parsed = line.split(",", " ", ";", "\n")
.mapNotNull { it.trim().takeIf { s -> s.isNotEmpty() }?.toDoubleOrNull() }
values += parsed
}
}
}
if (values.isEmpty()) {
return "❌ 未解析到任何数值,请检查 series=120,95,180,... 的格式是否正确"
}
if (shortWindow == null || shortWindow!! <= 0) {
return "❌ 未找到合法的 shortWindow=... 配置,请提供大于 0 的短周期窗口"
}
if (longWindow == null || longWindow!! <= 0) {
return "❌ 未找到合法的 longWindow=... 配置,请提供大于 0 的长周期窗口"
}
val sw = shortWindow!!
val lw = longWindow!!
if (sw >= lw) {
return "❌ 为了体现“短周期 vs 长周期”效果,请保证 shortWindow < longWindow,例如 3 和 7"
}
if (values.size < lw) {
return "❌ 样本数量 (${values.size}) 小于长周期窗口 ($lw),无法计算移动平均,请补充更多数据"
}
fun movingAverage(window: Int): List<Double> {
val result = mutableListOf<Double>()
var sum = 0.0
for (i in values.indices) {
sum += values[i]
if (i >= window) {
sum -= values[i - window]
}
if (i >= window - 1) {
result += sum / window
}
}
return result
}
val shortMa = movingAverage(sw)
val longMa = movingAverage(lw)
val builder = StringBuilder()
builder.appendLine("📉 滑动平均与趋势平滑分析报告")
builder.appendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
builder.appendLine("样本数量: ${values.size}")
builder.appendLine("短周期窗口 (shortWindow): $sw")
builder.appendLine("长周期窗口 (longWindow): $lw")
builder.appendLine()
builder.appendLine("🧮 原始序列")
builder.appendLine(values.joinToString(prefix = "[", postfix = "]"))
builder.appendLine()
builder.appendLine("📈 短周期移动平均 (响应更快,噪声更大)")
builder.appendLine(shortMa.joinToString(prefix = "[", postfix = "]") { round2(it).toString() })
builder.appendLine()
builder.appendLine("📈 长周期移动平均 (更平滑,体现整体趋势)")
builder.appendLine(longMa.joinToString(prefix = "[", postfix = "]") { round2(it).toString() })
builder.appendLine()
// 简单拐点 / 趋势切换提示:短期均线与长期均线的交叉
builder.appendLine("🔀 趋势拐点提示 (短期均线与长期均线交叉)")
val offset = lw - sw
var lastDiff: Double? = null
for (i in longMa.indices) {
val shortIndex = i + offset
if (shortIndex >= shortMa.size) break
val diff = shortMa[shortIndex] - longMa[i]
if (lastDiff != null && diff * lastDiff < 0) {
// 符号发生变化,认为发生了一次交叉
val pos = i + (lw - 1)
val direction = if (diff > 0) "短期向上突破长期 (可能上行趋势开启)" else "短期跌破长期 (可能下行趋势开启)"
builder.appendLine("索引 ~$pos 附近发生交叉: $direction")
}
lastDiff = diff
}
if (lastDiff == null) {
builder.appendLine("未检测到明显的短期 / 长期均线交叉点")
}
builder.appendLine()
builder.appendLine("🧠 工程化解读")
builder.appendLine("- 短周期移动平均线适合观测短期波动与局部异常;")
builder.appendLine("- 长周期移动平均线更适合刻画整体趋势,过滤噪声;")
builder.appendLine("- 短期均线向上突破长期均线时,往往意味着“指标进入上升通道”;反之则可能预示着“回落或降温”。")
return builder.toString().trim()
}
整体实现要点:
- 使用“滑动窗口 + 前缀和”的方式计算移动平均,整体时间复杂度 \( O(n) \);
- 对不同窗口大小调用同一
movingAverage辅助函数,减少重复代码; - 通过比较短期均线与长期均线的差值符号变化,粗略识别“金叉 / 死叉”这类趋势拐点。
三、OpenHarmony 侧调用与 UI 展示思路
在 ArkTS 页面中,可以像之前案例一样导入该分析函数:
import { movingAverageTrendAnalyzer } from './hellokjs'
页面状态可以设计为:
shortWindow: 短周期窗口(默认如"3");longWindow: 长周期窗口(默认如"5"或"7");seriesInput: 指标时间序列数据,如"120,95,180,210,160,300,240,260,220"。
拼接 payload 并调用:
const seriesLine = this.seriesInput.includes('series=') ? this.seriesInput : `series=${this.seriesInput}`
const payload = `shortWindow=${this.shortWindow}\nlongWindow=${this.longWindow}\n${seriesLine}`
this.result = movingAverageTrendAnalyzer(payload)
展示区域建议:
- 使用等宽字体显示原始序列、短周期均线、长周期均线三列数据;
- 对“趋势拐点提示”部分使用醒目颜色或图标标注,方便快速定位可能的金叉/死叉位置;
- 如有需要,可以在 ArkTS 侧进一步绘制简单的折线图,将原始值与两条 MA 线 Overlay 在一起。
四、复杂度与工程实践建议
复杂度分析:
- 移动平均计算:对每个窗口只做加减操作,时间复杂度 \( O(n) \);
- 拐点检测:对齐短期/长期均线后做一次线性扫描,复杂度 \( O(n) \);
- 整体复杂度 \( O(n) \),空间复杂度 \( O(n) \)(存储两条移动平均序列)。
工程实践建议:
-
多窗口对比
可以支持 3/5/7 等多个窗口组合,生成多条移动平均线,分别对应短、中、长期趋势。 -
与分位数 / 直方图联动
对“趋势上升区间”单独做分位数或直方图分析,评估在趋势变动阶段的分布特性。 -
阈值 + 趋势联合告警
不仅在单点阈值越界时告警,也可以在“长期趋势持续走高 + 短期均线金叉”时发出预警,避免临界点才告警。 -
边缘节点 / 端侧趋势分析
利用 KMP + OpenHarmony 的跨端能力,把这套移动平均与趋势识别逻辑下沉到设备侧,在弱网甚至离线情况下也能完成本地趋势诊断。
通过这个滑动平均与趋势平滑分析案例,你可以将“技术分析”中经典的移动平均思想应用到接口耗时、QPS、资源利用率等各种指标上,
在噪声众多的实时曲线中提炼出真正有价值的趋势信号。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)