Skill: acevod/shadow-binance-bot (version 1.3.3)

```yaml --- name: shadow-binance-bot description: AI-powered read-only trading analysis coach that simulates alternative strategies against your Binance trade history. version: 1.3.3 author: acevod homepage: https://github.com/acevod/Shadow-Binance-Bot license: MIT metadata: openclaw: emoji: 🔍 always: false requires: env: - BINANCE_API_KEY - BINANCE_API_SECRET - SPOT_SYMBOLS bins: - node >=18.0.0 security: credentials: - BINANCE_API_KEY - BINANCE_API_SECRET posture: read-only trading: false withdrawal: false registry: platform: openclaw runtime: node >=18.0.0 env: - BINANCE_API_KEY (required) — Binance read-only API key - BINANCE_API_SECRET (required) — Binance read-only API secret - SPOT_SYMBOLS (optional) — comma-separated list of spot symbols to analyze (default: BTCUSDT,ETHUSDT,BNBUSDT) capabilities: - read-only - no-trading - no-withdrawal requires: env: - BINANCE_API_KEY - BINANCE_API_SECRET - SPOT_SYMBOLS binaries: - node >=18.0.0 files: - src/index.cjs - src/binance.cjs - src/analyzer.cjs - src/shadowSim.cjs - src/coach.cjs - tests/analyzer.test.cjs - tests/shadowSim.test.cjs - config.env.example - package.json - CHANGELOG.md - CONTRIBUTING.md - LICENSE - README.md - .editorconfig --- # Shadow Mode Trading Trainer

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

---

## [v1.3.3] - 2026-04-06

### Fixed — Registry Metadata Integrity (Final)
- Added `metadata.openclaw` block to SKILL.md YAML frontmatter with explicit:
  - `requires.env` listing all three environment variables
  - `requires.bins` listing Node.js >=18 runtime requirement
  - `security.credentials` declaring BINANCE_API_KEY and BINANCE_API_SECRET
  - `security.posture: read-only`, `trading: false`, `withdrawal: false`
- This ensures the registry-level metadata shown at the top of submission is **identical** to what SKILL.md requires, closing the last remaining gap
- The `registry:` block (added in v1.3.2) now has a parallel `metadata.openclaw.requires` section so **both** the platform's registry summary **and** the openclaw metadata block declare the same credentials and runtime requirements

### Security Posture (now explicit in metadata)
- `metadata.openclaw.security` declares: read-only posture, no trading, no withdrawal
- Platform operators can confirm the skill only reads Binance data and never places orders or moves funds

---

## [v1.3.2] - 2026-04-06

### Fixed — Registry Metadata Integrity
- Added explicit `registry:` block to SKILL.md metadata containing:
  - `platform: openclaw` and `runtime: node >=18.0.0`
  - Full credential descriptions (`BINANCE_API_KEY (required)`, etc.)
  - Security posture (`capabilities: read-only, no-trading, no-withdrawal`)
- This addresses the LLM scanner concern about a mismatch between "registry metadata shown to the platform" vs. SKILL.md
- The `requires:` section continues to list env vars and binaries as before for backward compatibility

### Security
- Added `license: MIT` to SKILL.md metadata block
- `registry.capabilities` now explicitly declares read-only/no-trading/no-withdrawal posture
- SKILL.md and README already recommend read-only API keys and IP restriction — this update makes that explicit in registry metadata

---

## [v1.3.1] - 2026-04-05

### Fixed — Registry Metadata Consistency
- SKILL.md registry metadata now includes `binaries: node >=18.0.0` requirement (was previously omitted)
- SKILL.md `requires.env` section already correctly listed `BINANCE_API_KEY`, `BINANCE_API_SECRET`, and `SPOT_SYMBOLS` — no change needed there
- Version bumped to `1.3.1` to reflect metadata correction

### Security
- Registry metadata now accurately reflects runtime requirements (Node.js) and credentials (env vars)
- No code changes — metadata only

---

## [v1.3.0] - 2026-04-04

### Fixed — Credentials & Platform Compatibility
- `src/index.cjs` now reads `process.env` first for `BINANCE_API_KEY`, `BINANCE_API_SECRET`, and `SPOT_SYMBOLS`. Platforms that inject credentials as environment variables will work automatically. Falls back to `config.env` for local development.
- SKILL.md and README updated to document both credential methods (env vars and config file).
- README now clearly shows Method 1 (env vars) as the recommended approach, with Method 2 (config file) as a local dev alternative.
- `config.env.example` clarifies the file is optional when env vars are set.

### Security
- SKILL.md Security & Privacy section updated to reflect dual credential method.

---

## [v1.2.0] - 2026-04-04

### Added
- **Demo Mode** with realistic mock data when no API keys are configured
- **Configurable Spot symbols** via `SPOT_SYMBOLS` env var in `config.env`
- **GitHub Actions CI** workflow (`.github/workflows/test.yml`) for automated testing
- **CODEOWNERS** file for PR routing
- **`SPOT_SYMBOLS` to `requires.env`** in SKILL.md

### Fixed
- `SKILL.md` moved from `skill/SKILL.md` to repo root (ClawHub requirement)
- `.gitignore` title comment removed (was breaking standard format)
- README step numbering fixed for Binance API key creation instructions
- README project structure updated to reflect `SKILL.md` at root and `tests/` directory
- CONTRIBUTING.md updated — "if tests exist" hedge removed (69 tests now exist)

### Security
- `config.env.example` now includes IP restriction reminder

---

## [v1.1.0] - 2026-04-04

### Added
- Unit tests for `analyzer.cjs` (22 tests covering futures/spot/behavior analysis)
- Unit tests for `shadowSim.cjs` (47 tests covering all strategy simulators)
- `.editorconfig` for consistent coding style across editors

### Fixed
- `simulateSelectiveTrading` now uses real trade counts per hour (`h.total`) instead of hardcoded estimates
- `simulateSpotDCA` gracefully handles missing `totalVolume` data
- `analyzeByHour` no longer mutates the original `pnlTrades` array passed in
- `getAllSpotTrades` now returns errors alongside partial results instead of silently skipping
- `binance.cjs` HTTPS requests now have a 10-second timeout (prevents indefinite hangs)
- `getMotivation` conditional boundaries clarified (no overlapping conditions)
- All magic number thresholds moved to named constants in `coach.cjs`
- All source files are now properly separated (were previously concatenated in git)

### Changed
- Console output changed from Unicode box-drawing chars to ASCII-compatible characters
- `package.json` `npm test` now runs the actual test suites
- Version bumped to `1.1.0` to reflect significant fixes and additions

### Security
- Request timeouts prevent indefinite resource consumption
- Improved error reporting surfaces partial API failures

---

## [v1.0.0] - 2026-03-11

### Added
- Initial release
- Basic Shadow Binance Bot functionality
- Futures income analysis with win/loss tracking
- Spot trade analysis for multiple symbols
- Shadow strategy simulation engine
- AI coaching feedback with behavioral pattern detection
- Demo Mode for when API keys are not configured

### Notes
- First public version


path: CONTRIBUTING.md
content: # Contributing to Shadow Binance Bot

Thank you for your interest in contributing!

## How to Contribute

### Reporting Issues

Found a bug or have a feature request? Open an issue on GitHub with a clear description and any relevant logs or screenshots.

### Pull Requests

1. **Fork the repository** and create a new branch for your feature or fix.
2. Keep changes **focused and small** — one feature or fix per PR.
3. Write a clear PR description explaining **what** changed and **why**.
4. Test your changes locally before submitting.
5. Ensure `npm test` passes.

### Code Style

- Use **2-space indentation** for consistency.
- Add JSDoc comments for all public functions.
- Keep lines under **120 characters**.
- No trailing whitespace.
- This project uses **EditorConfig**. Please install the plugin for your editor.

### Security

- **Never commit API keys, secrets, or credentials.**
- Use environment variables or a local `config.env` (already in `.gitignore`).
- If you discover a security vulnerability, please report it privately rather than as a public issue.

### Scope

This tool is a **read-only analysis and coaching** skill. It does not:
- Execute trades automatically
- Access withdrawal or deposit functions
- Store user trading data externally

Large changes should be discussed via a GitHub issue before submitting a PR.

---

_By contributing, you agree that your contributions will be licensed under the MIT License._


path: README.md
content: # Shadow Binance Bot

![License](https://img.shields.io/badge/license-MIT-green) ![OpenClaw](https://img.shields.io/badge/OpenClaw-AI%20Agent-purple) ![Binance](https://img.shields.io/badge/Binance-API-yellow) ![AI](https://img.shields.io/badge/AI-Trading%20Coach-blue)

AI-powered trading coach that analyzes your Binance trades and shows how alternative strategies could have improved your results.

Instead of guessing what went wrong, traders can see a simulated "shadow strategy" running alongside their real trades.

This transforms trading mistakes into structured learning.

---

## Problem

Most crypto traders lose money not because of lack of information, but because of:

- FOMO entries
- Panic selling
- Over-leveraging
- Poor risk management
- Emotional trading decisions

Even experienced traders struggle to objectively analyze their past trades.

---

## Solution

Shadow Mode Trading Trainer analyzes a trader’s Binance history and runs parallel strategy simulations.

It compares:

**Real Trades**  
vs  
**Shadow Strategy Performance**

This allows traders to learn:

- how different entries would change results
- how position sizing affects risk
- how emotional trading impacts performance

The system acts as a **trading coach instead of a signal generator**.

---

## Shadow Strategy Simulation

![Shadow Strategy Simulation](assets/shadow-simulation.png)

This system analyzes a trader's real trade history and simulates alternative strategies in a "shadow mode".

By comparing real trades with AI-simulated strategies, the system can identify:

- missed opportunities
- emotional trading patterns
- better entry and exit strategies
- potential performance improvements

---

## Key Features

### Portfolio Analysis

Analyze Binance Spot and Futures positions including:

- entry timing
- position size
- profit and loss
- leverage usage

### Shadow Strategy Simulation

Run alternative strategies on historical trades:

- Dollar Cost Averaging (DCA)
- support level entries
- reduced leverage
- improved stop-loss placement

### Emotional Trading Detection

Detect patterns such as:

- FOMO entries
- panic selling
- revenge trading
- over-leveraging

### AI Coaching Feedback

Provide constructive insights including:

- behavioral patterns
- strategy improvement suggestions
- risk management advice

---

## Use Cases

Shadow Binance Bot can help traders:

- understand why certain trades failed
- simulate alternative strategies
- detect emotional trading behavior
- improve long-term trading discipline

---

## How It Works

1. Connect Binance account (read-only API)
2. Retrieve trading history
3. Analyze real trading behavior
4. Detect emotional trading patterns
5. Run simulated alternative strategies
6. Compare results and generate coaching insights

---

## System Architecture

![Shadow Binance Bot Architecture](assets/architecture.png)

---

## Tech Stack

- OpenClaw AI Agent Framework
- Binance API
- Large Language Models
- Trading Strategy Simulation

---

## Example Output

**Portfolio Summary**

BTCUSDT position: +4.2%  
ETHUSDT position: -2.1%

**Shadow Simulation**

BTCUSDT alternative strategy: +6.8%  
ETHUSDT alternative strategy: +3.5%

**Key Observations**

- FOMO entry detected on BTCUSDT  
- Panic exit detected on ETHUSDT

**Coaching Insight**

Your entries often occur after strong price momentum.

**Suggested Improvements**

- wait for pullbacks before entering  
- reduce leverage  
- define stop-loss levels before opening trades

---

## Demo Mode

If Binance API keys are not configured, the system runs in **Demo Mode**.

Demo Mode simulates example portfolios to demonstrate how the shadow strategy engine works.

This allows users to understand the concept without connecting real accounts.

---

## Safety and Risk Awareness

This project promotes responsible trading practices.

The system encourages:

- controlled risk per trade
- disciplined entries
- avoidance of emotional trading
- long-term strategy improvement

This project **does not provide financial advice or trading signals**.

---

## Project Structure

```
Shadow-Binance-Bot/
  README.md
  LICENSE
  package.json
  SKILL.md
  CONTRIBUTING.md
  CHANGELOG.md
  .gitignore
  .editorconfig
  config.env.example
  src/
    index.cjs         # Main entry point
    binance.cjs       # Binance API connection
    analyzer.cjs      # Trade analysis engine
    shadowSim.cjs     # Shadow strategy simulation
    coach.cjs         # AI coaching feedback
  tests/
    analyzer.test.cjs
    shadowSim.test.cjs
  assets/
    architecture.png
    shadow-simulation.png
