Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
**Learning:** Adding color-coded indicators (Green/Red) and emojis (💰, 📉) in CLI tools significantly reduces cognitive load when parsing financial data streams. It transforms a wall of text into a scannable narrative.
**Action:** For data-heavy CLI applications, always implement a semantic color system and visual anchors (icons/emojis) for key events.

## 2026-02-11 - Accessibility in CLI Tools
**Learning:** Hardcoded ANSI colors exclude users with visual impairments or conflicting terminal themes. Providing a standard `--no-color` flag is a zero-cost accessibility win that respects user preference.
**Action:** Always wrap color codes in a configurable class and expose a disable mechanism via CLI arguments.
## 2024-05-23 - CLI Accessibility and Control
**Learning:** While color and emojis enhance UX, they can be inaccessible (color blindness) or intrusive (automation logs). Providing `--no-color` and `--quiet` flags is essential for a robust CLI tool that respects user context and accessibility needs.
**Action:** Always include flags to disable visual enhancements and suppress verbose output in CLI tools.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ A Python-based CLI tool that simulates Bitcoin trading using a 'Golden Cross' mo

## Features

- **Price Simulation:** Uses Geometric Brownian Motion to simulate 60 days of Bitcoin prices.
- **Price Simulation:** Uses Geometric Brownian Motion to simulate Bitcoin prices.
- **Trading Strategy:** Implements a Golden Cross strategy (Short MA > Long MA = Buy, Short MA < Long MA = Sell).
- **Rich CLI Output:** features color-coded logs (Green for Buy/Profit, Red for Sell/Loss) and emojis for better readability.
- **Rich CLI Output:** Features color-coded logs (Green for Buy/Profit, Red for Sell/Loss) and emojis for better readability.
- **Performance metrics:** Compares the strategy's performance against a "Buy and Hold" approach.
- **Customizable:** Configure simulation parameters via command-line arguments.

## Installation

Expand All @@ -20,33 +21,36 @@ pip install -r requirements.txt

## Usage

Run the simulation script with default settings:
Run the simulation script with default settings (60 days, $10k initial cash):

```bash
python bitcoin_trading_simulation.py
```

### Options

You can customize the simulation with the following arguments:
Customize the simulation with the following arguments:

```bash
python bitcoin_trading_simulation.py --days 100 --initial-cash 5000 --initial-price 60000 --volatility 0.03
```

- `--days`: Number of days to simulate (default: 60)
- `--initial-cash`: Initial cash amount (default: 10000)
- `--initial-price`: Initial Bitcoin price (default: 50000)
- `--volatility`: Volatility factor (default: 0.02)
- `--quiet`: Suppress daily ledger output (show only final results)
- `--no-color`: Disable colored output (for accessibility)

**Example:**
- `--volatility`: Price volatility (default: 0.02)
- `--quiet`: Suppress daily portfolio log (only show final result)
- `--no-color`: Disable colored output (for accessibility or logging)

Example:
```bash
python bitcoin_trading_simulation.py --days 100 --initial-cash 5000 --quiet
python bitcoin_trading_simulation.py --days 30 --quiet --no-color
```

## Tests

Run the test suite:
Run the test suite using `pytest`:

```bash
python -m unittest test_bitcoin_trading.py
pytest
```
45 changes: 27 additions & 18 deletions bitcoin_trading_simulation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import argparse
import sys
import numpy as np
import pandas as pd
import argparse


class Colors:
HEADER = '\033[95m'
BLUE = '\033[94m'
Expand All @@ -21,6 +21,7 @@ def disable(cls):
cls.ENDC = ''
cls.BOLD = ''


def simulate_bitcoin_prices(days=60, initial_price=50000, volatility=0.02):
"""
Simulates Bitcoin prices for a given number of days using Geometric Brownian Motion.
Expand All @@ -35,6 +36,7 @@ def simulate_bitcoin_prices(days=60, initial_price=50000, volatility=0.02):
prices.append(prices[-1] + price_change)
return pd.Series(prices, name='Price')


def calculate_moving_averages(prices, short_window=7, long_window=30):
"""
Calculates short and long moving averages for a given price series.
Expand All @@ -45,6 +47,7 @@ def calculate_moving_averages(prices, short_window=7, long_window=30):
signals['long_mavg'] = prices.rolling(window=long_window, min_periods=1, center=False).mean()
return signals


