diff --git a/.Jules/palette.md b/.Jules/palette.md index 9eea189..1279bd2 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -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. diff --git a/README.md b/README.md index 6f2e3e3..4cd8db6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -20,7 +21,7 @@ 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 @@ -28,25 +29,28 @@ 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 ``` diff --git a/bitcoin_trading_simulation.py b/bitcoin_trading_simulation.py index f7af61c..2f7f5af 100644 --- a/bitcoin_trading_simulation.py +++ b/bitcoin_trading_simulation.py @@ -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' @@ -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. @@ -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. @@ -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. @@ -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. @@ -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'] @@ -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() @@ -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}") diff --git a/test_bitcoin_trading.py b/test_bitcoin_trading.py index 9a63e5d..e7eac6f 100644 --- a/test_bitcoin_trading.py +++ b/test_bitcoin_trading.py @@ -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