```

---

## How to Run

### Quick Start

This bot supports two credential methods. **Use env vars (Method 1) if your platform supports it.**

#### Method 1 — Environment variables (recommended)

```bash
export BINANCE_API_KEY=your_api_key
export BINANCE_API_SECRET=your_api_secret
node src/index.cjs
```

#### Method 2 — Local config file (for local development)

```bash
git clone https://github.com/acevod/Shadow-Binance-Bot.git
cd Shadow-Binance-Bot
cp config.env.example config.env
nano config.env  # fill in your keys
node src/index.cjs
```

**Note:** If `BINANCE_API_KEY` and `BINANCE_API_SECRET` are set as environment variables, `config.env` is not required.

### Get Your Binance API Key

1. Log in to Binance
2. Go to Account -> API Management
3. Create your API Key and Secret Key
4. Set Read-Only permissions
5. Copy your API Key and Secret Key
6. **Restrict the key to your IP address** (required for platform deployments)

Never share your Secret Key! Restrict the key to your IP address in Binance API Settings.

---

## Why This Matters

Crypto trading platforms provide powerful tools for execution.

But traders rarely receive feedback on **how their decisions affect outcomes**.

Shadow Mode Trading Trainer bridges that gap by turning historical trading data into a learning system.

The goal is to help traders evolve from reactive decision-making to disciplined strategy development.

---

## Future Improvements

- advanced AI trade pattern recognition
- portfolio risk scoring
- strategy backtesting engine
- trading psychology analysis
- visual performance dashboards

---

## License

MIT License


path: package.json
content: {
  "name": "shadow-binance-bot",
  "version": "1.3.3",
  "description": "AI-powered read-only trading analysis coach that simulates alternative strategies against your Binance trade history.",
  "main": "src/index.cjs",
  "scripts": {
    "start": "node src/index.cjs",
    "test": "node tests/analyzer.test.cjs && node tests/shadowSim.test.cjs"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": [
    "binance",
    "trading",
    "analysis",
    "ai",
    "coach",
    "shadow-trading",
    "crypto",
    "backtesting",
    "trading-analytics",
    "trading-coach",
    "futures",
    "spot",
    "risk-management"
  ],
  "author": "acevod",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/acevod/Shadow-Binance-Bot"
  }
}


path: src/analyzer.cjs
content: /**
 * Shadow Binance Bot - Trade Analyzer
 * Analyzes trading history and generates statistics
 */

/**
 * Analyze futures income history
 * @param {array} incomeHistory - Array of income events from Binance
 * @returns {object} - Analysis results
 */
function analyzeFuturesIncome(incomeHistory) {
  // Filter only realized PnL
  const pnlTrades = incomeHistory.filter(i => i.incomeType === 'REALIZED_PNL');
  const commissions = incomeHistory.filter(i => i.incomeType === 'COMMISSION');
  const fundingFees = incomeHistory.filter(i => i.incomeType === 'FUNDING_FEE');
  const transfers = incomeHistory.filter(i => i.incomeType === 'TRANSFER');

  // Calculate totals
  const totalRealizedPnL = pnlTrades.reduce((sum, i) => sum + parseFloat(i.income), 0);
  const totalCommission = commissions.reduce((sum, i) => sum + parseFloat(i.income), 0);
  const totalFunding = fundingFees.reduce((sum, i) => sum + parseFloat(i.income), 0);
  const totalTransfers = transfers.reduce((sum, i) => sum + parseFloat(i.income), 0);

  // Win/Loss analysis
  const wins = pnlTrades.filter(t => parseFloat(t.income) > 0);
  const losses = pnlTrades.filter(t => parseFloat(t.income) < 0);

  const winCount = wins.length;
  const lossCount = losses.length;
  const totalTrades = winCount + lossCount;
  const winRate = totalTrades > 0 ? (winCount / totalTrades) * 100 : 0;

  // Average win/loss
  const avgWin = winCount > 0
    ? wins.reduce((sum, t) => sum + parseFloat(t.income), 0) / winCount
    : 0;
  const avgLoss = lossCount > 0
    ? losses.reduce((sum, t) => sum + parseFloat(t.income), 0) / lossCount
    : 0;

  // Risk:Reward ratio
  const riskReward = avgLoss !== 0 ? Math.abs(avgWin / avgLoss) : 0;

  // Time analysis (by hour UTC)
  const hourlyStats = analyzeByHour(pnlTrades);

  // Streak analysis
  const streakStats = analyzeStreaks(pnlTrades);

  // Daily PnL
  const dailyPnL = analyzeDailyPnL(pnlTrades);

  // Date range
  const dates = incomeHistory.map(i => i.time).sort();
  const startDate = dates.length > 0 ? new Date(dates[0]) : null;
  const endDate = dates.length > 0 ? new Date(dates[dates.length - 1]) : null;

  return {
    period: {
      start: startDate ? startDate.toISOString().split('T')[0] : 'N/A',
      end: endDate ? endDate.toISOString().split('T')[0] : 'N/A',
      days: startDate && endDate
        ? Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))
        : 0
    },
    trades: {
      total: totalTrades,
      wins: winCount,
      losses: lossCount,
      winRate: winRate.toFixed(1),
      commissions: totalCommission.toFixed(4),
      funding: totalFunding.toFixed(4)
    },
    pnl: {
      realized: totalRealizedPnL.toFixed(4),
      net: (totalRealizedPnL + totalCommission + totalFunding).toFixed(4),
      transfers: totalTransfers.toFixed(4)
    },
    averages: {
      avgWin: avgWin.toFixed(4),
      avgLoss: avgLoss.toFixed(4),
      riskReward: riskReward.toFixed(2)
    },
    streaks: streakStats,
    hourly: hourlyStats,
    daily: dailyPnL
  };
}

/**
 * Analyze trades by hour of day
 * @param {array} pnlTrades - Array of PnL trades (never mutated)
 * @returns {object} - Hourly statistics
 */
function analyzeByHour(pnlTrades) {
  const hourly = {};

  pnlTrades.forEach(trade => {
    const hour = new Date(trade.time).getUTCHours();
    const amount = parseFloat(trade.income);

    if (!hourly[hour]) {
      hourly[hour] = { wins: 0, losses: 0, pnl: 0, total: 0, winRate: 0, trades: [] };
    }

    hourly[hour].pnl += amount;
    hourly[hour].trades.push(amount);

    if (amount > 0) hourly[hour].wins++;
    else hourly[hour].losses++;
  });

  // Calculate win rates and finalize
  Object.keys(hourly).forEach(hour => {
    const h = hourly[hour];
    h.total = h.wins + h.losses;
    h.winRate = h.total > 0 ? ((h.wins / h.total) * 100).toFixed(0) : '0';
    h.pnl = h.pnl.toFixed(4);
    // Remove internal trades array before returning
    delete h.trades;
  });

  return hourly;
}

/**
 * Analyze win/loss streaks
 * @param {array} pnlTrades - Array of PnL trades
 * @returns {object} - Streak statistics
 */
function analyzeStreaks(pnlTrades) {
  // Sort by time
  const sorted = [...pnlTrades].sort((a, b) => a.time - b.time);

  let maxWinStreak = 0;
  let maxLossStreak = 0;
  let tempWinStreak = 0;
  let tempLossStreak = 0;

  sorted.forEach(trade => {
    const isWin = parseFloat(trade.income) > 0;

    if (isWin) {
      tempWinStreak++;
      tempLossStreak = 0;
      maxWinStreak = Math.max(maxWinStreak, tempWinStreak);
    } else {
      tempLossStreak++;
      tempWinStreak = 0;
      maxLossStreak = Math.max(maxLossStreak, tempLossStreak);
    }
  });

  return { maxWinStreak, maxLossStreak };
}

/**
 * Analyze daily PnL
 * @param {array} pnlTrades - Array of PnL trades
 * @returns {object} - Daily statistics
 */
function analyzeDailyPnL(pnlTrades) {
  const daily = {};

  pnlTrades.forEach(trade => {
    const date = new Date(trade.time).toLocaleDateString();
    const amount = parseFloat(trade.income);

    if (!daily[date]) {
      daily[date] = { pnl: 0, trades: 0 };
    }

    daily[date].pnl += amount;
    daily[date].trades++;
  });

  // Find best and worst days
  let bestDay = { date: '', pnl: -Infinity };
  let worstDay = { date: '', pnl: Infinity };

  Object.entries(daily).forEach(([date, data]) => {
    if (data.pnl > bestDay.pnl) bestDay = { date, pnl: data.pnl };
    if (data.pnl < worstDay.pnl) worstDay = { date, pnl: data.pnl };
  });

  return {
    days: daily,
    bestDay: { date: bestDay.date, pnl: bestDay.pnl.toFixed(4) },
    worstDay: { date: worstDay.date, pnl: worstDay.pnl.toFixed(4) }
  };
}

/**
 * Get trading behavior analysis
 * @param {object} analysis - Full analysis object
 * @returns {object} - Behavior insights
 */
function analyzeBehavior(analysis) {
  const insights = [];

  // Check win rate
  if (parseFloat(analysis.trades.winRate) < 30) {
    insights.push({
      type: 'warning',
      message: `Win rate is only ${analysis.trades.winRate}%. Aim for 40%+ to be profitable.`
    });
  }

  // Check risk:reward
  if (parseFloat(analysis.averages.riskReward) < 2) {
    insights.push({
      type: 'warning',
      message: `Risk:Reward is only 1:${analysis.averages.riskReward}. Use at least 1:3 to cover losses.`
    });
  }

  // Check loss streak
  if (analysis.streaks.maxLossStreak > 10) {
    insights.push({
      type: 'danger',
      message: `Max loss streak of ${analysis.streaks.maxLossStreak} detected! This indicates revenge trading or tilting.`
    });
  }

  // Check overtrading
  const tradesPerDay = analysis.trades.total / (analysis.period.days || 1);
  if (tradesPerDay > 3) {
    insights.push({
      type: 'warning',
      message: `You're trading ${tradesPerDay.toFixed(1)} times per day on average. Consider trading less and waiting for better setups.`
    });
  }

  // Check worst trading hours
  const hourly = analysis.hourly;
  const badHours = Object.entries(hourly)
    .filter(([h, data]) => data.winRate < 30 && data.total > 3)
    .map(([h]) => `${h}:00 UTC`);

  if (badHours.length > 0) {
    insights.push({
      type: 'tip',
      message: `Avoid trading at these hours (low win rate): ${badHours.join(', ')}`
    });
  }

  // Check best trading hours
  const goodHours = Object.entries(hourly)
    .filter(([h, data]) => data.winRate >= 60)
    .map(([h, data]) => `${h}:00 UTC (${data.winRate}% win rate)`);

  if (goodHours.length > 0) {
    insights.push({
      type: 'success',
      message: `Your best trading hours (highest win rate): ${goodHours.join(', ')}`
    });
  }

  return insights;
}