def generate_trading_signals(signals):
"""
Generates trading signals based on the Golden Cross strategy.
Expand All @@ -56,11 +59,12 @@ def generate_trading_signals(signals):
signals.loc[signals['short_mavg'] > signals['long_mavg'], 'signal'] = 1.0
# A Death Cross (sell signal)
signals.loc[signals['short_mavg'] < signals['long_mavg'], 'signal'] = -1.0

# We create 'positions' to represent the trading action: 1 for buy, -1 for sell, 0 for hold
signals['positions'] = signals['signal'].diff().shift(1)
return signals


def simulate_trading(signals, initial_cash=10000, quiet=False):
"""
Simulates trading based on signals and prints a daily ledger.
Expand All @@ -73,6 +77,7 @@ def simulate_trading(signals, initial_cash=10000, quiet=False):

if not quiet:
print(f"{Colors.HEADER}{Colors.BOLD}------ Daily Trading Ledger ------{Colors.ENDC}")

for i, row in signals.iterrows():
if i > 0:
portfolio.loc[i, 'cash'] = portfolio.loc[i-1, 'cash']
Expand All @@ -96,19 +101,22 @@ def simulate_trading(signals, initial_cash=10000, quiet=False):
portfolio.loc[i, 'btc'] = 0

portfolio.loc[i, 'total_value'] = portfolio.loc[i, 'cash'] + portfolio.loc[i, 'btc'] * row['price']

if not quiet:
print(f"Day {i}: Portfolio Value: ${portfolio.loc[i, 'total_value']:.2f}, Cash: ${portfolio.loc[i, 'cash']:.2f}, BTC: {portfolio.loc[i, 'btc']:.4f}")

print(f"Day {i}: Portfolio Value: ${portfolio.loc[i, 'total_value']:.2f}, "
f"Cash: ${portfolio.loc[i, 'cash']:.2f}, BTC: {portfolio.loc[i, 'btc']:.4f}")

return portfolio


if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Bitcoin Trading Simulation')
parser.add_argument('--days', type=int, default=60, help='Number of days to simulate')
parser.add_argument('--initial-cash', type=float, default=10000.0, help='Initial cash amount')
parser.add_argument('--initial-price', type=float, default=50000.0, help='Initial Bitcoin price')
parser.add_argument('--volatility', type=float, default=0.02, help='Volatility factor')
parser.add_argument('--quiet', action='store_true', help='Suppress daily ledger output')
parser.add_argument('--no-color', action='store_true', help='Disable colored output')
parser = argparse.ArgumentParser(description="Bitcoin Trading Simulation")
parser.add_argument("--days", type=int, default=60, help="Number of days to simulate")
parser.add_argument("--initial-cash", type=float, default=10000, help="Initial cash amount")
parser.add_argument("--initial-price", type=float, default=50000, help="Initial Bitcoin price")
parser.add_argument("--volatility", type=float, default=0.02, help="Price volatility")
parser.add_argument("--quiet", action="store_true", help="Suppress daily portfolio log")
parser.add_argument("--no-color", action="store_true", help="Disable colored output")

args = parser.parse_args()

Expand All @@ -117,24 +125,25 @@ def simulate_trading(signals, initial_cash=10000, quiet=False):

# Simulate prices
prices = simulate_bitcoin_prices(days=args.days, initial_price=args.initial_price, volatility=args.volatility)

# Calculate moving averages
signals = calculate_moving_averages(prices)

# Generate trading signals
signals = generate_trading_signals(signals)

# Simulate trading
portfolio = simulate_trading(signals, initial_cash=args.initial_cash, quiet=args.quiet)

# Final portfolio performance
final_value = portfolio['total_value'].iloc[-1]
profit = final_value - args.initial_cash

initial_cash = args.initial_cash
profit = final_value - initial_cash

# Compare with buy and hold strategy
buy_and_hold_btc = args.initial_cash / prices.iloc[0]
buy_and_hold_value = buy_and_hold_btc * prices.iloc[-1]

print(f"\n{Colors.HEADER}{Colors.BOLD}------ Final Portfolio Performance ------{Colors.ENDC}")
print(f"Initial Cash: ${args.initial_cash:.2f}")
print(f"Final Portfolio Value: ${final_value:.2f}")
Expand Down
173 changes: 73 additions & 100 deletions test_bitcoin_trading.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,74 @@
import unittest
import sys
import io
import pytest
import pandas as pd
import subprocess
from bitcoin_trading_simulation import Colors, simulate_trading, simulate_bitcoin_prices

class TestBitcoinTradingSimulation(unittest.TestCase):

def setUp(self):
# Reset Colors to default before each test to prevent side effects
Colors.HEADER = '\033[95m'
Colors.BLUE = '\033[94m'
Colors.GREEN = '\033[92m'
Colors.RED = '\033[91m'
Colors.ENDC = '\033[0m'
Colors.BOLD = '\033[1m'

def test_simulate_bitcoin_prices(self):
prices = simulate_bitcoin_prices(days=10, initial_price=100)
self.assertEqual(len(prices), 10)
self.assertEqual(prices.iloc[0], 100)

