文章目录
-
- 每日一句正能量
- 前言
- 一、前言:量化交易的交互范式革新
- 二、核心特性解析与技术选型
-
- [2.1 沉浸光感在交易场景中的价值](#2.1 沉浸光感在交易场景中的价值)
- [2.2 Face AR在交易风控中的创新应用](#2.2 Face AR在交易风控中的创新应用)
- [2.3 Body AR在图表操控中的创新应用](#2.3 Body AR在图表操控中的创新应用)
- 三、环境配置与权限声明
-
- [3.1 模块依赖配置](#3.1 模块依赖配置)
- [3.2 权限声明(module.json5)](#3.2 权限声明(module.json5))
- 四、核心代码实战
-
- [4.1 市场行情光感引擎(MarketLightEngine.ets)](#4.1 市场行情光感引擎(MarketLightEngine.ets))
- [4.2 Face AR交易情绪风控系统(EmotionRiskSystem.ets)](#4.2 Face AR交易情绪风控系统(EmotionRiskSystem.ets))
- [4.3 Body AR图表手势操控系统(ChartGestureSystem.ets)](#4.3 Body AR图表手势操控系统(ChartGestureSystem.ets))
- [4.4 沉浸光感交易标题栏(ImmersiveTradingTitleBar.ets)](#4.4 沉浸光感交易标题栏(ImmersiveTradingTitleBar.ets))
- [4.5 悬浮板块导航面板(FloatAssetNav.ets)](#4.5 悬浮板块导航面板(FloatAssetNav.ets))
- [4.6 主交易页面(TradingMainPage.ets)](#4.6 主交易页面(TradingMainPage.ets))
- 五、关键技术总结
-
- [5.1 Face AR在交易风控中的适配清单](#5.1 Face AR在交易风控中的适配清单)
- [5.2 Body AR在图表操控中的最佳实践](#5.2 Body AR在图表操控中的最佳实践)
- [5.3 沉浸光感交易适配要点](#5.3 沉浸光感交易适配要点)
- 六、调试与测试建议
-
- [6.1 AR性能监控](#6.1 AR性能监控)
- [6.2 多窗口测试矩阵](#6.2 多窗口测试矩阵)
- [6.3 常见问题排查](#6.3 常见问题排查)
- 七、总结与展望

每日一句正能量
顺势而为的努力,远比逆势硬拼更有价值。
势 可以是时代趋势、行业周期、自身状态、环境条件。逆势硬拼 看起来很努力,实则消耗巨大、收效甚微,容易陷入"我这么辛苦为什么还不成功"的委屈。顺势而为 需要敏锐的观察力和耐心等待的定力。像冲浪,不是造浪,而是识别浪、踩上浪。
前言
摘要 :HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为金融量化交易开辟了全新的交互维度。本文将实战开发一款面向HarmonyOS PC的"灵犀智投"量化交易分析工作台,展示如何利用
systemMaterialEffect构建随市场行情动态变化的交易环境光感,通过悬浮导航实现市场板块与交易策略的快速切换,基于Face AR实现交易员情绪状态与冲动交易风险的实时监测,基于Body AR实现手势操控的K线图表与多维数据可视化,以及基于多窗口架构构建行情看板、策略回测、风险监控和交易执行的协作交易界面。
一、前言:量化交易的交互范式革新
传统交易软件依赖鼠标点击和键盘快捷键,交易员在海量数据面前容易因情绪波动做出非理性决策。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation) 、沉浸光感(Immersive Light Effects)与Face AR & Body AR特性,为量化交易带来了"情绪即风控、肢体即操作"的全新可能。
本文核心亮点:
- 行情感知光效:根据市场状态(牛市/熊市/震荡/崩盘/反弹)动态切换交易环境光色与氛围
- 情绪风控系统:通过Face AR实时捕捉交易员焦虑、贪婪、恐惧等情绪,自动触发交易限制
- 手势图表操控:Body AR手势实现K线缩放、时间轴滑动、指标切换、多屏联动
- 悬浮板块导航:底部悬浮页签切换A股/港股/美股/期货/外汇/加密货币,支持透明度调节
- 多窗口交易协作:主行情窗口 + 浮动订单簿 + 浮动持仓面板 + 浮动策略回测 + 浮动风控告警
二、核心特性解析与技术选型
2.1 沉浸光感在交易场景中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为UI组件带来细腻的光晕与反射效果。在交易场景中:
- 行情氛围渲染:牛市时呈现积极红光、熊市时呈现冷静青光、崩盘时闪烁警示红光
- 盈亏可视化:持仓盈利时环境泛绿光、亏损时泛红光,直观反馈账户状态
- 情绪调节:检测到焦虑时自动切换为柔和蓝光,帮助交易员冷静决策
- 告警引导:重大风险事件发生时,光效快速脉冲吸引注意力
2.2 Face AR在交易风控中的创新应用
- 冲动交易拦截:检测到贪婪表情(瞳孔放大、嘴角上扬)时自动增加确认步骤
- 恐慌抛售预警:识别恐惧表情(眉毛上扬、嘴巴张开)时暂停卖出操作
- 疲劳度监测:长时间盯盘导致眨眼频率降低时,建议休息或降低仓位
- 情绪档案:记录交易过程中的表情变化,生成"交易情绪报告"用于复盘
2.3 Body AR在图表操控中的创新应用
- K线缩放:双手张开放大时间周期、捏合缩小查看细节
- 时间轴滑动:单手左右滑动平移时间轴、快速滑动实现跳转
- 指标切换:单手上下滑动切换MACD/KDJ/RSI/BOLL等技术指标
- 多屏联动:双手框选区域同步到副屏展示,画圈触发策略回测
三、环境配置与权限声明
3.1 模块依赖配置
json
{
"dependencies": {
"@hms.core.ar.engine": "^6.1.0",
"@hms.core.graphics.2d": "^6.0.0",
"@hms.core.arkui.design": "^6.0.0",
"@hms.core.finance.data": "^6.0.0",
"@hms.core.ai.vision": "^6.0.0",
"@hms.core.distributed.device": "^6.0.0"
}
}
3.2 权限声明(module.json5)
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:face_ar_camera_permission",
"usedScene": {
"abilities": ["TradingAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:market_data_access"
},
{
"name": "ohos.permission.FINANCE_DATA",
"reason": "$string:trading_data_access"
}
]
}
}
四、核心代码实战
4.1 市场行情光感引擎(MarketLightEngine.ets)
typescript
// engines/MarketLightEngine.ets
import { ColorUtils } from '@hms.core.graphics.2d';
export enum MarketState {
BULL = 'bull', // 牛市 - 积极红
BEAR = 'bear', // 熊市 - 冷静青
CONSOLIDATION = 'consolidation', // 震荡 - 中性黄
CRASH = 'crash', // 崩盘 - 警示闪烁红
REBOUND = 'rebound', // 反弹 - 希望绿
VOLATILE = 'volatile' // 剧烈波动 - 警示紫
}
export enum AssetClass {
A_SHARE = 'a_share', // A股
HK_STOCK = 'hk_stock', // 港股
US_STOCK = 'us_stock', // 美股
FUTURES = 'futures', // 期货
FOREX = 'forex', // 外汇
CRYPTO = 'crypto' // 加密货币
}
export interface TradingLightTheme {
primaryColor: ResourceColor;
secondaryColor: ResourceColor;
ambientColor: ResourceColor;
bullColor: ResourceColor; // 涨色
bearColor: ResourceColor; // 跌色
glowIntensity: number;
pulseSpeed: number;
alertMode: boolean; // 告警模式
colorTemperature: number;
brightness: number;
}
export class MarketLightEngine {
private static themes: Map<MarketState, TradingLightTheme> = new Map([
[MarketState.BULL, {
primaryColor: '#EF4444', // 牛市红
secondaryColor: '#DC2626', // 深红
ambientColor: 'rgba(239, 68, 68, 0.08)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 0.7,
pulseSpeed: 3000,
alertMode: false,
colorTemperature: 4000,
brightness: 0.9
}],
[MarketState.BEAR, {
primaryColor: '#06B6D4', // 熊市青
secondaryColor: '#0891B2', // 深青
ambientColor: 'rgba(6, 182, 212, 0.08)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 0.5,
pulseSpeed: 5000,
alertMode: false,
colorTemperature: 6000,
brightness: 0.8
}],
[MarketState.CONSOLIDATION, {
primaryColor: '#FBBF24', // 震荡黄
secondaryColor: '#D97706', // 深黄
ambientColor: 'rgba(251, 191, 36, 0.08)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 0.5,
pulseSpeed: 4000,
alertMode: false,
colorTemperature: 4500,
brightness: 0.85
}],
[MarketState.CRASH, {
primaryColor: '#EF4444', // 崩盘红
secondaryColor: '#991B1B', // 暗红
ambientColor: 'rgba(239, 68, 68, 0.15)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 1.0,
pulseSpeed: 800, // 快速闪烁
alertMode: true,
colorTemperature: 3500,
brightness: 0.95
}],
[MarketState.REBOUND, {
primaryColor: '#22C55E', // 反弹绿
secondaryColor: '#15803D', // 深绿
ambientColor: 'rgba(34, 197, 94, 0.1)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 0.75,
pulseSpeed: 2500,
alertMode: false,
colorTemperature: 4200,
brightness: 0.9
}],
[MarketState.VOLATILE, {
primaryColor: '#A855F7', // 波动紫
secondaryColor: '#7C3AED', // 深紫
ambientColor: 'rgba(168, 85, 247, 0.12)',
bullColor: '#EF4444',
bearColor: '#22C55E',
glowIntensity: 0.85,
pulseSpeed: 1500,
alertMode: true,
colorTemperature: 5000,
brightness: 0.9
}]
]);
static getTheme(state: MarketState): TradingLightTheme {
return this.themes.get(state) || this.themes.get(MarketState.CONSOLIDATION)!;
}
// 根据盈亏状态调整光效
static adjustByPnL(theme: TradingLightTheme, pnl: number): TradingLightTheme {
const adjusted = { ...theme };
if (pnl > 0) {
// 盈利:泛绿光
adjusted.ambientColor = 'rgba(34, 197, 94, 0.1)';
adjusted.glowIntensity *= 1.1;
} else if (pnl < 0) {
// 亏损:泛红光
adjusted.ambientColor = 'rgba(239, 68, 68, 0.1)';
adjusted.glowIntensity *= 1.1;
}
return adjusted;
}
// 根据情绪状态调整
static adjustByEmotion(theme: TradingLightTheme, emotion: TraderEmotion): TradingLightTheme {
const adjusted = { ...theme };
switch (emotion) {
case TraderEmotion.GREEDY:
adjusted.primaryColor = '#F97316'; // 橙色警示
adjusted.pulseSpeed *= 0.8;
break;
case TraderEmotion.FEARFUL:
adjusted.primaryColor = '#818CF8'; // 紫蓝安抚
adjusted.glowIntensity *= 0.8;
break;
case TraderEmotion.ANXIOUS:
adjusted.primaryColor = '#FBBF24'; // 暖黄缓解
adjusted.pulseSpeed *= 1.2;
break;
case TraderEmotion.CALM:
// 保持默认
break;
case TraderEmotion.EXCITED:
adjusted.glowIntensity *= 1.2;
break;
}
return adjusted;
}
}
export enum TraderEmotion {
CALM = 'calm',
GREEDY = 'greedy',
FEARFUL = 'fearful',
ANXIOUS = 'anxious',
EXCITED = 'excited',
FATIGUED = 'fatigued'
}
4.2 Face AR交易情绪风控系统(EmotionRiskSystem.ets)
typescript
// systems/EmotionRiskSystem.ets
import { ARSession, ARFaceTrack, ARBlendShapes } from '@hms.core.ar.engine';
export enum RiskLevel {
LOW = 'low', // 低风险
MEDIUM = 'medium', // 中风险
HIGH = 'high', // 高风险
CRITICAL = 'critical' // 极高风险 - 禁止交易
}
export interface RiskAssessment {
level: RiskLevel;
score: number; // 风险分数 0-100
emotion: TraderEmotion;
triggers: string[]; // 触发因素
suggestedAction: string;
cooldownSeconds: number; // 冷却时间
}
export class EmotionRiskSystem {
private session: ARSession | null = null;
private faceTrack: ARFaceTrack | null = null;
private expressionHistory: FaceExpression[] = [];
private riskHistory: RiskAssessment[] = [];
private lastTradeTime: number = 0;
private tradeCooldown: number = 0;
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.FACE],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.faceTrack = this.session.getFaceTrack();
await this.session.start();
}
update(frameData: ARFrame): RiskAssessment | null {
if (!this.faceTrack) return null;
const faces = this.faceTrack.getTrackedFaces(frameData);
if (faces.length === 0) return null;
const face = faces[0];
const blendshapes = face.getBlendShapes();
const expression = this.parseExpression(blendshapes);
this.expressionHistory.push(expression);
if (this.expressionHistory.length > 20) this.expressionHistory.shift();
const risk = this.assessRisk(expression);
this.riskHistory.push(risk);
// 更新冷却时间
if (risk.cooldownSeconds > 0) {
this.tradeCooldown = Date.now() + risk.cooldownSeconds * 1000;
}
AppStorage.set('current_risk_level', risk.level);
AppStorage.set('current_risk_score', risk.score);
return risk;
}
private parseExpression(blendshapes: ARBlendShapes): FaceExpression {
return {
smileLeft: blendshapes.getValue('MOUTH_SMILE_LEFT') || 0,
smileRight: blendshapes.getValue('MOUTH_SMILE_RIGHT') || 0,
browRaise: blendshapes.getValue('BROW_RAISE') || 0,
browLower: blendshapes.getValue('BROW_LOWERER') || 0,
eyeWideLeft: blendshapes.getValue('EYE_WIDE_LEFT') || 0,
eyeWideRight: blendshapes.getValue('EYE_WIDE_RIGHT') || 0,
jawClench: blendshapes.getValue('JAW_CLENCH') || 0,
mouthOpen: blendshapes.getValue('MOUTH_OPEN') || 0,
noseWrinkle: blendshapes.getValue('NOSE_WRINKLE') || 0,
eyeBlink: blendshapes.getValue('EYE_BLINK_LEFT') || 0,
timestamp: Date.now()
};
}
private assessRisk(expression: FaceExpression): RiskAssessment {
let riskScore = 0;
const triggers: string[] = [];
let emotion = TraderEmotion.CALM;
// 贪婪检测:大笑 + 瞳孔放大 + 频繁眨眼减少
const smileIntensity = (expression.smileLeft + expression.smileRight) / 2;
const eyeWide = (expression.eyeWideLeft + expression.eyeWideRight) / 2;
if (smileIntensity > 0.6 && eyeWide > 0.5) {
riskScore += 30;
triggers.push('贪婪表情');
emotion = TraderEmotion.GREEDY;
}
// 恐惧检测:眉毛上扬 + 嘴巴张开 + 鼻子皱起
if (expression.browRaise > 0.5 && expression.mouthOpen > 0.4 && expression.noseWrinkle > 0.3) {
riskScore += 35;
triggers.push('恐惧表情');
emotion = TraderEmotion.FEARFUL;
}
// 焦虑检测:频繁眨眼 + 轻微皱眉
if (expression.eyeBlink > 0.7 && expression.browLower > 0.3) {
riskScore += 20;
triggers.push('焦虑表情');
emotion = TraderEmotion.ANXIOUS;
}
// 疲劳检测:眨眼频率低 + 眉毛下垂
if (expression.eyeBlink < 0.2 && expression.browLower < 0.2) {
riskScore += 15;
triggers.push('疲劳状态');
emotion = TraderEmotion.FATIGUED;
}
// 兴奋检测:眼睛睁大 + 微笑
if (eyeWide > 0.6 && smileIntensity > 0.4) {
riskScore += 10;
triggers.push('过度兴奋');
emotion = TraderEmotion.EXCITED;
}
// 连续亏损后的报复性交易检测
const recentEmotions = this.expressionHistory.slice(-5);
const fearCount = recentEmotions.filter(e =>
e.browRaise > 0.5 && e.mouthOpen > 0.4
).length;
if (fearCount >= 3) {
riskScore += 25;
triggers.push('连续恐惧 - 可能报复性交易');
}
// 确定风险等级
let level: RiskLevel;
let cooldownSeconds = 0;
let suggestedAction = '正常交易';
if (riskScore >= 70) {
level = RiskLevel.CRITICAL;
cooldownSeconds = 300; // 5分钟冷却
suggestedAction = '立即停止交易,深呼吸放松';
} else if (riskScore >= 50) {
level = RiskLevel.HIGH;
cooldownSeconds = 120; // 2分钟冷却
suggestedAction = '增加交易确认步骤,降低仓位';
} else if (riskScore >= 30) {
level = RiskLevel.MEDIUM;
cooldownSeconds = 30; // 30秒冷却
suggestedAction = '谨慎交易,注意止损';
} else {
level = RiskLevel.LOW;
}
return {
level,
score: Math.min(100, riskScore),
emotion,
triggers,
suggestedAction,
cooldownSeconds
};
}
// 检查是否可以交易
canTrade(): boolean {
return Date.now() > this.tradeCooldown;
}
// 获取剩余冷却时间
getCooldownRemaining(): number {
const remaining = this.tradeCooldown - Date.now();
return Math.max(0, Math.ceil(remaining / 1000));
}
// 获取风险历史
getRiskHistory(): RiskAssessment[] {
return [...this.riskHistory];
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
interface FaceExpression {
smileLeft: number;
smileRight: number;
browRaise: number;
browLower: number;
eyeWideLeft: number;
eyeWideRight: number;
jawClench: number;
mouthOpen: number;
noseWrinkle: number;
eyeBlink: number;
timestamp: number;
}
4.3 Body AR图表手势操控系统(ChartGestureSystem.ets)
typescript
// systems/ChartGestureSystem.ets
import { ARSession, ARBodyTrack, ARBodySkeleton, KeyPointType } from '@hms.core.ar.engine';
export enum ChartGesture {
ZOOM_IN = 'zoom_in', // 双手张开 → 放大K线
ZOOM_OUT = 'zoom_out', // 双手捏合 → 缩小K线
PAN_LEFT = 'pan_left', // 单手左滑 → 时间轴左移
PAN_RIGHT = 'pan_right', // 单手右滑 → 时间轴右移
INDICATOR_UP = 'indicator_up', // 单手上滑 → 切换上一个指标
INDICATOR_DOWN = 'indicator_down', // 单手下滑 → 切换下一个指标
CROSSHAIR = 'crosshair', // 单指指向 → 十字光标
MULTI_SCREEN = 'multi_screen', // 双手框选 → 多屏联动
REFRESH = 'refresh' // 画圈 → 刷新数据
}
export interface ChartCommand {
gesture: ChartGesture;
value: number;
position: { x: number; y: number };
confidence: number;
}
export class ChartGestureSystem {
private session: ARSession | null = null;
private bodyTrack: ARBodyTrack | null = null;
private rightHandHistory: HandPosition[] = [];
private leftHandHistory: HandPosition[] = [];
// 图表状态
private zoomLevel: number = 1;
private timeOffset: number = 0;
private currentIndicator: number = 0;
private indicators: string[] = ['MA', 'MACD', 'KDJ', 'RSI', 'BOLL', 'VOL'];
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.BODY],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.bodyTrack = this.session.getBodyTrack();
await this.session.start();
}
update(frameData: ARFrame): ChartCommand | null {
if (!this.bodyTrack) return null;
const bodies = this.bodyTrack.getTrackedBodies(frameData);
if (bodies.length === 0) return null;
const body = bodies[0];
const skeleton = body.getSkeleton();
const keypoints = skeleton.getKeyPoints();
const leftWrist = keypoints.find(kp => kp.type === KeyPointType.LEFT_WRIST);
const rightWrist = keypoints.find(kp => kp.type === KeyPointType.RIGHT_WRIST);
const rightIndex = keypoints.find(kp => kp.type === KeyPointType.RIGHT_INDEX_FINGER_TIP);
if (!leftWrist || !rightWrist) return null;
this.recordHandTrajectory(leftWrist, rightWrist);
return this.recognizeChartGesture(leftWrist, rightWrist, rightIndex);
}
private recognizeChartGesture(
leftWrist: KeyPoint,
rightWrist: KeyPoint,
rightIndex: KeyPoint | undefined
): ChartCommand | null {
const handDistance = this.calculateDistance(leftWrist, rightWrist);
const handCenterX = (leftWrist.x + rightWrist.x) / 2;
const handCenterY = (leftWrist.y + rightWrist.y) / 2;
// 1. 双手张开 → 放大
if (handDistance > 300) {
this.zoomLevel = Math.min(5, this.zoomLevel + 0.2);
return {
gesture: ChartGesture.ZOOM_IN,
value: this.zoomLevel,
position: { x: handCenterX, y: handCenterY },
confidence: Math.min((handDistance - 300) / 200, 1)
};
}
// 2. 双手捏合 → 缩小
if (handDistance < 80) {
this.zoomLevel = Math.max(0.5, this.zoomLevel - 0.2);
return {
gesture: ChartGesture.ZOOM_OUT,
value: this.zoomLevel,
position: { x: handCenterX, y: handCenterY },
confidence: 1 - (handDistance / 80)
};
}
// 3. 单手滑动 → 平移时间轴
const rightVelocity = this.calculateVelocity(this.rightHandHistory);
if (rightVelocity) {
if (rightVelocity.dx < -80) {
this.timeOffset -= 10;
return {
gesture: ChartGesture.PAN_LEFT,
value: Math.abs(rightVelocity.dx),
position: { x: rightWrist.x, y: rightWrist.y },
confidence: Math.min(Math.abs(rightVelocity.dx) / 200, 1)
};
}
if (rightVelocity.dx > 80) {
this.timeOffset += 10;
return {
gesture: ChartGesture.PAN_RIGHT,
value: Math.abs(rightVelocity.dx),
position: { x: rightWrist.x, y: rightWrist.y },
confidence: Math.min(Math.abs(rightVelocity.dx) / 200, 1)
};
}
}
// 4. 单手上滑/下滑 → 切换指标
if (rightVelocity) {
if (rightVelocity.dy < -100) {
this.currentIndicator = (this.currentIndicator + 1) % this.indicators.length;
return {
gesture: ChartGesture.INDICATOR_UP,
value: this.currentIndicator,
position: { x: rightWrist.x, y: rightWrist.y },
confidence: Math.min(Math.abs(rightVelocity.dy) / 200, 1)
};
}
if (rightVelocity.dy > 100) {
this.currentIndicator = (this.currentIndicator - 1 + this.indicators.length) % this.indicators.length;
return {
gesture: ChartGesture.INDICATOR_DOWN,
value: this.currentIndicator,
position: { x: rightWrist.x, y: rightWrist.y },
confidence: Math.min(Math.abs(rightVelocity.dy) / 200, 1)
};
}
}
// 5. 单指指向 → 十字光标
if (rightIndex) {
return {
gesture: ChartGesture.CROSSHAIR,
value: 1,
position: { x: rightIndex.x, y: rightIndex.y },
confidence: 0.9
};
}
// 6. 双手框选 → 多屏联动
if (handDistance > 150 && handDistance < 300) {
const isFraming = Math.abs(leftWrist.y - rightWrist.y) < 50;
if (isFraming) {
return {
gesture: ChartGesture.MULTI_SCREEN,
value: handDistance,
position: { x: handCenterX, y: handCenterY },
confidence: 0.75
};
}
}
// 7. 画圈 → 刷新
const circle = this.detectCircularMotion(this.rightHandHistory, 'clockwise');
if (circle && circle.confidence > 0.7) {
return {
gesture: ChartGesture.REFRESH,
value: 1,
position: { x: rightWrist.x, y: rightWrist.y },
confidence: circle.confidence
};
}
return null;
}
private detectCircularMotion(
history: HandPosition[],
direction: 'clockwise' | 'counterclockwise'
): { confidence: number; totalAngle: number } | null {
if (history.length < 15) return null;
let totalAngle = 0;
let validSegments = 0;
for (let i = 1; i < history.length; i++) {
const prev = history[i - 1];
const curr = history[i];
const angle1 = Math.atan2(prev.y - 540, prev.x - 960);
const angle2 = Math.atan2(curr.y - 540, curr.x - 960);
let diff = angle2 - angle1;
if (diff > Math.PI) diff -= 2 * Math.PI;
if (diff < -Math.PI) diff += 2 * Math.PI;
const expectedSign = direction === 'clockwise' ? -1 : 1;
if (Math.sign(diff) === expectedSign && Math.abs(diff) > 0.05) {
totalAngle += Math.abs(diff);
validSegments++;
}
}
if (validSegments < 10) return null;
const confidence = Math.min(totalAngle / Math.PI, 1);
return { confidence, totalAngle };
}
private calculateVelocity(history: HandPosition[]): { dx: number; dy: number } | null {
if (history.length < 3) return null;
const recent = history.slice(-3);
const dx = recent[recent.length - 1].x - recent[0].x;
const dy = recent[recent.length - 1].y - recent[0].y;
const dt = (recent[recent.length - 1].timestamp - recent[0].timestamp) / 1000;
if (dt === 0) return null;
return { dx: dx / dt, dy: dy / dt };
}
private recordHandTrajectory(left: KeyPoint, right: KeyPoint): void {
const now = Date.now();
this.leftHandHistory.push({ x: left.x, y: left.y, timestamp: now });
this.rightHandHistory.push({ x: right.x, y: right.y, timestamp: now });
const cutoff = now - 1500;
this.leftHandHistory = this.leftHandHistory.filter(h => h.timestamp > cutoff);
this.rightHandHistory = this.rightHandHistory.filter(h => h.timestamp > cutoff);
}
private calculateDistance(p1: KeyPoint, p2: KeyPoint): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
getCurrentIndicator(): string {
return this.indicators[this.currentIndicator];
}
getZoomLevel(): number {
return this.zoomLevel;
}
getTimeOffset(): number {
return this.timeOffset;
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
interface HandPosition {
x: number;
y: number;
timestamp: number;
}
4.4 沉浸光感交易标题栏(ImmersiveTradingTitleBar.ets)
typescript
// components/ImmersiveTradingTitleBar.ets
import { MarketLightEngine, MarketState, AssetClass, TraderEmotion } from '../engines/MarketLightEngine';
import { RiskAssessment, RiskLevel } from '../systems/EmotionRiskSystem';
@Component
export struct ImmersiveTradingTitleBar {
@Prop currentMarket: MarketState;
@Prop currentAsset: AssetClass;
@Prop accountPnL: number;
@Prop riskAssessment: RiskAssessment;
@Prop totalAssets: number;
@Prop availableBalance: number;
@State theme = MarketLightEngine.getTheme(MarketState.CONSOLIDATION);
@State pulseAnimation: boolean = false;
aboutToAppear(): void {
this.theme = MarketLightEngine.getTheme(this.currentMarket);
setInterval(() => {
this.pulseAnimation = !this.pulseAnimation;
}, this.theme.pulseSpeed / 2);
}
build() {
Row() {
// 左侧:市场状态与资产类别
Row({ space: 12 }) {
Stack() {
Circle()
.width(44)
.height(44)
.fill(this.theme.primaryColor)
.shadow({
radius: this.theme.alertMode ? (this.pulseAnimation ? 25 : 10) : (this.pulseAnimation ? 15 : 6),
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 0
})
.animation({
duration: this.theme.pulseSpeed / 2,
curve: Curve.EaseInOut,
iterations: -1
})
Text(this.getMarketIcon())
.fontSize(22)
}
Column({ space: 4 }) {
Text(this.getAssetName())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(this.getMarketLabel())
.fontSize(12)
.fontColor(this.theme.primaryColor)
}
}
// 中间:账户数据
Row({ space: 20 }) {
// 总盈亏
Column({ space: 2 }) {
Row({ space: 4 }) {
Text(this.accountPnL >= 0 ? '📈' : '📉')
.fontSize(12)
Text(`${this.accountPnL >= 0 ? '+' : ''}${this.accountPnL.toFixed(2)}`)
.fontSize(14)
.fontColor(this.accountPnL >= 0 ? '#22C55E' : '#EF4444')
}
}
// 总资产
Column({ space: 2 }) {
Text('💰')
.fontSize(12)
Text(`${this.totalAssets.toFixed(2)}`)
.fontSize(14)
.fontColor('#FFFFFF')
}
// 可用余额
Column({ space: 2 }) {
Text('💳')
.fontSize(12)
Text(`${this.availableBalance.toFixed(2)}`)
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
}
}
// 右侧:风控状态
Row({ space: 12 }) {
// 风险等级
Row({ space: 4 }) {
Circle()
.width(10)
.height(10)
.fill(this.getRiskColor())
Text(this.getRiskLabel())
.fontSize(11)
.fontColor(this.getRiskColor())
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(this.getRiskBgColor())
.borderRadius(12)
// 情绪状态
Row({ space: 4 }) {
Text(this.getEmotionIcon())
.fontSize(14)
Text(this.getEmotionLabel())
.fontSize(11)
.fontColor(this.getEmotionColor())
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(12)
}
}
.width('100%')
.height(64)
.padding({ left: 24, right: 24 })
.backgroundColor(this.theme.ambientColor)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.borderRadius({ bottomLeft: 20, bottomRight: 20 })
.shadow({
radius: 25,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 6
})
.justifyContent(FlexAlign.SpaceBetween)
}
private getMarketIcon(): string {
const icons: Map<MarketState, string> = new Map([
[MarketState.BULL, '🐂'],
[MarketState.BEAR, '🐻'],
[MarketState.CONSOLIDATION, '📊'],
[MarketState.CRASH, '⚠️'],
[MarketState.REBOUND, '🚀'],
[MarketState.VOLATILE, '⚡']
]);
return icons.get(this.currentMarket) || '📈';
}
private getAssetName(): string {
const names: Map<AssetClass, string> = new Map([
[AssetClass.A_SHARE, 'A股市场'],
[AssetClass.HK_STOCK, '港股市场'],
[AssetClass.US_STOCK, '美股市场'],
[AssetClass.FUTURES, '期货市场'],
[AssetClass.FOREX, '外汇市场'],
[AssetClass.CRYPTO, '加密货币']
]);
return names.get(this.currentAsset) || '未知市场';
}
private getMarketLabel(): string {
const labels: Map<MarketState, string> = new Map([
[MarketState.BULL, '牛市'],
[MarketState.BEAR, '熊市'],
[MarketState.CONSOLIDATION, '震荡'],
[MarketState.CRASH, '崩盘预警'],
[MarketState.REBOUND, '反弹中'],
[MarketState.VOLATILE, '剧烈波动']
]);
return labels.get(this.currentMarket) || '未知';
}
private getRiskColor(): ResourceColor {
const colors: Map<RiskLevel, ResourceColor> = new Map([
[RiskLevel.LOW, '#4ADE80'],
[RiskLevel.MEDIUM, '#FBBF24'],
[RiskLevel.HIGH, '#F97316'],
[RiskLevel.CRITICAL, '#EF4444']
]);
return colors.get(this.riskAssessment.level) || '#4ADE80';
}
private getRiskLabel(): string {
const labels: Map<RiskLevel, string> = new Map([
[RiskLevel.LOW, '低风险'],
[RiskLevel.MEDIUM, '中风险'],
[RiskLevel.HIGH, '高风险'],
[RiskLevel.CRITICAL, '禁止交易']
]);
return labels.get(this.riskAssessment.level) || '未知';
}
private getRiskBgColor(): ResourceColor {
const colors: Map<RiskLevel, ResourceColor> = new Map([
[RiskLevel.LOW, 'rgba(74, 222, 128, 0.15)'],
[RiskLevel.MEDIUM, 'rgba(251, 191, 36, 0.15)'],
[RiskLevel.HIGH, 'rgba(249, 115, 22, 0.15)'],
[RiskLevel.CRITICAL, 'rgba(239, 68, 68, 0.2)']
]);
return colors.get(this.riskAssessment.level) || 'rgba(74, 222, 128, 0.15)';
}
private getEmotionIcon(): string {
const icons: Map<TraderEmotion, string> = new Map([
[TraderEmotion.CALM, '😊'],
[TraderEmotion.GREEDY, '🤩'],
[TraderEmotion.FEARFUL, '😰'],
[TraderEmotion.ANXIOUS, '😰'],
[TraderEmotion.EXCITED, '🤗'],
[TraderEmotion.FATIGUED, '😴']
]);
return icons.get(this.riskAssessment.emotion) || '😐';
}
private getEmotionLabel(): string {
const labels: Map<TraderEmotion, string> = new Map([
[TraderEmotion.CALM, '平静'],
[TraderEmotion.GREEDY, '贪婪'],
[TraderEmotion.FEARFUL, '恐惧'],
[TraderEmotion.ANXIOUS, '焦虑'],
[TraderEmotion.EXCITED, '兴奋'],
[TraderEmotion.FATIGUED, '疲劳']
]);
return labels.get(this.riskAssessment.emotion) || '未知';
}
private getEmotionColor(): ResourceColor {
const colors: Map<TraderEmotion, ResourceColor> = new Map([
[TraderEmotion.CALM, '#4ADE80'],
[TraderEmotion.GREEDY, '#F97316'],
[TraderEmotion.FEARFUL, '#EF4444'],
[TraderEmotion.ANXIOUS, '#FBBF24'],
[TraderEmotion.EXCITED, '#EC4899'],
[TraderEmotion.FATIGUED, '#6B7280']
]);
return colors.get(this.riskAssessment.emotion) || '#888888';
}
}
4.5 悬浮板块导航面板(FloatAssetNav.ets)
typescript
// components/FloatAssetNav.ets
import { HdsTabs, HdsTabBarStyle } from '@hms.core.arkui.design';
import { MarketLightEngine, AssetClass } from '../engines/MarketLightEngine';
@Component
export struct FloatAssetNav {
@Prop currentAsset: AssetClass;
@Prop transparencyLevel: number;
@State selectedIndex: number = 0;
@State theme = MarketLightEngine.getTheme(MarketState.CONSOLIDATION);
private assets: AssetClass[] = [
AssetClass.A_SHARE,
AssetClass.HK_STOCK,
AssetClass.US_STOCK,
AssetClass.FUTURES,
AssetClass.FOREX,
AssetClass.CRYPTO
];
build() {
Column() {
HdsTabs({
barStyle: HdsTabBarStyle.FLOATING,
index: this.selectedIndex,
onChange: (index: number) => {
this.selectedIndex = index;
this.handleAssetChange(this.assets[index]);
}
}) {
ForEach(this.assets, (asset: AssetClass, index: number) => {
TabContent() {
Stack() {}
}
.tabBar(this.buildTabBar(asset, index))
})
}
.barBackgroundColor(`rgba(15, 15, 35, ${this.transparencyLevel})`)
.barActiveColor(this.theme.primaryColor)
.barInactiveColor('#666666')
.barHeight(72)
.barMargin({ left: 48, right: 48, bottom: 20 })
.barBorderRadius(36)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.backdropBlur(20)
}
.width('100%')
.padding({ bottom: 16 })
}
@Builder
buildTabBar(asset: AssetClass, index: number): void {
Column({ space: 4 }) {
Stack() {
Text(this.getAssetIcon(asset))
.fontSize(26)
if (asset === this.currentAsset) {
Circle()
.width(10)
.height(10)
.fill('#00F0FF')
.position({ x: 20, y: -6 })
.shadow({ radius: 6, color: 'rgba(0, 240, 255, 0.6)' })
}
}
Text(this.getAssetShortName(asset))
.fontSize(11)
.fontColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')
}
.width(68)
.height(60)
.justifyContent(FlexAlign.Center)
}
private handleAssetChange(asset: AssetClass): void {
AppStorage.set('switch_asset', asset);
}
private getAssetIcon(asset: AssetClass): string {
const icons: Map<AssetClass, string> = new Map([
[AssetClass.A_SHARE, '🇨🇳'],
[AssetClass.HK_STOCK, '🇭🇰'],
[AssetClass.US_STOCK, '🇺🇸'],
[AssetClass.FUTURES, '📦'],
[AssetClass.FOREX, '💱'],
[AssetClass.CRYPTO, '₿']
]);
return icons.get(asset) || '❓';
}
private getAssetShortName(asset: AssetClass): string {
const names: Map<AssetClass, string> = new Map([
[AssetClass.A_SHARE, 'A股'],
[AssetClass.HK_STOCK, '港股'],
[AssetClass.US_STOCK, '美股'],
[AssetClass.FUTURES, '期货'],
[AssetClass.FOREX, '外汇'],
[AssetClass.CRYPTO, '加密']
]);
return names.get(asset) || '未知';
}
}
4.6 主交易页面(TradingMainPage.ets)
typescript
// pages/TradingMainPage.ets
import { EmotionRiskSystem, RiskAssessment, RiskLevel } from '../systems/EmotionRiskSystem';
import { ChartGestureSystem, ChartCommand, ChartGesture } from '../systems/ChartGestureSystem';
import { MarketLightEngine, MarketState, AssetClass, TraderEmotion } from '../engines/MarketLightEngine';
import { ImmersiveTradingTitleBar } from '../components/ImmersiveTradingTitleBar';
import { FloatAssetNav } from '../components/FloatAssetNav';
@Entry
@Component
struct TradingMainPage {
// AR系统
private riskSystem: EmotionRiskSystem = new EmotionRiskSystem();
private chartSystem: ChartGestureSystem = new ChartGestureSystem();
// 市场状态
@State currentMarket: MarketState = MarketState.CONSOLIDATION;
@State currentAsset: AssetClass = AssetClass.A_SHARE;
@State accountPnL: number = 12580.5;
@State totalAssets: number = 500000;
@State availableBalance: number = 150000;
// 风控数据
@State riskAssessment: RiskAssessment = {
level: RiskLevel.LOW,
score: 15,
emotion: TraderEmotion.CALM,
triggers: [],
suggestedAction: '正常交易',
cooldownSeconds: 0
};
// 图表状态
@State currentGesture: ChartCommand | null = null;
@State zoomLevel: number = 1;
@State currentIndicator: string = 'MA';
// 多窗口
@State showOrderBook: boolean = false;
@State showPosition: boolean = false;
@State showBacktest: boolean = false;
@State showAlert: boolean = false;
// K线数据(模拟)
@State klineData: KLineData[] = this.generateMockKLine();
aboutToAppear(): void {
this.setupImmersiveWindow();
this.initializeTradingSystems();
this.setupEventListeners();
}
private setupImmersiveWindow(): void {
const window = windowStage.getMainWindowSync();
window.setWindowLayoutFullScreen(true);
window.setWindowBackgroundColor('#0A0A0F');
}
private async initializeTradingSystems(): Promise<void> {
try {
await this.riskSystem.initialize();
await this.chartSystem.initialize();
this.startTradingLoop();
} catch (err) {
console.error('交易系统初始化失败:', err);
}
}
private async startTradingLoop(): Promise<void> {
const loop = async () => {
try {
const frame = await this.riskSystem.session?.getCurrentFrame();
if (!frame) {
requestAnimationFrame(loop);
return;
}
// Face AR:情绪风控
const risk = this.riskSystem.update(frame);
if (risk) {
this.riskAssessment = risk;
this.updateTradingLighting();
}
// Body AR:图表操控
const gesture = this.chartSystem.update(frame);
if (gesture) {
this.currentGesture = gesture;
this.handleChartGesture(gesture);
} else {
this.currentGesture = null;
}
} catch (err) {
console.error('交易循环错误:', err);
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
private updateTradingLighting(): void {
const baseTheme = MarketLightEngine.getTheme(this.currentMarket);
const pnlTheme = MarketLightEngine.adjustByPnL(baseTheme, this.accountPnL);
const finalTheme = MarketLightEngine.adjustByEmotion(pnlTheme, this.riskAssessment.emotion);
AppStorage.set('current_trading_theme', finalTheme);
}
private handleChartGesture(gesture: ChartCommand): void {
switch (gesture.gesture) {
case ChartGesture.ZOOM_IN:
case ChartGesture.ZOOM_OUT:
this.zoomLevel = gesture.value;
break;
case ChartGesture.INDICATOR_UP:
case ChartGesture.INDICATOR_DOWN:
this.currentIndicator = this.chartSystem.getCurrentIndicator();
break;
case ChartGesture.MULTI_SCREEN:
AppStorage.set('multi_screen_sync', gesture.position);
break;
case ChartGesture.REFRESH:
this.klineData = this.generateMockKLine();
break;
}
}
private setupEventListeners(): void {
AppStorage.watch('switch_asset', (asset: AssetClass) => {
this.currentAsset = asset;
});
}
private generateMockKLine(): KLineData[] {
const data: KLineData[] = [];
let price = 100;
for (let i = 0; i < 100; i++) {
const change = (Math.random() - 0.5) * 10;
const open = price;
const close = price + change;
const high = Math.max(open, close) + Math.random() * 5;
const low = Math.min(open, close) - Math.random() * 5;
data.push({ open, close, high, low, volume: Math.random() * 10000 });
price = close;
}
return data;
}
build() {
Stack() {
// 背景环境光
Column()
.width('100%')
.height('100%')
.backgroundColor(MarketLightEngine.getTheme(this.currentMarket).ambientColor)
.animation({
duration: 1000,
curve: Curve.EaseInOut
})
// 主K线图
Column() {
KLineChart({
data: this.klineData,
zoom: this.zoomLevel,
indicator: this.currentIndicator,
bullColor: MarketLightEngine.getTheme(this.currentMarket).bullColor,
bearColor: MarketLightEngine.getTheme(this.currentMarket).bearColor
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
// 手势提示
if (this.currentGesture) {
ChartGestureHint({
gesture: this.currentGesture
})
.position({ x: '50%', y: '85%' })
.translate({ x: '-50%' })
}
// 风控告警弹窗
if (this.riskAssessment.level === RiskLevel.CRITICAL) {
RiskAlertOverlay({
risk: this.riskAssessment,
cooldown: this.riskSystem.getCooldownRemaining()
})
.width('100%')
.height('100%')
}
// 沉浸光感标题栏
ImmersiveTradingTitleBar({
currentMarket: this.currentMarket,
currentAsset: this.currentAsset,
accountPnL: this.accountPnL,
riskAssessment: this.riskAssessment,
totalAssets: this.totalAssets,
availableBalance: this.availableBalance
})
.position({ x: 0, y: 0 })
.zIndex(100)
// 浮动订单簿
if (this.showOrderBook) {
FloatOrderBook({
onClose: () => {
this.showOrderBook = false;
}
})
.position({ x: '2%', y: '15%' })
.width(280)
.height('70%')
.zIndex(90)
}
// 浮动持仓面板
if (this.showPosition) {
FloatPositionPanel({
onClose: () => {
this.showPosition = false;
}
})
.position({ x: '78%', y: '15%' })
.width(280)
.height('70%')
.zIndex(90)
}
// 底部悬浮板块导航
FloatAssetNav({
currentAsset: this.currentAsset,
transparencyLevel: 0.65
})
.position({ x: 0, y: '100%' })
.translate({ y: -88 })
.zIndex(100)
}
.width('100%')
.height('100%')
.backgroundColor('#0A0A0F')
}
aboutToDisappear(): void {
this.riskSystem.release();
this.chartSystem.release();
}
}
// K线数据接口
interface KLineData {
open: number;
close: number;
high: number;
low: number;
volume: number;
}
// K线图组件
@Component
struct KLineChart {
@Prop data: KLineData[];
@Prop zoom: number;
@Prop indicator: string;
@Prop bullColor: ResourceColor;
@Prop bearColor: ResourceColor;
build() {
Canvas(this.renderKLine)
.width('100%')
.height('100%')
.backgroundColor('transparent')
}
private renderKLine = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
// 绘制网格
context.strokeStyle = 'rgba(255,255,255,0.05)';
context.lineWidth = 1;
for (let i = 0; i < 10; i++) {
context.beginPath();
context.moveTo(0, h * i / 10);
context.lineTo(w, h * i / 10);
context.stroke();
}
// 绘制K线
const candleWidth = (w / this.data.length) * this.zoom;
const startIndex = Math.max(0, this.data.length - Math.floor(w / candleWidth));
for (let i = startIndex; i < this.data.length; i++) {
const d = this.data[i];
const x = (i - startIndex) * candleWidth + candleWidth / 2;
const isBull = d.close >= d.open;
// 计算Y坐标
const maxPrice = Math.max(...this.data.slice(startIndex).map(d => d.high));
const minPrice = Math.min(...this.data.slice(startIndex).map(d => d.low));
const priceRange = maxPrice - minPrice;
const yHigh = h - ((d.high - minPrice) / priceRange) * h * 0.8 - h * 0.1;
const yLow = h - ((d.low - minPrice) / priceRange) * h * 0.8 - h * 0.1;
const yOpen = h - ((d.open - minPrice) / priceRange) * h * 0.8 - h * 0.1;
const yClose = h - ((d.close - minPrice) / priceRange) * h * 0.8 - h * 0.1;
// 绘制影线
context.strokeStyle = isBull ? this.bullColor as string : this.bearColor as string;
context.lineWidth = 1;
context.beginPath();
context.moveTo(x, yHigh);
context.lineTo(x, yLow);
context.stroke();
// 绘制实体
context.fillStyle = isBull ? this.bullColor as string : this.bearColor as string;
const bodyTop = Math.min(yOpen, yClose);
const bodyHeight = Math.abs(yOpen - yClose);
context.fillRect(x - candleWidth * 0.4, bodyTop, candleWidth * 0.8, Math.max(bodyHeight, 1));
}
// 绘制指标
context.fillStyle = 'rgba(255,255,255,0.5)';
context.font = '12px sans-serif';
context.fillText(`指标: ${this.indicator}`, 10, 20);
};
}
// 图表手势提示组件
@Component
struct ChartGestureHint {
@Prop gesture: ChartCommand;
build() {
Column({ space: 8 }) {
Text(this.getGestureIcon(this.gesture.gesture))
.fontSize(32)
Text(this.getGestureLabel(this.gesture.gesture))
.fontSize(14)
.fontColor('#FFFFFF')
}
.padding(16)
.backgroundColor('rgba(0, 0, 0, 0.6)')
.borderRadius(16)
.backdropBlur(10)
}
private getGestureIcon(gesture: ChartGesture): string {
const icons: Map<ChartGesture, string> = new Map([
[ChartGesture.ZOOM_IN, '🔍'],
[ChartGesture.ZOOM_OUT, '🔎'],
[ChartGesture.PAN_LEFT, '⬅️'],
[ChartGesture.PAN_RIGHT, '➡️'],
[ChartGesture.INDICATOR_UP, '⬆️'],
[ChartGesture.INDICATOR_DOWN, '⬇️'],
[ChartGesture.CROSSHAIR, '👆'],
[ChartGesture.MULTI_SCREEN, '🖥️'],
[ChartGesture.REFRESH, '🔄']
]);
return icons.get(gesture) || '👋';
}
private getGestureLabel(gesture: ChartGesture): string {
const labels: Map<ChartGesture, string> = new Map([
[ChartGesture.ZOOM_IN, '放大K线'],
[ChartGesture.ZOOM_OUT, '缩小K线'],
[ChartGesture.PAN_LEFT, '查看历史'],
[ChartGesture.PAN_RIGHT, '查看最新'],
[ChartGesture.INDICATOR_UP, '上一个指标'],
[ChartGesture.INDICATOR_DOWN, '下一个指标'],
[ChartGesture.CROSSHAIR, '十字光标'],
[ChartGesture.MULTI_SCREEN, '多屏联动'],
[ChartGesture.REFRESH, '刷新数据']
]);
return labels.get(gesture) || '手势操控';
}
}
// 风控告警覆盖层
@Component
struct RiskAlertOverlay {
@Prop risk: RiskAssessment;
@Prop cooldown: number;
build() {
Column({ space: 20 }) {
Text('⚠️ 交易风险告警')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#EF4444')
.shadow({
radius: 20,
color: '#EF4444',
offsetX: 0,
offsetY: 0
})
Text(`风险等级: ${this.getRiskLabel(this.risk.level)}`)
.fontSize(20)
.fontColor('#FFFFFF')
Text(`风险分数: ${this.risk.score}/100`)
.fontSize(16)
.fontColor('#FBBF24')
// 触发因素
Column({ space: 8 }) {
ForEach(this.risk.triggers, (trigger: string) => {
Text(`• ${trigger}`)
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
})
}
Text(this.risk.suggestedAction)
.fontSize(16)
.fontColor('#4ADE80')
.padding(12)
.backgroundColor('rgba(74, 222, 128, 0.15)')
.borderRadius(12)
if (this.cooldown > 0) {
Text(`冷却倒计时: ${this.cooldown}秒`)
.fontSize(18)
.fontColor('#EF4444')
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('rgba(0, 0, 0, 0.85)')
.backdropBlur(20)
}
private getRiskLabel(level: RiskLevel): string {
const labels: Map<RiskLevel, string> = new Map([
[RiskLevel.LOW, '低风险'],
[RiskLevel.MEDIUM, '中风险'],
[RiskLevel.HIGH, '高风险'],
[RiskLevel.CRITICAL, '极高风险 - 禁止交易']
]);
return labels.get(level) || '未知';
}
}
// 浮动订单簿组件
@Component
struct FloatOrderBook {
@Prop onClose: () => void;
private mockOrders: Order[] = [
{ price: 100.5, volume: 1500, type: 'buy' },
{ price: 100.3, volume: 2300, type: 'buy' },
{ price: 100.1, volume: 800, type: 'buy' },
{ price: 100.0, volume: 5000, type: 'sell' },
{ price: 99.8, volume: 1200, type: 'sell' },
{ price: 99.6, volume: 3400, type: 'sell' }
];
build() {
Column({ space: 16 }) {
Row() {
Text('📋 订单簿')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 买卖盘
Column({ space: 4 }) {
// 卖盘
ForEach(this.mockOrders.filter(o => o.type === 'sell').reverse(), (order: Order) => {
Row({ space: 8 }) {
Text(order.price.toFixed(2))
.fontSize(14)
.fontColor('#EF4444')
.width(60)
Progress({ value: order.volume, total: 5000, type: ProgressType.Linear })
.width(100)
.height(4)
.color('rgba(239, 68, 68, 0.5)')
.backgroundColor('rgba(255,255,255,0.1)')
Text(`${order.volume}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
}
.width('100%')
})
// 分隔线
Divider()
.color('rgba(255,255,255,0.2)')
.margin({ top: 8, bottom: 8 })
// 买盘
ForEach(this.mockOrders.filter(o => o.type === 'buy'), (order: Order) => {
Row({ space: 8 }) {
Text(order.price.toFixed(2))
.fontSize(14)
.fontColor('#22C55E')
.width(60)
Progress({ value: order.volume, total: 5000, type: ProgressType.Linear })
.width(100)
.height(4)
.color('rgba(34, 197, 94, 0.5)')
.backgroundColor('rgba(255,255,255,0.1)')
Text(`${order.volume}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
}
.width('100%')
})
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
}
interface Order {
price: number;
volume: number;
type: 'buy' | 'sell';
}
// 浮动持仓面板组件
@Component
struct FloatPositionPanel {
@Prop onClose: () => void;
private mockPositions: Position[] = [
{ symbol: '600519', name: '贵州茅台', quantity: 100, cost: 1680, current: 1750, pnl: 7000 },
{ symbol: '000858', name: '五粮液', quantity: 200, cost: 145, current: 152, pnl: 1400 },
{ symbol: '300750', name: '宁德时代', quantity: 50, cost: 380, current: 365, pnl: -750 }
];
build() {
Column({ space: 16 }) {
Row() {
Text('💼 持仓')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
List({ space: 8 }) {
ForEach(this.mockPositions, (pos: Position) => {
ListItem() {
Column({ space: 6 }) {
Row({ space: 8 }) {
Text(pos.symbol)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
Text(pos.name)
.fontSize(14)
.fontColor('#FFFFFF')
}
Row({ space: 8 }) {
Text(`${pos.quantity}股`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
Text(`成本: ${pos.cost.toFixed(2)}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
Text(`现价: ${pos.current.toFixed(2)}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
}
Text(`${pos.pnl >= 0 ? '+' : ''}${pos.pnl.toFixed(2)}`)
.fontSize(16)
.fontColor(pos.pnl >= 0 ? '#22C55E' : '#EF4444')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(12)
.backgroundColor(pos.pnl >= 0 ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)')
.borderRadius(12)
}
})
}
.width('100%')
.layoutWeight(1)
// 总盈亏
Row({ space: 8 }) {
Text('总盈亏:')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
Text(`+${this.mockPositions.reduce((sum, p) => sum + p.pnl, 0).toFixed(2)}`)
.fontSize(18)
.fontColor('#22C55E')
.fontWeight(FontWeight.Bold)
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
}
interface Position {
symbol: string;
name: string;
quantity: number;
cost: number;
current: number;
pnl: number;
}
五、关键技术总结
5.1 Face AR在交易风控中的适配清单
| 适配项 | 说明 | 代码位置 |
|---|---|---|
| 贪婪检测 | 大笑 + 瞳孔放大 + 眨眼减少 | EmotionRiskSystem.assessRisk() |
| 恐惧检测 | 眉毛上扬 + 嘴巴张开 + 鼻皱 | EmotionRiskSystem.assessRisk() |
| 报复性交易 | 连续恐惧表情检测 | EmotionRiskSystem.assessRisk() |
| 交易冷却 | 风险等级对应不同冷却时间 | EmotionRiskSystem.tradeCooldown |
5.2 Body AR在图表操控中的最佳实践
| 实践项 | 说明 | 代码位置 |
|---|---|---|
| K线缩放 | 双手张合控制缩放级别 | ChartGestureSystem.recognizeChartGesture() |
| 时间轴滑动 | 单手水平滑动平移 | ChartGestureSystem.calculateVelocity() |
| 指标切换 | 单手垂直滑动切换 | ChartGestureSystem.recognizeChartGesture() |
| 多屏联动 | 双手框选区域同步 | ChartGestureSystem.recognizeChartGesture() |
5.3 沉浸光感交易适配要点
- 行情色动态切换:牛市红、熊市青、震荡黄、崩盘闪烁红、反弹绿、波动紫
- 盈亏反馈:持仓盈利泛绿光、亏损泛红光,实时反馈账户状态
- 情绪响应:贪婪时橙色警示、恐惧时紫蓝安抚、焦虑时暖黄缓解
- 告警模式:崩盘和剧烈波动时快速脉冲闪烁,吸引交易员注意力
六、调试与测试建议
6.1 AR性能监控
typescript
const startTime = performance.now();
// ... AR处理逻辑
const processTime = performance.now() - startTime;
if (processTime > 33) {
console.warn(`AR处理帧耗时${processTime.toFixed(1)}ms,存在掉帧风险`);
}
6.2 多窗口测试矩阵
| 测试场景 | 预期结果 |
|---|---|
| 主K线图 + 浮动订单簿 + 浮动持仓 | 标题栏光效同步,面板不遮挡主图 |
| 风控告警触发时 | 全屏覆盖告警,冷却倒计时准确 |
| 多板块切换 | 光效随市场状态平滑过渡 |
| 手势操控图表 | K线缩放流畅,指标切换即时响应 |
6.3 常见问题排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 情绪识别不准确 | 光线不足导致面部特征模糊 | 确保交易环境光线充足 |
| 图表缩放不灵敏 | 手势幅度过小 | 建议双手张合幅度大于30cm |
| 光效切换闪烁 | 动画时长过短 | 调整duration至1000ms以上 |
| 风控误触发 | 正常表情被识别为风险 | 调整BlendShape阈值参数 |
七、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航 、沉浸光感 与Face AR & Body AR特性,完整实战了一款PC端"灵犀智投"量化交易分析工作台。核心创新点总结:
- 行情感知光效系统:牛市红、熊市青、震荡黄、崩盘闪烁红、反弹绿、波动紫,实时反映市场状态
- Face AR情绪风控:贪婪检测自动增加确认步骤、恐惧检测暂停卖出、疲劳检测建议休息、连续情绪异常触发交易冷却
- Body AR图表操控:双手张合缩放K线、单手滑动平移时间轴、垂直滑动切换指标、画圈刷新数据
- 盈亏可视化反馈:持仓盈利泛绿光、亏损泛红光,直观反馈账户状态
- 悬浮导航自适应 :采用
HdsTabs悬浮样式,四周留白,支持透明度三档调节 - PC级多窗口协作:主K线图 + 浮动订单簿 + 浮动持仓面板 + 浮动策略回测 + 浮动风控告警
未来扩展方向:
- AI量化策略:结合情绪数据训练交易决策模型,实现情绪因子量化
- 分布式交易:通过鸿蒙分布式软总线,实现多设备同步盯盘和交易
- 语音交易指令:结合语音助手,实现"买入100股茅台"等语音指令
- 社交化交易:交易员之间共享情绪状态和交易策略,形成交易社区
- 区块链资产:扩展支持DeFi、NFT等新型数字资产的交易和分析
转载自:https://blog.csdn.net/u014727709/article/details/161085788
欢迎 👍点赞✍评论⭐收藏,欢迎指正