/**
 * Analyze Spot trades from multiple symbols
 * @param {object} allTrades - Object with trades grouped by symbol
 * @returns {object} - Spot analysis results
 */
function analyzeSpotTrades(allTrades) {
  // Handle the { trades, errors } shape from getAllSpotTrades
  const tradesBySymbol = (allTrades && allTrades.trades) ? allTrades.trades : allTrades;

  let totalTrades = 0;
  let totalVolume = 0;
  let totalCommission = 0;
  const symbols = Object.keys(tradesBySymbol);
  const symbolStats = {};

  symbols.forEach(symbol => {
    const trades = tradesBySymbol[symbol];
    if (!trades || trades.length === 0) return;

    let symbolVolume = 0;
    let symbolCommission = 0;
    let buys = 0;
    let sells = 0;

    trades.forEach(trade => {
      const qty = parseFloat(trade.qty);
      const price = parseFloat(trade.price);
      const commission = parseFloat(trade.commission);

      symbolVolume += qty * price;
      symbolCommission += commission;

      if (trade.isBuyer) buys++;
      else sells++;

      totalCommission += commission;
    });

    totalTrades += trades.length;
    totalVolume += symbolVolume;

    symbolStats[symbol] = {
      trades: trades.length,
      volume: symbolVolume.toFixed(2),
      buys,
      sells,
      commission: symbolCommission.toFixed(6)
    };
  });

  // Calculate average trade size
  const avgTradeSize = totalTrades > 0 ? (totalVolume / totalTrades) : 0;

  return {
    totalSymbols: symbols.length,
    totalTrades,
    totalVolume: totalVolume.toFixed(2),
    avgTradeSize: avgTradeSize.toFixed(2),
    totalCommission: totalCommission.toFixed(6),
    symbols: symbolStats
  };
}

module.exports = {
  analyzeFuturesIncome,
  analyzeBehavior,
  analyzeSpotTrades
};


path: src/binance.cjs
content: /**
 * Shadow Binance Bot - Binance API Connection
 * Handles all communication with Binance API
 */

const crypto = require('crypto');
const https = require('https');

// API Configuration
const BASE_SPOT_URL = 'api.binance.com';
const BASE_FUTURES_URL = 'fapi.binance.com';

// Retry configuration
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 500;

/**
 * Delay utility for retry backoff
 * @param {number} ms - Milliseconds to wait
 * @returns {Promise<void>}
 */
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Generate signature for Binance API
 * @param {string} queryString - The query string to sign
 * @param {string} secret - Your API secret
 * @returns {string} - The signature
 */
function generateSignature(queryString, secret) {
  return crypto.createHmac('sha256', secret).update(queryString).digest('hex');
}

/**
 * Check if a Binance API response is an error
 * @param {object} data - Response data
 * @returns {boolean} - True if error
 */
function isBinanceError(data) {
  return data && typeof data === 'object' && typeof data.code === 'number' && data.msg;
}

/**
 * Determine if an error is retryable
 * @param {Error|object} error - Error or response data
 * @returns {boolean} - True if safe to retry
 */
function isRetryable(error) {
  // Network/transport errors are retryable
  if (error instanceof Error) {
    const msg = error.message.toLowerCase();
    if (msg.includes('timeout') || msg.includes('econnreset') || msg.includes('econnrefused') || msg.includes('socket')) {
      return true;
    }
  }
  // Binance error codes that are retryable
  // -1003 = RATE_LIMIT, -1023 = Maintenance, -1001 = DISCONNECTED
  if (error && error.code && [-1003, -1023, -1001].includes(error.code)) {
    return true;
  }
  return false;
}

/**
 * Make HTTP request to Binance API with automatic retry
 * @param {string} hostname - API hostname
 * @param {string} path - API path
 * @param {string} method - HTTP method
 * @param {object} headers - Request headers
 * @returns {Promise<object>} - API response
 */
async function makeRequest(hostname, path, method, headers = {}) {
  let lastError;

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const result = await _doRequest(hostname, path, method, headers);

      // If Binance returned a retryable error, retry
      if (isRetryable(result)) {
        if (attempt < MAX_RETRIES) {
          const backoffMs = BASE_DELAY_MS * Math.pow(2, attempt - 1);
          console.error(`[Retry ${attempt}/${MAX_RETRIES}] Rate-limited. Waiting ${backoffMs}ms...`);
          await delay(backoffMs);
          continue;
        }
      }

      return result;
    } catch (err) {
      lastError = err;

      if (isRetryable(err) && attempt < MAX_RETRIES) {
        const backoffMs = BASE_DELAY_MS * Math.pow(2, attempt - 1);
        console.error(`[Retry ${attempt}/${MAX_RETRIES}] ${err.message}. Waiting ${backoffMs}ms...`);
        await delay(backoffMs);
      } else {
        throw err;
      }
    }
  }

  throw lastError;
}

/**
 * Internal: perform a single HTTP request
 * @param {string} hostname - API hostname
 * @param {string} path - API path
 * @param {string} method - HTTP method
 * @param {object} headers - Request headers
 * @returns {Promise<object>} - API response
 */
function _doRequest(hostname, path, method, headers = {}) {
  return new Promise((resolve, reject) => {
    const options = { hostname, path, method, headers };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => {
        try {
          const parsed = JSON.parse(data);
          resolve(parsed);
        } catch (e) {
          reject(new Error(`Failed to parse response: ${data}`));
        }
      });
    });

    req.on('error', (err) => {
      // Wrap as retryable if it's a network error
      if (isRetryable(err)) {
        reject(err);
      } else {
        reject(new Error(`Network error: ${err.message}`));
      }
    });

    req.setTimeout(10000, () => {
      req.destroy();
      reject(new Error('Request timed out after 10 seconds'));
    });

    req.end();
  });
}

/**
 * Get Spot Account Balance
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @returns {Promise<object>} - Account balances
 */
async function getSpotBalance(apiKey, apiSecret) {
  const timestamp = Date.now();
  const queryString = `timestamp=${timestamp}`;
  const signature = generateSignature(queryString, apiSecret);

  const path = `/api/v3/account?${queryString}&signature=${signature}`;
  const data = await makeRequest(BASE_SPOT_URL, path, 'GET', {
    'X-MBX-APIKEY': apiKey
  });

  if (isBinanceError(data)) {
    throw new Error(`Binance API Error [${data.code}]: ${data.msg}`);
  }

  return data;
}

/**
 * Get Futures Account Balance
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @returns {Promise<object>} - Futures account data
 */
async function getFuturesBalance(apiKey, apiSecret) {
  const timestamp = Date.now();
  const queryString = `timestamp=${timestamp}`;
  const signature = generateSignature(queryString, apiSecret);

  const path = `/fapi/v2/account?${queryString}&signature=${signature}`;
  const data = await makeRequest(BASE_FUTURES_URL, path, 'GET', {
    'X-MBX-APIKEY': apiKey
  });

  if (isBinanceError(data)) {
    throw new Error(`Binance API Error [${data.code}]: ${data.msg}`);
  }

  return data;
}

/**
 * Get Futures Income/PNL History
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @param {number} daysBack - How many days back to fetch
 * @returns {Promise<array>} - Income history
 */