def test_colors_disable(self):
Colors.disable()
self.assertEqual(Colors.HEADER, '')
self.assertEqual(Colors.GREEN, '')
self.assertEqual(Colors.RED, '')

def test_quiet_mode_function(self):
# Create dummy signals
prices = pd.Series([100, 101, 102], name='Price')
signals = pd.DataFrame(index=prices.index)
signals['price'] = prices
signals['positions'] = 0.0

# Capture stdout
captured_output = io.StringIO()
sys.stdout = captured_output

simulate_trading(signals, quiet=True)

sys.stdout = sys.__stdout__
output = captured_output.getvalue()

# Expect no output in quiet mode (since no trades and quiet=True)
self.assertEqual(output, "")

def test_verbose_mode_function(self):
# Create dummy signals
prices = pd.Series([100, 101, 102], name='Price')
signals = pd.DataFrame(index=prices.index)
signals['price'] = prices
signals['positions'] = 0.0

# Capture stdout
captured_output = io.StringIO()
sys.stdout = captured_output

simulate_trading(signals, quiet=False)

sys.stdout = sys.__stdout__
output = captured_output.getvalue()

self.assertIn("Daily Trading Ledger", output)

def test_integration_no_color(self):
# Run the script via subprocess to verify --no-color
result = subprocess.run(
[sys.executable, 'bitcoin_trading_simulation.py', '--days', '5', '--no-color'],
capture_output=True,
text=True
)
# Check that ANSI codes are NOT present
self.assertNotIn('\033[', result.stdout)
# Check that the output is still meaningful
self.assertIn('Final Portfolio Performance', result.stdout)

def test_integration_quiet(self):
# Run the script via subprocess to verify --quiet
result = subprocess.run(
[sys.executable, 'bitcoin_trading_simulation.py', '--days', '5', '--quiet'],
capture_output=True,
text=True
)
# Check that Daily Ledger is NOT present
self.assertNotIn('Daily Trading Ledger', result.stdout)
# Check that Final Performance IS present
self.assertIn('Final Portfolio Performance', result.stdout)

def test_integration_args(self):
# Verify custom arguments work
result = subprocess.run(
[sys.executable, 'bitcoin_trading_simulation.py', '--days', '5', '--initial-cash', '500'],
capture_output=True,
text=True
)
self.assertIn('Initial Cash: $500.00', result.stdout)

if __name__ == '__main__':
unittest.main()
from bitcoin_trading_simulation import (
simulate_trading, Colors, calculate_moving_averages,
generate_trading_signals, simulate_bitcoin_prices
)


@pytest.fixture
def reset_colors():
# Save original colors
original_colors = {
'HEADER': Colors.HEADER,
'BLUE': Colors.BLUE,
'GREEN': Colors.GREEN,
'RED': Colors.RED,
'ENDC': Colors.ENDC,
'BOLD': Colors.BOLD,
}
yield
# Restore colors
Colors.HEADER = original_colors['HEADER']
Colors.BLUE = original_colors['BLUE']
Colors.GREEN = original_colors['GREEN']
Colors.RED = original_colors['RED']
Colors.ENDC = original_colors['ENDC']
Colors.BOLD = original_colors['BOLD']


def test_simulate_trading_quiet_mode(capsys):
"""Test that quiet mode suppresses output."""
signals = pd.DataFrame(index=range(5))
signals['price'] = [100.0, 101.0, 102.0, 103.0, 104.0]
signals['positions'] = [0.0] * 5

simulate_trading(signals, initial_cash=1000, quiet=True)

captured = capsys.readouterr()
assert captured.out == ""


def test_simulate_trading_verbose_mode(capsys):
"""Test that verbose mode prints daily ledger."""
signals = pd.DataFrame(index=range(2))
signals['price'] = [100.0, 101.0]
signals['positions'] = [0.0, 0.0]

simulate_trading(signals, initial_cash=1000, quiet=False)

captured = capsys.readouterr()
assert "Daily Trading Ledger" in captured.out
assert "Portfolio Value" in captured.out


def test_colors_disable(reset_colors):
"""Test that Colors.disable() clears color codes."""
assert Colors.HEADER != ""
Colors.disable()
assert Colors.HEADER == ""
assert Colors.GREEN == ""
assert Colors.RED == ""


def test_simulation_integration():
"""Test full simulation pipeline with small parameters."""
prices = simulate_bitcoin_prices(days=10, initial_price=100)
signals = calculate_moving_averages(prices, short_window=2, long_window=5)
signals = generate_trading_signals(signals)
portfolio = simulate_trading(signals, quiet=True)

assert len(portfolio) == 10
assert 'total_value' in portfolio.columns
assert 'btc' in portfolio.columns
assert 'cash' in portfolio.columns