diff --git a/.Jules/palette.md b/.Jules/palette.md index 5189b91..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. -## 2024-05-23 - CLI Accessibility and Configuration -**Learning:** Hardcoded simulation parameters create friction and limit accessibility. Adding CLI arguments (`argparse`) empowers users to explore scenarios without code edits. Additionally, providing a `--no-color` flag is crucial for users with visual impairments or those piping output to logs. -**Action:** Always implement standard CLI flags for configuration and accessibility (quiet mode, no-color) in command-line tools. +## 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/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..36232fc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,101 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '42 16 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index 03c45b7..907591b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,4 @@ # Python __pycache__/ -*.py[cod] -*$py.class +*.pyc diff --git a/README.md b/README.md index be561ef..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,16 +21,36 @@ pip install -r requirements.txt ## Usage -Run the simulation script: +Run the simulation script with default settings (60 days, $10k initial cash): ```bash python bitcoin_trading_simulation.py ``` +### Options + +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`: 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 30 --quiet --no-color +``` + ## Tests -Run the test suite: +Run the test suite using `pytest`: ```bash -python test.py +pytest ``` diff --git a/__pycache__/bitcoin_trading_simulation.cpython-312.pyc b/__pycache__/bitcoin_trading_simulation.cpython-312.pyc new file mode 100644 index 0000000..52abfc2 Binary files /dev/null and b/__pycache__/bitcoin_trading_simulation.cpython-312.pyc differ diff --git a/__pycache__/test_bitcoin_trading.cpython-312.pyc b/__pycache__/test_bitcoin_trading.cpython-312.pyc new file mode 100644 index 0000000..ecc297a Binary files /dev/null and b/__pycache__/test_bitcoin_trading.cpython-312.pyc differ diff --git a/bitcoin_trading_simulation.py b/bitcoin_trading_simulation.py index 3557c3f..ccb5edd 100644 --- a/bitcoin_trading_simulation.py +++ b/bitcoin_trading_simulation.py @@ -1,6 +1,8 @@ import argparse import numpy as np import pandas as pd +import argparse + class Colors: HEADER = '\033[95m' @@ -19,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. @@ -33,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. @@ -43,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. @@ -54,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. @@ -71,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'] @@ -94,37 +101,51 @@ 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 -def main(days, initial_price, volatility, initial_cash, quiet, no_color): - if no_color: + +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, 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() + + if args.no_color: Colors.disable() # Simulate prices - prices = simulate_bitcoin_prices(days=days, initial_price=initial_price, volatility=volatility) - + 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=initial_cash, quiet=quiet) - + portfolio = simulate_trading(signals, initial_cash=args.initial_cash, quiet=args.quiet) + # Final portfolio performance final_value = portfolio['total_value'].iloc[-1] + initial_cash = args.initial_cash profit = final_value - initial_cash - + # Compare with buy and hold strategy - buy_and_hold_btc = initial_cash / prices.iloc[0] + 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: ${initial_cash:.2f}") + print(f"Initial Cash: ${args.initial_cash:.2f}") print(f"Final Portfolio Value: ${final_value:.2f}") if profit >= 0: diff --git a/test.py b/test.py deleted file mode 100644 index b326d87..0000000 --- a/test.py +++ /dev/null @@ -1,3 +0,0 @@ -# Filename: tests/test_sample.py -def test_example(): - assert 1 + 1 == 2 diff --git a/test_bitcoin_trading.py b/test_bitcoin_trading.py index 0d4c256..e7eac6f 100644 --- a/test_bitcoin_trading.py +++ b/test_bitcoin_trading.py @@ -1,64 +1,74 @@ -import unittest -from unittest.mock import patch +import pytest import pandas as pd -from bitcoin_trading_simulation import Colors, simulate_trading, main, simulate_bitcoin_prices, calculate_moving_averages, generate_trading_signals +from bitcoin_trading_simulation import ( + simulate_trading, Colors, calculate_moving_averages, + generate_trading_signals, simulate_bitcoin_prices +) -class TestBitcoinSimulation(unittest.TestCase): - def tearDown(self): - # Reset Colors to default just in case - Colors.HEADER = '\033[95m' - Colors.BLUE = '\033[94m' - Colors.GREEN = '\033[92m' - Colors.RED = '\033[91m' - Colors.ENDC = '\033[0m' - Colors.BOLD = '\033[1m' +@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_colors_disable(self): - # Disable colors - Colors.disable() - # Verify empty strings - self.assertEqual(Colors.HEADER, '') - self.assertEqual(Colors.BLUE, '') - self.assertEqual(Colors.GREEN, '') - self.assertEqual(Colors.RED, '') - self.assertEqual(Colors.ENDC, '') - self.assertEqual(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 - @patch('builtins.print') - def test_simulate_trading_quiet(self, mock_print): - # Create dummy signals - prices = pd.Series([100, 101, 102], name='Price') - signals = pd.DataFrame(index=prices.index) - signals['price'] = prices - signals['positions'] = 0.0 + simulate_trading(signals, initial_cash=1000, quiet=True) - simulate_trading(signals, quiet=True) + captured = capsys.readouterr() + assert captured.out == "" - # Verify print was not called - mock_print.assert_not_called() - @patch('builtins.print') - def test_simulate_trading_verbose(self, mock_print): - # Create dummy signals - prices = pd.Series([100, 101, 102], name='Price') - signals = pd.DataFrame(index=prices.index) - signals['price'] = prices - signals['positions'] = 0.0 +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, quiet=False) + simulate_trading(signals, initial_cash=1000, quiet=False) - # Verify print was called (at least for the header) - self.assertTrue(mock_print.called) + captured = capsys.readouterr() + assert "Daily Trading Ledger" in captured.out + assert "Portfolio Value" in captured.out - def test_main_runs(self): - # Run main with small number of days to ensure no errors - # We can suppress output with quiet=True - try: - main(days=5, initial_price=100, volatility=0.01, initial_cash=1000, quiet=True, no_color=True) - except Exception as e: - self.fail(f"main() raised {e} unexpectedly!") -if __name__ == '__main__': - unittest.main() +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 diff --git a/test_simulation.py b/test_simulation.py new file mode 100644 index 0000000..0f4f1f8 --- /dev/null +++ b/test_simulation.py @@ -0,0 +1,33 @@ +import pytest +import pandas as pd +import numpy as np +from bitcoin_trading_simulation import simulate_bitcoin_prices, calculate_moving_averages, generate_trading_signals + +def test_simulate_bitcoin_prices(): + days = 10 + prices = simulate_bitcoin_prices(days=days, initial_price=50000) + assert len(prices) == days + assert isinstance(prices, pd.Series) + assert prices.name == 'Price' + +def test_calculate_moving_averages(): + prices = pd.Series([100, 101, 102, 103, 104, 105, 106, 107, 108, 109], name='Price') + signals = calculate_moving_averages(prices, short_window=3, long_window=5) + assert 'short_mavg' in signals.columns + assert 'long_mavg' in signals.columns + assert not signals['short_mavg'].isnull().all() + +def test_generate_trading_signals(): + # Create dummy signals DataFrame + data = { + 'price': [100, 101, 102, 103, 104], + 'short_mavg': [100, 101, 105, 102, 100], + 'long_mavg': [100, 100, 100, 103, 105] + } + signals = pd.DataFrame(data) + signals = generate_trading_signals(signals) + + assert 'signal' in signals.columns + assert 'positions' in signals.columns + # Check that positions are calculated (not all nan, though first might be) + assert signals['positions'].isin([0, 1, -1, 2, -2, np.nan]).any()