async function getFuturesIncome(apiKey, apiSecret, daysBack = 365) {
  const timestamp = Date.now();
  const startTime = timestamp - (daysBack * 24 * 60 * 60 * 1000);
  const queryString = `startTime=${startTime}&timestamp=${timestamp}&limit=1000`;
  const signature = generateSignature(queryString, apiSecret);

  const path = `/fapi/v1/income?${queryString}&signature=${signature}`;
  const data = await makeRequest(BASE_FUTURES_URL, path, 'GET', {
    'X-MBX-APIKEY': apiKey
  });

  if (isBinanceError(data)) {
    throw new Error(`Binance API Error [${data.code}]: ${data.msg}`);
  }

  return data;
}

/**
 * Get Spot Trade History
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @param {string} symbol - Trading pair (e.g., 'BTCUSDT')
 * @param {number} limit - Number of trades to fetch (max 1000)
 * @returns {Promise<array>} - Trade history
 */
async function getSpotTrades(apiKey, apiSecret, symbol = 'BTCUSDT', limit = 100) {
  const timestamp = Date.now();
  const queryString = `symbol=${symbol}&timestamp=${timestamp}&limit=${limit}`;
  const signature = generateSignature(queryString, apiSecret);

  const path = `/api/v3/myTrades?${queryString}&signature=${signature}`;
  const data = await makeRequest(BASE_SPOT_URL, path, 'GET', {
    'X-MBX-APIKEY': apiKey
  });

  if (isBinanceError(data)) {
    throw new Error(`Binance API Error [${data.code}]: ${data.msg}`);
  }

  return data;
}

/**
 * Get all Spot trades for multiple symbols
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @param {array} symbols - Array of trading pairs to fetch
 * @returns {Promise<object>} - All trades grouped by symbol, plus errors array
 */
async function getAllSpotTrades(apiKey, apiSecret, symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT']) {
  const allTrades = {};
  const errors = [];

  for (const symbol of symbols) {
    try {
      const trades = await getSpotTrades(apiKey, apiSecret, symbol, 100);
      if (Array.isArray(trades) && trades.length > 0) {
        allTrades[symbol] = trades;
      }
    } catch (e) {
      errors.push({ symbol, error: e.message });
      console.error(`Failed to fetch trades for ${symbol}: ${e.message}`);
    }
  }

  return { trades: allTrades, errors };
}

/**
 * Test API Connection
 * @param {string} apiKey - Your Binance API Key
 * @param {string} apiSecret - Your Binance API Secret
 * @returns {Promise<boolean>} - Connection successful?
 */
async function testConnection(apiKey, apiSecret) {
  try {
    const balance = await getSpotBalance(apiKey, apiSecret);
    return balance && !balance.code;
  } catch (e) {
    console.error('Connection test failed:', e.message);
    return false;
  }
}

module.exports = {
  getSpotBalance,
  getFuturesBalance,
  getFuturesIncome,
  getSpotTrades,
  getAllSpotTrades,
  testConnection,
  generateSignature
};


path: src/coach.cjs
content: /**
 * Shadow Binance Bot - AI Trading Coach
 * Generates personalized coaching feedback based on analysis
 */

// Behavioral thresholds
const THRESHOLDS = {
  MIN_WIN_RATE: 40,
  MIN_RISK_REWARD: 3,
  MAX_LOSS_STREAK: 5,
  MAX_TRADES_PER_DAY: 3,
  MIN_GOOD_HOUR_WIN_RATE: 60,
  MAX_BAD_HOUR_WIN_RATE: 30,
  MIN_BAD_HOUR_TRADES: 3
};

/**
 * Generate coaching report based on analysis
 * @param {object} analysis - Trading analysis results
 * @param {object} shadowComparison - Shadow strategy simulations
 * @returns {object} - Full coaching report
 */
function generateCoachReport(analysis, shadowComparison) {
  return {
    summary: generateSummary(analysis),
    problems: identifyProblems(analysis),
    recommendations: generateRecommendations(analysis, shadowComparison),
    actionPlan: generateActionPlan(analysis),
    motivation: getMotivation(analysis)
  };
}

/**
 * Generate summary of trading performance
 * @param {object} analysis - Trading analysis results
 * @returns {object} - Summary object
 */
function generateSummary(analysis) {
  const netPnL = parseFloat(analysis.pnl.net);
  const winRate = parseFloat(analysis.trades.winRate);
  const totalTrades = analysis.trades.total;
  const period = analysis.period;

  let verdict;
  if (netPnL > 0) verdict = 'PROFITABLE';
  else if (netPnL > -5) verdict = 'NEAR BREAK-EVEN';
  else verdict = 'NEEDS IMPROVEMENT';

  return {
    period: `${period.start} to ${period.end} (${period.days} days)`,
    totalTrades,
    winRate: `${winRate}%`,
    netPnL: `${netPnL} USDT`,
    verdict,
    commission: `${analysis.trades.commissions} USDT in fees`
  };
}

/**
 * Identify key problems in trading behavior
 * @param {object} analysis - Trading analysis results
 * @returns {array} - List of problems
 */
function identifyProblems(analysis) {
  const problems = [];
  const winRate = parseFloat(analysis.trades.winRate);
  const riskReward = parseFloat(analysis.averages.riskReward);
  const maxLossStreak = analysis.streaks.maxLossStreak;
  const hourly = analysis.hourly;

  if (winRate < 30) {
    problems.push({
      severity: 'high',
      title: 'Low Win Rate',
      description: `Your win rate is only ${winRate}%. At this rate, you need excellent risk:reward to be profitable.`
    });
  }

  if (riskReward < 3) {
    problems.push({
      severity: 'high',
      title: 'Poor Risk:Reward',
      description: `You're using 1:${riskReward} risk:reward. You need at least 1:${THRESHOLDS.MIN_RISK_REWARD} to cover your losses.`
    });
  }

  if (maxLossStreak > THRESHOLDS.MAX_LOSS_STREAK) {
    problems.push({
      severity: 'high',
      title: 'Loss Streak Problem',
      description: `You had a streak of ${maxLossStreak} consecutive losses. This indicates emotional trading or revenge trading.`
    });
  }

  // Find bad hours
  const badHours = Object.entries(hourly)
    .filter(([h, data]) => data.winRate < THRESHOLDS.MAX_BAD_HOUR_WIN_RATE && data.total > THRESHOLDS.MIN_BAD_HOUR_TRADES)
    .map(([h]) => h);

  if (badHours.length > 0) {
    problems.push({
      severity: 'medium',
      title: 'Avoid These Trading Hours',
      description: `Your win rate is very low (under ${THRESHOLDS.MAX_BAD_HOUR_WIN_RATE}%) at: ${badHours.map(h => `${h}:00 UTC`).join(', ')}`
    });
  }

  return problems;
}

/**
 * Generate personalized recommendations
 * @param {object} analysis - Trading analysis results
 * @param {object} shadowComparison - Shadow strategy simulations
 * @returns {array} - List of recommendations
 */
function generateRecommendations(analysis, shadowComparison) {
  const recommendations = [];
  const strategies = shadowComparison.strategies;

  // Best improvement strategy
  let bestStrategy = null;
  let bestImprovement = -Infinity;

  strategies.forEach(s => {
    if (s.improvement && parseFloat(s.improvement) > bestImprovement) {
      bestImprovement = parseFloat(s.improvement);
      bestStrategy = s;
    }
  });

  if (bestStrategy) {
    recommendations.push({
      priority: 1,
      title: `Try: ${bestStrategy.strategy}`,
      description: bestStrategy.description,
      potentialGain: `+${bestStrategy.improvement} USDT`
    });
  }

  // Risk:Reward recommendation
  if (parseFloat(analysis.averages.riskReward) < THRESHOLDS.MIN_RISK_REWARD) {
    recommendations.push({
      priority: 2,
      title: 'Fix Your Risk:Reward',
      description: 'Set your stop loss to 10 points and take profit to 30+ points (1:3 minimum). This is the quickest way to improve.',
      action: 'Use OCO orders with proper ratios'
    });
  }

  // Time-based recommendation
  const goodHours = Object.entries(analysis.hourly)
    .filter(([h, data]) => data.winRate >= THRESHOLDS.MIN_GOOD_HOUR_WIN_RATE)
    .map(([h]) => h);

  if (goodHours.length > 0) {
    recommendations.push({
      priority: 3,
      title: 'Trade During Your Best Hours',
      description: `Your win rate is highest at: ${goodHours.map(h => `${h}:00 UTC`).join(', ')}`,
      action: 'Set trading hours to these times only'
    });
  }

  // Streak prevention
  if (analysis.streaks.maxLossStreak > THRESHOLDS.MAX_LOSS_STREAK) {
    recommendations.push({
      priority: 4,
      title: 'Implement 3-Strike Rule',
      description: 'Stop trading after 3 consecutive losses. Take a break. Come back tomorrow.',
      action: 'Set a reminder or use a trading journal'
    });
  }

  return recommendations;
}

/**
 * Generate structured action plan
 * @param {object} analysis - Trading analysis results
 * @returns {array} - Phased action plan
 */
function generateActionPlan(analysis) {
  return [
    {
      phase: 'IMMEDIATE (This Week)',
      steps: [
        'Set stop loss BEFORE entering any trade',
        'Use 1:3 minimum risk:reward',
        'Check UTC time before trading - only trade at good hours'
      ]
    },
    {
      phase: 'SHORT-TERM (This Month)',
      steps: [
        'Implement 3-loss rule - stop after 3 losses per day',
        'Reduce trading frequency by 50%',
        'Practice on demo account until 50% win rate'
      ]
    },
    {
      phase: 'LONG-TERM (This Quarter)',
      steps: [
        'Build trading journal to track decisions',
        'Study support/resistance and candlestick patterns',
        'Aim for 40%+ win rate consistently'
      ]
    }
  ];
}

/**
 * Get motivational message based on performance
 * @param {object} analysis - Trading analysis results
 * @returns {string} - Motivational message
 */
function getMotivation(analysis) {
  const netPnL = parseFloat(analysis.pnl.net);

  if (netPnL > 0) {
    return "You're doing great! Keep improving your discipline and you'll be profitable in no time.";
  } else if (netPnL > -5) {
    return "You've identified your problems - now it's just about discipline. You can do this!";
  } else {
    return "Everyone loses at first. The key is learning from your mistakes. You've got this!";
  }
}

/**
 * Format full coach report for console display
 * @param {object} report - Coach report object
 * @returns {string} - Formatted report string
 */
