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

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

相关推荐
袁震1 小时前
HarmonyOS 7 深色模式与全局换肤实战:一套色板管到底
华为·harmonyos
李游Leo1 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》05:Camera控制、手势映射与空间浏览交互【鸿蒙心迹】
3d·交互·harmonyos
resh_people2 小时前
开源鸿蒙平台 KMP/CMP 三方库「Okio」适配全流程
华为·开源·harmonyos
李游Leo2 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》08:动画、生命周期与SpaceRoom工程化收尾【鸿蒙心迹】
3d·harmonyos
-madongyu-3 小时前
HCIE-GaussDB V2.0 笔试考点总览:模块权重、核心要点与高频数字速记
数据库·华为·gaussdb
轻口味3 小时前
HarmonyOS 7 新特性2:音频编创——轻音台里的降噪、环绕与格式转换
华为·音视频·harmonyos·鸿蒙·音频编创
极客范儿3 小时前
华为HCIP网络工程师认证—DHCP、NAT和 PPPOE
网络·华为
m0_738185823 小时前
Flutter 鸿蒙化实战:flutter_blue_plus 适配 OpenHarmony,蓝牙扫描连接开箱即用
flutter·华为·harmonyos·鸿蒙
李游Leo3 小时前
HarmonyOS 7 + ArkUI + Adaptive Layout 学习笔记:折叠屏多形态布局适配与窗口状态响应机制【鸿蒙心迹】
笔记·学习·harmonyos
贾伟康4 小时前
【HarmonyOS 7新能力|067】LTPO可变帧率实战:让动画流畅而静态页面省电
harmonyos·arkts·低功耗·ltpo·可变帧率