HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与Face AR & Body AR的“灵犀智投“——PC端沉浸式AR量化交易分析工作台

文章目录

    • 每日一句正能量
    • 前言
    • 一、前言:量化交易的交互范式革新
    • 二、核心特性解析与技术选型
      • [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 沉浸光感交易适配要点

  1. 行情色动态切换:牛市红、熊市青、震荡黄、崩盘闪烁红、反弹绿、波动紫
  2. 盈亏反馈:持仓盈利泛绿光、亏损泛红光,实时反馈账户状态
  3. 情绪响应:贪婪时橙色警示、恐惧时紫蓝安抚、焦虑时暖黄缓解
  4. 告警模式:崩盘和剧烈波动时快速脉冲闪烁,吸引交易员注意力

六、调试与测试建议

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端"灵犀智投"量化交易分析工作台。核心创新点总结:

  1. 行情感知光效系统:牛市红、熊市青、震荡黄、崩盘闪烁红、反弹绿、波动紫,实时反映市场状态
  2. Face AR情绪风控:贪婪检测自动增加确认步骤、恐惧检测暂停卖出、疲劳检测建议休息、连续情绪异常触发交易冷却
  3. Body AR图表操控:双手张合缩放K线、单手滑动平移时间轴、垂直滑动切换指标、画圈刷新数据
  4. 盈亏可视化反馈:持仓盈利泛绿光、亏损泛红光,直观反馈账户状态
  5. 悬浮导航自适应 :采用HdsTabs悬浮样式,四周留白,支持透明度三档调节
  6. PC级多窗口协作:主K线图 + 浮动订单簿 + 浮动持仓面板 + 浮动策略回测 + 浮动风控告警

未来扩展方向

  • AI量化策略:结合情绪数据训练交易决策模型,实现情绪因子量化
  • 分布式交易:通过鸿蒙分布式软总线,实现多设备同步盯盘和交易
  • 语音交易指令:结合语音助手,实现"买入100股茅台"等语音指令
  • 社交化交易:交易员之间共享情绪状态和交易策略,形成交易社区
  • 区块链资产:扩展支持DeFi、NFT等新型数字资产的交易和分析

转载自:https://blog.csdn.net/u014727709/article/details/161085788

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
特立独行的猫a1 小时前
鸿蒙 PC 三方库移植实战 · 直播课件(详细教案)
华为·harmonyos·移植·鸿蒙pc·opendesk
xmdy58663 小时前
Flutter+开源鸿蒙实战|企业级工具APP Day2 全局网络封装与 Dio 拦截器实战(鸿蒙兼容版)
flutter·开源·harmonyos
xmdy58663 小时前
Flutter+开源鸿蒙实战:企业级工具类APP开发教程(含第三方库适配)
flutter·开源·harmonyos
richard_yuu4 小时前
鸿蒙Stage模型实战|心晴驿站分层架构与隐私安全设计
安全·架构·harmonyos
Swift社区4 小时前
Flutter / React / ArkUI:在鸿蒙 PC 上怎么选?
flutter·react.js·harmonyos
ZHW_AI课题组4 小时前
调用华为智能云API实现手写图片识别
图像处理·python·机器学习·华为·分类
leon_teacher5 小时前
HarmonyOS 6 鸿蒙APP应用实战:基于 ArkUI V2 打造儿童古诗学习宝 App 从 0 到 1
学习·华为·harmonyos
想你依然心痛5 小时前
HarmonyOS 6(API 23)实战:基于Face AR数字人驱动与Body AR手势控制的“星播工坊“——PC端沉浸式虚拟直播系统
华为·ar·harmonyos·悬浮导航·沉浸光感
哦***76 小时前
真实评测 | FreeBuds Pro 5独立空间音频
华为·音频·harmonyos