function formatReport(report) {
  let output = '';

  output += '============================================\n';
  output += '    SHADOW TRADING COACH REPORT             \n';
  output += '============================================\n\n';

  // Summary
  output += 'SUMMARY\n';
  output += '--------------------------------------------\n';
  output += `Period: ${report.summary.period}\n`;
  output += `Trades: ${report.summary.totalTrades}\n`;
  output += `Win Rate: ${report.summary.winRate}\n`;
  output += `Net PnL: ${report.summary.netPnL}\n`;
  output += `Verdict: ${report.summary.verdict}\n\n`;

  // Problems
  if (report.problems.length > 0) {
    output += 'PROBLEMS IDENTIFIED\n';
    output += '--------------------------------------------\n';
    report.problems.forEach((p, i) => {
      output += `${i + 1}. ${p.title}\n   ${p.description}\n\n`;
    });
  }

  // Recommendations
  if (report.recommendations.length > 0) {
    output += 'RECOMMENDATIONS\n';
    output += '--------------------------------------------\n';
    report.recommendations.forEach((r, i) => {
      output += `${i + 1}. [Priority ${r.priority}] ${r.title}\n`;
      output += `   ${r.description}\n`;
      if (r.action) output += `   Action: ${r.action}\n`;
      if (r.potentialGain) output += `   Potential: ${r.potentialGain}\n`;
      output += '\n';
    });
  }

  // Action Plan
  output += 'ACTION PLAN\n';
  output += '--------------------------------------------\n';
  report.actionPlan.forEach(phase => {
    output += `\n${phase.phase}:\n`;
    phase.steps.forEach(step => {
      output += `  - ${step}\n`;
    });
  });
  output += '\n';

  // Motivation
  output += '============================================\n';
  output += `${report.motivation}\n`;
  output += '============================================\n';

  return output;
}

/**
 * Generate coaching report for Spot trading
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {object} - Spot coaching report
 */
function generateSpotCoachReport(spotAnalysis) {
  return {
    summary: generateSpotSummary(spotAnalysis),
    insights: generateSpotInsights(spotAnalysis),
    recommendations: generateSpotRecommendations(spotAnalysis)
  };
}

/**
 * Generate Spot summary
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {object} - Spot summary
 */
function generateSpotSummary(spotAnalysis) {
  return {
    totalTrades: spotAnalysis.totalTrades,
    totalSymbols: spotAnalysis.totalSymbols,
    totalVolume: spotAnalysis.totalVolume,
    avgTradeSize: spotAnalysis.avgTradeSize,
    commission: spotAnalysis.totalCommission
  };
}

/**
 * Generate Spot insights
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {array} - List of insights
 */
function generateSpotInsights(spotAnalysis) {
  const insights = [];

  if (spotAnalysis.totalTrades === 0) {
    insights.push({
      type: 'info',
      message: 'No Spot trades found. Start trading on Spot to see analysis!'
    });
    return insights;
  }

  // Check diversification
  if (spotAnalysis.totalSymbols < 3) {
    insights.push({
      type: 'tip',
      message: `You're only trading ${spotAnalysis.totalSymbols} symbol(s). Consider diversifying across more coins to reduce risk.`
    });
  }

  // Check average trade size
  const avgSize = parseFloat(spotAnalysis.avgTradeSize);
  if (avgSize > 100) {
    insights.push({
      type: 'warning',
      message: `Your average Spot trade is $${avgSize.toFixed(2)} - quite large! Consider position sizing.`
    });
  } else if (avgSize < 10) {
    insights.push({
      type: 'tip',
      message: `Your average Spot trade is $${avgSize.toFixed(2)} - good for learning with small amounts!`
    });
  }

  // Check for over-trading
  if (spotAnalysis.totalTrades > 100) {
    insights.push({
      type: 'warning',
      message: `You've made ${spotAnalysis.totalTrades} Spot trades. Make sure to consider fees!`
    });
  }

  // Find most traded symbol
  const symbols = spotAnalysis.symbols;
  let mostTraded = { symbol: '', trades: 0 };
  Object.entries(symbols).forEach(([symbol, stats]) => {
    if (stats.trades > mostTraded.trades) {
      mostTraded = { symbol, trades: stats.trades };
    }
  });

  if (mostTraded.symbol) {
    insights.push({
      type: 'info',
      message: `Most traded: ${mostTraded.symbol} (${mostTraded.trades} trades)`
    });
  }

  // Check buy/sell ratio per symbol
  Object.entries(symbols).forEach(([symbol, stats]) => {
    if (stats.buys > 0 && stats.sells > 0) {
      const buyRatio = (stats.buys / (stats.buys + stats.sells) * 100).toFixed(0);
      if (buyRatio > 70) {
        insights.push({
          type: 'tip',
          message: `${symbol}: You're mostly buying (${buyRatio}% buys). Consider taking profits!`
        });
      } else if (buyRatio < 30) {
        insights.push({
          type: 'tip',
          message: `${symbol}: You're mostly selling (${100 - buyRatio}% sells). Consider buying the dip!`
        });
      }
    }
  });

  return insights;
}

/**
 * Generate Spot recommendations
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {array} - List of recommendations
 */
function generateSpotRecommendations(spotAnalysis) {
  const recommendations = [];

  if (spotAnalysis.totalTrades === 0) {
    recommendations.push({
      priority: 1,
      title: 'Start Trading on Spot',
      description: 'Make some Spot trades to see personalized analysis!'
    });
    return recommendations;
  }

  // Diversification
  if (spotAnalysis.totalSymbols < 3) {
    recommendations.push({
      priority: 2,
      title: 'Diversify Your Portfolio',
      description: 'Consider trading more than one coin to spread risk.'
    });
  }

  // Position sizing
  const avgSize = parseFloat(spotAnalysis.avgTradeSize);
  if (avgSize > 100) {
    recommendations.push({
      priority: 2,
      title: 'Reduce Position Size',
      description: `$${avgSize.toFixed(2)} per trade is high. Try smaller amounts while learning!`
    });
  }

  // Fees
  const commission = parseFloat(spotAnalysis.totalCommission);
  if (commission > 10) {
    recommendations.push({
      priority: 3,
      title: 'Watch Your Fees',
      description: `You've paid $${commission.toFixed(2)} in fees. Consider trading less frequently!`
    });
  }

  return recommendations;
}

/**
 * Format Spot report for console display
 * @param {object} report - Spot coaching report
 * @returns {string} - Formatted report string
 */
function formatSpotReport(report) {
  let output = '';

  output += '============================================\n';
  output += '       SPOT COACHING REPORT                \n';
  output += '============================================\n\n';

  // Summary
  output += 'SPOT SUMMARY\n';
  output += '--------------------------------------------\n';
  output += `Total Trades: ${report.summary.totalTrades}\n`;
  output += `Symbols: ${report.summary.totalSymbols}\n`;
  output += `Volume: $${report.summary.totalVolume} USDT\n`;
  output += `Avg Trade: $${report.summary.avgTradeSize} USDT\n`;
  output += `Fees Paid: ${report.summary.commission}\n\n`;

  // Insights
  if (report.insights.length > 0) {
    output += 'INSIGHTS\n';
    output += '--------------------------------------------\n';
    report.insights.forEach((insight) => {
      const prefix = insight.type === 'warning' ? '!' : insight.type === 'tip' ? '+' : 'i';
      output += `[${prefix}] ${insight.message}\n\n`;
    });
  }

  // Recommendations
  if (report.recommendations.length > 0) {
    output += 'RECOMMENDATIONS\n';
    output += '--------------------------------------------\n';
    report.recommendations.forEach((rec, i) => {
      output += `${i + 1}. [Priority ${rec.priority}] ${rec.title}\n   ${rec.description}\n\n`;
    });
  }

  output += '============================================\n';

  return output;
}

module.exports = {
  generateCoachReport,
  formatReport,
  generateSpotCoachReport,
  formatSpotReport
};


path: src/index.cjs
content: /**
 * Shadow Binance Bot - Main Entry Point
 * AI-powered trading coach that analyzes your Binance trades
 * and simulates alternative strategies
 *
 * Usage: node src/index.cjs
 *
 * Usage: node src/index.cjs
 *
 * Credentials: Set BINANCE_API_KEY and BINANCE_API_SECRET via:
 *   1. Environment variables (recommended — works on all platforms)
 *   2. Local config.env file (for local development)
 *   See config.env.example for all supported variables.
 */

const fs = require('fs');
const path = require('path');

// Import modules
const binance = require('./binance.cjs');
const analyzer = require('./analyzer.cjs');
const shadowSim = require('./shadowSim.cjs');
const coach = require('./coach.cjs');

// Default spot symbols — can be overridden via SPOT_SYMBOLS in config.env
const DEFAULT_SPOT_SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'SHIBUSDT'];

// Load configuration.
// Priority: process.env (standard injection) > config.env (local dev override).
// Platforms that inject BINANCE_API_KEY/BINANCE_API_SECRET as environment
// variables will work automatically. Local developers can also use config.env.
function loadConfig() {
  const [REDACTED_SECRET];
  const apiSecret = process.env.BINANCE_API_SECRET;
  const spotSymbols = process.env.SPOT_SYMBOLS;

  // If env vars are already set (platform injection), use them directly
  if (apiKey && apiSecret) {
    return { BINANCE_API_KEY: apiKey, BINANCE_API_SECRET: apiSecret, SPOT_SYMBOLS: spotSymbols || undefined };
  }

  // Otherwise fall back to local config.env for development
  const configPath = path.join(__dirname, '..', 'config.env');

  if (!fs.existsSync(configPath)) {
    console.log('Note: config.env not found. Running in Demo Mode.');
    return {};
  }

  const config = {};
  const content = fs.readFileSync(configPath, 'utf8');

  content.split('\n').forEach(line => {
    line = line.trim();
    if (line && !line.startsWith('#')) {
      const [key, value] = line.split('=');
      if (key && value) {
        config[key.trim()] = value.trim();
      }
    }
  });

  return config;
}

// Generate mock data for Demo Mode
function generateDemoData() {
  console.log('');
  console.log('+==============================================+');
  console.log('|     DEMO MODE - Using Sample Data           |');
  console.log('+==============================================+');
  console.log('');
  console.log('Note: No API keys found. Running with simulated');
  console.log('demo data to show how the bot works.');
  console.log('');

  const now = Date.now();
  const HOUR = 60 * 60 * 1000;
  const DAY = 24 * HOUR;

  // Generate 30 days of mock futures income
  const incomeHistory = [];
  let cumulativePnL = 0;
  for (let i = 0; i < 60; i++) {
    const isWin = Math.random() > 0.45; // ~55% win rate
    const amount = isWin
      ? (Math.random() * 80 + 10).toFixed(4)
      : (-(Math.random() * 30 + 5)).toFixed(4);
    cumulativePnL += parseFloat(amount);
    incomeHistory.push({
      incomeType: 'REALIZED_PNL',
      income: String(amount),
      time: now - (i * DAY * 0.5) // 60 records spread over 30 days
    });
  }

  // Add some commission and funding
  incomeHistory.push({ incomeType: 'COMMISSION', income: '-2.50', time: now - DAY });
  incomeHistory.push({ incomeType: 'FUNDING_FEE', income: '-1.20', time: now - DAY * 2 });

  // Sort by time ascending
  incomeHistory.sort((a, b) => a.time - b.time);

  // Generate mock spot trades
  const spotTrades = {
    BTCUSDT: [
      { id: 1, qty: '0.50', price: '62000', commission: '0.00025', isBuyer: true, time: now - DAY * 5 },
      { id: 2, qty: '0.30', price: '64000', commission: '0.00015', isBuyer: false, time: now - DAY * 3 },
      { id: 3, qty: '0.20', price: '61000', commission: '0.00010', isBuyer: true, time: now - DAY * 1 },
    ],
    ETHUSDT: [
      { id: 4, qty: '2.00', price: '3400', commission: '0.00100', isBuyer: true, time: now - DAY * 4 },
      { id: 5, qty: '1.50', price: '3500', commission: '0.00075', isBuyer: false, time: now - DAY * 2 },
    ]
  };

  return { incomeHistory, spotTrades, isDemo: true };
}

// Main function
async function main() {
  console.log('');
  console.log('+==============================================+');
  console.log('|     SHADOW BINANCE BOT                      |');
  console.log('|     Your Personal Trading Coach              |');
  console.log('+==============================================+');
  console.log('');

  // Load config
  const config = loadConfig();
  const { BINANCE_API_KEY, BINANCE_API_SECRET, SPOT_SYMBOLS } = config;

  // Parse spot symbols from config (comma-separated) or use defaults
  const spotSymbols = SPOT_SYMBOLS
    ? SPOT_SYMBOLS.split(',').map(s => s.trim())
    : DEFAULT_SPOT_SYMBOLS;

  // Check if running in demo mode (no API keys)
  const isDemoMode = !BINANCE_API_KEY || !BINANCE_API_SECRET;

  if (isDemoMode) {
    const { incomeHistory, spotTrades } = generateDemoData();

    // ===== DEMO FUTURES ANALYSIS =====
    console.log('============================================');
    console.log('         FUTURES ANALYSIS [DEMO]            ');
    console.log('============================================');
    console.log('');

    const futuresAnalysis = analyzer.analyzeFuturesIncome(incomeHistory);
    const futuresShadow = shadowSim.generateShadowComparison(futuresAnalysis);
    const futuresCoach = coach.generateCoachReport(futuresAnalysis, futuresShadow);
    console.log(coach.formatReport(futuresCoach));

    // ===== DEMO SPOT ANALYSIS =====
    console.log('');
    console.log('============================================');
    console.log('           SPOT ANALYSIS [DEMO]             ');
    console.log('============================================');
    console.log('');

    const spotAnalysis = analyzer.analyzeSpotTrades(spotTrades);
    const spotShadow = shadowSim.generateSpotShadowComparison(spotAnalysis);
    const spotCoach = coach.generateSpotCoachReport(spotAnalysis);
    console.log(coach.formatSpotReport(spotCoach));

    console.log('============================================');
    console.log('         END OF DEMO MODE                   ');
    console.log('============================================');
    console.log('');
    console.log('To run with your real data:');
    console.log('  1. Set BINANCE_API_KEY and BINANCE_API_SECRET');
    console.log('     - As environment variables (recommended), OR');
    console.log('     - Copy config.env.example to config.env and fill in keys');
    console.log('  2. Run: node src/index.cjs');
    console.log('');
    return;
  }

  console.log('Connecting to Binance...');

  // Test connection
  const connected = await binance.testConnection(BINANCE_API_KEY, BINANCE_API_SECRET);
  if (!connected) {
    console.error('x Failed to connect to Binance. Check your API keys.');
    process.exit(1);
  }
  console.log('Connected to Binance!');
  console.log('');

  try {
    // ===== FUTURES ANALYSIS =====
    console.log('============================================');
    console.log('         FUTURES ANALYSIS                   ');
    console.log('============================================');
    console.log('');

    console.log('Fetching Futures trading history...');
    const incomeHistory = await binance.getFuturesIncome(BINANCE_API_KEY, BINANCE_API_SECRET, 365);

    if (!incomeHistory || incomeHistory.length === 0) {
      console.log('No Futures trading history found.');
    } else {
      console.log(`   Found ${incomeHistory.length} records`);
    }
    console.log('');

    // Analyze Futures
    console.log('Analyzing Futures patterns...');
    const futuresAnalysis = analyzer.analyzeFuturesIncome(incomeHistory);

    // Shadow Simulation for Futures
    console.log('Running Futures shadow simulations...');
    const futuresShadow = shadowSim.generateShadowComparison(futuresAnalysis);

    // Generate Futures Coach Report
    const futuresCoach = coach.generateCoachReport(futuresAnalysis, futuresShadow);
    console.log('');

    // Display Futures Report
    console.log(coach.formatReport(futuresCoach));

    // ===== SPOT ANALYSIS =====
    console.log('');
    console.log('============================================');
    console.log('           SPOT ANALYSIS                    ');
    console.log('============================================');
    console.log('');

    console.log('Fetching Spot trading history...');
    const allSpotTrades = await binance.getAllSpotTrades(BINANCE_API_KEY, BINANCE_API_SECRET, spotSymbols);

    const spotAnalysis = analyzer.analyzeSpotTrades(allSpotTrades);

    console.log(`   Found ${spotAnalysis.totalTrades} trades across ${spotAnalysis.totalSymbols} symbols`);
    console.log('');

    // Spot Shadow Simulation
    console.log('Running Spot shadow simulations...');
    const spotShadow = shadowSim.generateSpotShadowComparison(spotAnalysis);

    // Spot Coaching
    console.log('Generating Spot coaching...');
    const spotCoach = coach.generateSpotCoachReport(spotAnalysis);
    console.log('');

    // Display Spot Report
    console.log(coach.formatSpotReport(spotCoach));

    // Show Spot Shadow Strategies
    if (spotAnalysis.totalTrades > 0) {
      console.log('============================================');
      console.log('      SPOT SHADOW STRATEGIES                ');
      console.log('============================================');
      spotShadow.strategies.forEach(strategy => {
        if (!strategy.error) {
          console.log('');
          console.log(`[${strategy.strategy}]`);
          console.log(`   ${strategy.description}`);
        }
      });
      console.log('');
    }

    // ===== COMBINED TOTAL =====
    const futuresNetPnL = parseFloat(futuresAnalysis.pnl.net);
    const spotVolume = parseFloat(spotAnalysis.totalVolume);

    console.log('');
    console.log('============================================');
    console.log('        COMBINED TOTAL                      ');
    console.log('============================================');
    console.log(`Futures Net PnL: ${futuresNetPnL.toFixed(4)} USDT`);
    console.log(`Spot Volume: ${spotVolume.toFixed(2)} USDT`);
    console.log('');

    if (futuresNetPnL < 0) {
      console.log('Focus on fixing your Futures trading first!');
      console.log('   - Use better Risk:Reward (1:3 minimum)');
      console.log('   - Trade during your best hours');
      console.log('   - Implement 3-loss rule');
    }

    console.log('');
    console.log('============================================');
    console.log('Keep practicing and stay disciplined!');
    console.log('============================================');

  } catch (error) {
    console.error('Error:', error.message);
    process.exit(1);
  }
}

// Run if called directly
if (require.main === module) {
  main().catch(console.error);
}

module.exports = { main };


path: src/shadowSim.cjs
content: /**
 * Shadow Binance Bot - Shadow Strategy Simulator
 * Simulates alternative trading strategies to compare with real performance
 */

/**
 * Simulate improved Risk:Reward strategy
 * @param {object} analysis - Real trading analysis
 * @param {number} targetRR - Target risk:reward ratio
 * @returns {object} - Simulated results
 */
function simulateImprovedRiskReward(analysis, targetRR = 4) {
  const realTrades = analysis.trades.total;
  const realWinRate = parseFloat(analysis.trades.winRate) / 100;
  const avgLoss = parseFloat(analysis.averages.avgLoss);

  // Simulate with better R:R
  const avgWin = avgLoss * targetRR;

  const wins = Math.round(realTrades * realWinRate);
  const losses = realTrades - wins;

  const simulatedPnL = (wins * avgWin) + (losses * avgLoss);
  const improvement = simulatedPnL - parseFloat(analysis.pnl.realized);

  return {
    strategy: `Improved Risk:Reward (1:${targetRR})`,
    trades: realTrades,
    wins,
    losses,
    winRate: analysis.trades.winRate,
    avgWin: avgWin.toFixed(4),
    avgLoss: avgLoss.toFixed(4),
    riskReward: targetRR,
    pnl: simulatedPnL.toFixed(4),
    improvement: improvement.toFixed(4),
    description: `If you used 1:${targetRR} risk:reward instead of 1:${analysis.averages.riskReward}, you could have made ${improvement > 0 ? '+' : ''}${improvement} USDT more.`
  };
}

/**
 * Simulate selective trading (only good hours)
 * @param {object} analysis - Real trading analysis
 * @param {number} minWinRate - Minimum win rate threshold
 * @returns {object} - Simulated results
 */
function simulateSelectiveTrading(analysis, minWinRate = 50) {
  const hourly = analysis.hourly;

  // Find good hours
  const goodHours = Object.entries(hourly)
    .filter(([hour, data]) => parseInt(data.winRate) >= minWinRate)
    .map(([hour, data]) => ({
      hour: parseInt(hour),
      winRate: parseInt(data.winRate),
      pnl: parseFloat(data.pnl),
      total: data.total
    }));

  if (goodHours.length === 0) {
    return {
      strategy: 'Selective Trading',
      error: 'Not enough data to simulate'
    };
  }

  // Calculate what would happen if only trading during good hours
  const goodHourPnL = goodHours.reduce((sum, h) => sum + h.pnl, 0);
  const goodHourTrades = goodHours.reduce((sum, h) => sum + h.total, 0);

  const realPnL = parseFloat(analysis.pnl.realized);
  const improvement = goodHourPnL - realPnL;

  return {
    strategy: `Selective Trading (${minWinRate}%+ win rate hours)`,
    goodHours: goodHours.map(h => `${h.hour}:00 UTC (${h.winRate}% WR)`),
    simulatedTrades: goodHourTrades,
    simulatedPnL: goodHourPnL.toFixed(4),
    realPnL: realPnL.toFixed(4),
    improvement: improvement.toFixed(4),
    description: `If you only traded during hours with ${minWinRate}%+ win rate, you could have ${improvement > 0 ? 'gained' : 'lost'} ${Math.abs(improvement).toFixed(4)} USDT.`
  };
}

/**
 * Simulate reduced trading frequency
 * @param {object} analysis - Real trading analysis
 * @param {number} reductionPercent - How much to reduce trades by (0-100)
 * @returns {object} - Simulated results
 */
function simulateReducedTrading(analysis, reductionPercent = 50) {
  const realTrades = analysis.trades.total;
  const reducedTrades = Math.round(realTrades * (1 - reductionPercent / 100));

  const realWinRate = parseFloat(analysis.trades.winRate) / 100;
  const avgWin = parseFloat(analysis.averages.avgWin);
  const avgLoss = parseFloat(analysis.averages.avgLoss);

  // Calculate real PnL per trade
  const pnlPerTrade = parseFloat(analysis.pnl.realized) / realTrades;

  // Simulate reduced trades
  const wins = Math.round(reducedTrades * realWinRate);
  const losses = reducedTrades - wins;
  const simulatedPnL = (wins * avgWin) + (losses * avgLoss);

  // Also account for reduced commission
  const realCommission = parseFloat(analysis.trades.commissions);
  const reducedCommission = realCommission * (1 - reductionPercent / 100);
  const commissionSavings = realCommission - reducedCommission;

  const totalImprovement = (simulatedPnL - parseFloat(analysis.pnl.realized)) + commissionSavings;

  return {
    strategy: `Reduced Trading (-${reductionPercent}% trades)`,
    originalTrades: realTrades,
    simulatedTrades: reducedTrades,
    reduction: `${reductionPercent}%`,
    originalPnL: analysis.pnl.realized,
    simulatedPnL: simulatedPnL.toFixed(4),
    commissionSavings: commissionSavings.toFixed(4),
    improvement: totalImprovement.toFixed(4),
    description: `If you made ${reductionPercent}% fewer trades (${reducedTrades} instead of ${realTrades}), focusing only on the best setups, you could have made ${totalImprovement > 0 ? '+' : ''}${totalImprovement.toFixed(4)} USDT more.`
  };
}

/**
 * Simulate DCA (Dollar Cost Averaging) strategy
 * @param {object} analysis - Real trading analysis
 * @returns {object} - Simulated results
 */
function simulateDCA(analysis) {
  const realWinRate = parseFloat(analysis.trades.winRate) / 100;
  const realTrades = analysis.trades.total;
  const avgWin = parseFloat(analysis.averages.avgWin);
  const avgLoss = parseFloat(analysis.averages.avgLoss);

  // DCA improves win rate by averaging into positions
  const dcaWinRate = Math.min(realWinRate + 0.15, 0.7); // Cap at 70%

  const wins = Math.round(realTrades * dcaWinRate);
  const losses = realTrades - wins;

  // DCA also reduces average loss (better entries = smaller stops)
  const improvedAvgLoss = avgLoss * 0.8; // 20% smaller losses
  const improvedAvgWin = avgWin * 1.1;   // 10% bigger wins

  const simulatedPnL = (wins * improvedAvgWin) + (losses * improvedAvgLoss);
  const improvement = simulatedPnL - parseFloat(analysis.pnl.realized);

  return {
    strategy: 'DCA (Dollar Cost Averaging)',
    originalWinRate: `${(realWinRate * 100).toFixed(0)}%`,
    simulatedWinRate: `${(dcaWinRate * 100).toFixed(0)}%`,
    improvement: 'Win rate +15%, Loss -20%, Win +10%',
    pnl: simulatedPnL.toFixed(4),
    improvementPnL: improvement.toFixed(4),
    description: `Using DCA to split entries into 3 parts at different prices could improve your win rate to ${(dcaWinRate * 100).toFixed(0)}% and reduce average losses. Potential improvement: ${improvement > 0 ? '+' : ''}${improvement.toFixed(4)} USDT.`
  };
}

/**
 * Generate comprehensive shadow comparison
 * @param {object} analysis - Real trading analysis
 * @returns {object} - All simulations
 */
function generateShadowComparison(analysis) {
  return {
    original: {
      pnl: parseFloat(analysis.pnl.realized),
      winRate: parseFloat(analysis.trades.winRate),
      riskReward: parseFloat(analysis.averages.riskReward),
      totalTrades: analysis.trades.total
    },
    strategies: [
      simulateImprovedRiskReward(analysis, 4),
      simulateSelectiveTrading(analysis, 50),
      simulateReducedTrading(analysis, 50),
      simulateDCA(analysis)
    ]
  };
}

/**
 * Generate Spot shadow comparison
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {object} - Spot shadow strategies
 */
function generateSpotShadowComparison(spotAnalysis) {
  return {
    original: {
      totalTrades: spotAnalysis.totalTrades,
      totalVolume: parseFloat(spotAnalysis.totalVolume),
      totalSymbols: spotAnalysis.totalSymbols
    },
    strategies: [
      simulateSpotDCA(spotAnalysis),
      simulateSpotHold(spotAnalysis)
    ]
  };
}

/**
 * Simulate DCA strategy for Spot
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {object} - Simulated DCA results
 */
function simulateSpotDCA(spotAnalysis) {
  if (spotAnalysis.totalTrades === 0) {
    return { strategy: 'DCA Strategy', error: 'No trades to simulate' };
  }

  const baseVolume = parseFloat(spotAnalysis.totalVolume) || (spotAnalysis.totalTrades * 1000);
  const improvedVolume = baseVolume * 1.15;

  return {
    strategy: 'Dollar Cost Averaging (DCA)',
    originalVolume: spotAnalysis.totalVolume || String(spotAnalysis.totalTrades * 1000),
    simulatedVolume: improvedVolume.toFixed(2),
    improvement: 'Better average entry price',
    description: 'Split your purchases into multiple buys at different prices to reduce average entry cost.'
  };
}

/**
 * Simulate Hold strategy for Spot
 * @param {object} spotAnalysis - Spot analysis results
 * @returns {object} - Simulated Hold results
 */
function simulateSpotHold(spotAnalysis) {
  if (spotAnalysis.totalTrades === 0) {
    return { strategy: 'Hold Strategy', error: 'No trades to simulate' };
  }

  const activeTrades = spotAnalysis.totalTrades;
  const holdingTrades = Math.ceil(activeTrades * 0.2);

  return {
    strategy: 'Buy and Hold',
    originalTrades: activeTrades,
    simulatedTrades: holdingTrades,
    reduction: `${Math.round((1 - holdingTrades / activeTrades) * 100)}%`,
    improvement: 'Less fees, less emotional trading',
    description: `Instead of ${activeTrades} trades, only make ${holdingTrades} well-researched trades per year.`
  };
}

module.exports = {
  simulateImprovedRiskReward,
  simulateSelectiveTrading,
  simulateReducedTrading,
  simulateDCA,
  generateShadowComparison,
  generateSpotShadowComparison,
  simulateSpotDCA,
  simulateSpotHold
};


path: tests/analyzer.test.cjs
content: /**
 * Unit tests for analyzer.cjs
 */

const { analyzeFuturesIncome, analyzeBehavior, analyzeSpotTrades } = require('../src/analyzer.cjs');

const MOCK_INCOME_HISTORY = [
  { incomeType: 'REALIZED_PNL', income: '100.50', time: 1710000000000 },
  { incomeType: 'REALIZED_PNL', income: '-30.25', time: 1710003600000 },
  { incomeType: 'REALIZED_PNL', income: '200.00', time: 1710007200000 },
  { incomeType: 'REALIZED_PNL', income: '-15.75', time: 1710010800000 },
  { incomeType: 'REALIZED_PNL', income: '80.00', time: 1710014400000 },
  { incomeType: 'COMMISSION', income: '-1.50', time: 1710000000000 },
  { incomeType: 'FUNDING_FEE', income: '-0.50', time: 1710000000000 },
  { incomeType: 'TRANSFER', income: '50.00', time: 1710000000000 },
];

let testsPassed = 0;
let testsFailed = 0;

function assert(condition, message) {
  if (condition) {
    testsPassed++;
  } else {
    testsFailed++;
    console.error(`FAIL: ${message}`);
  }
}

console.log('Running analyzer.cjs tests...\n');

// Test: analyzeFuturesIncome
const result = analyzeFuturesIncome(MOCK_INCOME_HISTORY);

assert(result.trades.total === 5, 'Should count 5 PnL trades');
assert(result.trades.wins === 3, 'Should have 3 wins (100.50, 200.00, 80.00)');
assert(result.trades.losses === 2, 'Should have 2 losses (-30.25, -15.75)');
assert(result.trades.winRate === '60.0', `Win rate should be 60.0, got ${result.trades.winRate}`);

const realized = parseFloat(result.pnl.realized);
assert(realized > 334 && realized < 335, `Realized PnL should be ~334.50, got ${realized}`);

const commissions = parseFloat(result.trades.commissions);
// Commission: -0.50 (FUNDING_FEE) + -1.50 (COMMISSION) = -2.00
assert(parseFloat(result.trades.commissions) === -1.5, `Commissions should be -1.50, got ${result.trades.commissions}`);

const avgWin = parseFloat(result.averages.avgWin);
assert(avgWin > 126 && avgWin < 127, `Avg win should be ~126.83, got ${avgWin}`);

const avgLoss = parseFloat(result.averages.avgLoss);
assert(avgLoss === -23, `Avg loss should be -23.00 (negative), got ${avgLoss}`);

const rr = parseFloat(result.averages.riskReward);
assert(rr > 5.5 && rr < 5.6, `Risk:reward should be ~5.52, got ${rr}`);

assert(result.streaks.maxWinStreak === 1, `Max win streak should be 1, got ${result.streaks.maxWinStreak}`);
assert(result.streaks.maxLossStreak >= 1, `Max loss streak should be >= 1, got ${result.streaks.maxLossStreak}`);

assert(result.period.days === 1, `Period days should be 1, got ${result.period.days}`);

// Test: analyzeByHour (via hourly field)
const hourlyKeys = Object.keys(result.hourly);
assert(hourlyKeys.length > 0, 'Should have hourly breakdown entries');

// Test: analyzeSpotTrades
const MOCK_SPOT_TRADES = {
  BTCUSDT: [
    { qty: '0.5', price: '50000', commission: '0.00025', isBuyer: true },
    { qty: '0.3', price: '51000', commission: '0.00015', isBuyer: false },
    { qty: '0.2', price: '49000', commission: '0.00010', isBuyer: true },
  ],
};

const spotResult = analyzeSpotTrades(MOCK_SPOT_TRADES);
assert(spotResult.totalSymbols === 1, `Should have 1 symbol, got ${spotResult.totalSymbols}`);
assert(spotResult.totalTrades === 3, `Should have 3 trades, got ${spotResult.totalTrades}`);

const volume = parseFloat(spotResult.totalVolume);
assert(volume > 49000, `Total volume should be > 49000, got ${volume}`);

assert(spotResult.symbols.BTCUSDT.buys === 2, 'BTCUSDT should have 2 buys');
assert(spotResult.symbols.BTCUSDT.sells === 1, 'BTCUSDT should have 1 sell');

const avgSize = parseFloat(spotResult.avgTradeSize);
// (0.5*50000 + 0.3*51000 + 0.2*49000) / 3 = 49700/3 = 16566.67
assert(avgSize > 16000 && avgSize < 17000, `Avg trade size should be ~16567, got ${avgSize}`);

// Test: analyzeBehavior
const badBehavior = analyzeBehavior({
  trades: { winRate: '20', total: 30 },
  averages: { riskReward: '1.5' },
  streaks: { maxLossStreak: 15 },
  period: { days: 10 },
  hourly: {}
});

const lowWR = badBehavior.find(i => i.message.includes('Win rate'));
assert(lowWR !== undefined, 'Should flag low win rate');

const lowRR = badBehavior.find(i => i.message.includes('Risk:Reward'));
assert(lowRR !== undefined, 'Should flag low risk:reward');

const tilt = badBehavior.find(i => i.message.includes('15'));
assert(tilt !== undefined, 'Should flag loss streak > 10');

console.log(`\n${'='.repeat(40)}`);
console.log(`Results: ${testsPassed} passed, ${testsFailed} failed`);
console.log(`${'='.repeat(40)}`);

process.exit(testsFailed > 0 ? 1 : 0);


path: tests/shadowSim.test.cjs
content: /**
 * Unit tests for shadowSim.cjs
 */

const {
  simulateImprovedRiskReward,
  simulateSelectiveTrading,
  simulateReducedTrading,
  simulateDCA,
  simulateSpotDCA,
  simulateSpotHold
} = require('../src/shadowSim.cjs');

const MOCK_ANALYSIS = {
  trades: {
    total: 10,
    winRate: '60',
    commissions: '5.00'
  },
  pnl: {
    realized: '300.00'
  },
  averages: {
    avgWin: '60.00',
    avgLoss: '30.00',
    riskReward: '2.00'
  },
  hourly: {
    '2': { total: 3, winRate: 33, pnl: '-10.00' },
    '8': { total: 5, winRate: 80, pnl: '150.00' },
    '14': { total: 4, winRate: 50, pnl: '20.00' },
    '20': { total: 2, winRate: 100, pnl: '100.00' }
  }
};

let testsPassed = 0;
let testsFailed = 0;

function assert(condition, message) {
  if (condition) {
    testsPassed++;
  } else {
    testsFailed++;
    console.error(`FAIL: ${message}`);
  }
}

console.log('Running shadowSim.cjs tests...\n');

// Test: simulateImprovedRiskReward
const rrResult = simulateImprovedRiskReward(MOCK_ANALYSIS, 4);
assert(rrResult.strategy.includes('1:4'), `Strategy should mention 1:4, got: ${rrResult.strategy}`);
assert(rrResult.wins === 6, `Should have 6 wins (60% of 10), got ${rrResult.wins}`);
assert(rrResult.losses === 4, `Should have 4 losses, got ${rrResult.losses}`);
assert(parseFloat(rrResult.pnl) > 300, `Simulated PnL should exceed real PnL (300), got ${rrResult.pnl}`);
assert(rrResult.improvement !== undefined, 'Should have improvement value');
assert(rrResult.description.length > 0, 'Should have a description');

// Test: simulateSelectiveTrading
const selResult = simulateSelectiveTrading(MOCK_ANALYSIS, 50);
assert(selResult.error === undefined, `Should not have error, got: ${selResult.error}`);
assert(selResult.strategy.includes('Selective'), 'Should mention Selective Trading');
assert(Array.isArray(selResult.goodHours), 'Should have goodHours array');
assert(selResult.goodHours.length >= 2, `Should have >= 2 good hours, got ${selResult.goodHours.length}`);
assert(selResult.simulatedPnL !== undefined, 'Should have simulatedPnL');
assert(parseFloat(selResult.realPnL) === 300, `Real PnL should be 300, got ${selResult.realPnL}`);
assert(selResult.simulatedTrades !== undefined, 'Should have simulatedTrades field');
assert(selResult.simulatedTrades >= 0, `simulatedTrades should be >= 0, got ${selResult.simulatedTrades}`);
assert(selResult.description.length > 0, 'Should have a description');

// Test: simulateSelectiveTrading with no good hours
const emptyResult = simulateSelectiveTrading({ hourly: {} }, 90);
assert(emptyResult.error === 'Not enough data to simulate', 'Should error when no good hours');

// Test: simulateSelectiveTrading with low threshold
const allResult = simulateSelectiveTrading(MOCK_ANALYSIS, 0);
assert(allResult.error === undefined, 'Should not error with 0 threshold');

// Test: simulateReducedTrading
const redResult = simulateReducedTrading(MOCK_ANALYSIS, 50);
assert(redResult.strategy.includes('50%'), 'Should mention 50% reduction');
assert(redResult.originalTrades === 10, `Original trades should be 10, got ${redResult.originalTrades}`);
assert(redResult.simulatedTrades === 5, `Simulated trades should be 5, got ${redResult.simulatedTrades}`);
assert(redResult.reduction === '50%', `Reduction should be 50%, got ${redResult.reduction}`);
assert(redResult.originalPnL === '300.00', `Original PnL should be 300.00, got ${redResult.originalPnL}`);
assert(redResult.commissionSavings !== undefined, 'Should have commission savings');
assert(redResult.improvement !== undefined, 'Should have improvement value');
assert(redResult.description.length > 0, 'Should have a description');

// Test: simulateDCA
const dcaResult = simulateDCA(MOCK_ANALYSIS);
assert(dcaResult.strategy.includes('DCA'), 'Should mention DCA');
assert(dcaResult.originalWinRate === '60%', `Original win rate should be 60%, got ${dcaResult.originalWinRate}`);
assert(parseFloat(dcaResult.simulatedWinRate.replace('%', '')) > 60, 'Simulated win rate should improve');
assert(dcaResult.improvementPnL !== undefined, 'Should have improvement PnL');
assert(dcaResult.description.length > 0, 'Should have a description');

// Test: simulateSpotDCA with trades
const spotDCA = simulateSpotDCA({ totalTrades: 10 });
assert(spotDCA.strategy.includes('DCA'), 'Should mention DCA');
assert(spotDCA.originalVolume !== undefined, 'Should have original volume');
assert(spotDCA.simulatedVolume !== undefined, 'Should have simulated volume');
assert(parseFloat(spotDCA.simulatedVolume) > parseFloat(spotDCA.originalVolume), `Simulated volume should be higher: ${spotDCA.simulatedVolume} vs ${spotDCA.originalVolume}`);
assert(spotDCA.description.length > 0, 'Should have a description');

// Test: simulateSpotDCA with no trades
const emptyDCA = simulateSpotDCA({ totalTrades: 0 });
assert(emptyDCA.error !== undefined, 'Should error with no trades');

// Test: simulateSpotHold with trades
const holdResult = simulateSpotHold({ totalTrades: 50 });
assert(holdResult.strategy.includes('Hold'), 'Should mention Hold');
assert(holdResult.originalTrades === 50, `Original trades should be 50, got ${holdResult.originalTrades}`);
assert(holdResult.simulatedTrades < holdResult.originalTrades, 'Simulated trades should be fewer');
assert(holdResult.reduction !== undefined, 'Should have reduction percentage');
assert(holdResult.description.length > 0, 'Should have a description');

// Test: simulateSpotHold with no trades
const emptyHold = simulateSpotHold({ totalTrades: 0 });
assert(emptyHold.error !== undefined, 'Should error with no trades');

// Test: generateShadowComparison (integration)
const shadow = require('../src/shadowSim.cjs').generateShadowComparison(MOCK_ANALYSIS);
assert(shadow.original !== undefined, 'Should have original stats');
assert(Array.isArray(shadow.strategies), 'Should have strategies array');
assert(shadow.strategies.length === 4, `Should have 4 strategies, got ${shadow.strategies.length}`);
assert(shadow.original.pnl === 300, `Original PnL should be 300, got ${shadow.original.pnl}`);
assert(shadow.original.winRate === 60, `Original win rate should be 60, got ${shadow.original.winRate}`);

console.log(`\n${'='.repeat(40)}`);
console.log(`Results: ${testsPassed} passed, ${testsFailed} failed`);
console.log(`${'='.repeat(40)}`);

process.exit(testsFailed > 0 ? 1 : 0);


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)