diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d8368f95..b359ac458 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -318,6 +318,17 @@ jobs: run: | mypy freqtrade scripts tests + - name: Run Pester tests (PowerShell) + run: | + $PSVersionTable + Set-PSRepository psgallery -InstallationPolicy trusted + Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force + $Error.clear() + Invoke-Pester -Path "tests" -CI + if ($Error.Length -gt 0) {exit 1} + + shell: powershell + - name: Discord notification uses: rjstone/discord-webhook-notify@v1 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index d513c0af5..5e28d98fb 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -5,6 +5,30 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. Otherwise, please follow the instructions below. +All instructions assume that python 3.9+ is installed and available. + +## Clone the git repository + +First of all clone the repository by running: + +``` powershell +git clone https://github.com/freqtrade/freqtrade.git +``` + +Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions. + +## Install freqtrade automatically + +### Run the installation script + +The script will ask you a few questions to determine which parts should be installed. + +```powershell +Set-ExecutionPolicy -ExecutionPolicy Bypass +cd freqtrade +. .\setup.ps1 +``` + ## Install freqtrade manually !!! Note "64bit Python version" @@ -14,13 +38,7 @@ Otherwise, please follow the instructions below. !!! Hint Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information. -### 1. Clone the git repository - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -### 2. Install ta-lib +### Install ta-lib Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows). diff --git a/freqtrade/enums/runmode.py b/freqtrade/enums/runmode.py index d5c2cf652..a24dd6e2c 100644 --- a/freqtrade/enums/runmode.py +++ b/freqtrade/enums/runmode.py @@ -1,7 +1,7 @@ from enum import Enum -class RunMode(Enum): +class RunMode(str, Enum): """ Bot running mode (backtest, hyperopt, ...) can be "live", "dry-run", "backtest", "edge", "hyperopt". diff --git a/requirements.txt b/requirements.txt index 8c31cc80e..8adb2f84e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ numpy==1.26.4 pandas==2.2.2 +bottleneck==1.3.8 +numexpr==2.10.0 pandas-ta==0.3.14b ccxt==4.3.35 diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 000000000..8647bea94 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,285 @@ +Clear-Host + +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt" + +$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt") +$VenvName = ".venv" +$VenvDir = Join-Path $PSScriptRoot $VenvName + +function Write-Log { + param ( + [string]$Message, + [string]$Level = 'INFO' + ) + + if (-not (Test-Path -Path $LogFilePath)) { + New-Item -ItemType File -Path $LogFilePath -Force | Out-Null + } + + switch ($Level) { + 'INFO' { Write-Host $Message -ForegroundColor Green } + 'WARNING' { Write-Host $Message -ForegroundColor Yellow } + 'ERROR' { Write-Host $Message -ForegroundColor Red } + 'PROMPT' { Write-Host $Message -ForegroundColor Cyan } + } + + "${Level}: $Message" | Out-File $LogFilePath -Append +} + +function Get-UserSelection { + param ( + [string]$Prompt, + [string[]]$Options, + [string]$DefaultChoice = 'A', + [bool]$AllowMultipleSelections = $true + ) + + Write-Log "$Prompt`n" -Level 'PROMPT' + for ($I = 0; $I -lt $Options.Length; $I++) { + Write-Log "$([char](65 + $I)). $($Options[$I])" -Level 'PROMPT' + } + + if ($AllowMultipleSelections) { + Write-Log "`nSelect one or more options by typing the corresponding letters, separated by commas." -Level 'PROMPT' + } + else { + Write-Log "`nSelect an option by typing the corresponding letter." -Level 'PROMPT' + } + + [string]$UserInput = Read-Host + if ([string]::IsNullOrEmpty($UserInput)) { + $UserInput = $DefaultChoice + } + $UserInput = $UserInput.ToUpper() + + if ($AllowMultipleSelections) { + $Selections = $UserInput.Split(',') | ForEach-Object { $_.Trim() } + $SelectedIndices = @() + foreach ($Selection in $Selections) { + if ($Selection -match '^[A-Z]$') { + $Index = [int][char]$Selection - [int][char]'A' + if ($Index -ge 0 -and $Index -lt $Options.Length) { + $SelectedIndices += $Index + } + else { + Write-Log "Invalid input: $Selection. Please enter letters within the valid range of options." -Level 'ERROR' + return -1 + } + } + else { + Write-Log "Invalid input: $Selection. Please enter a letter between A and Z." -Level 'ERROR' + return -1 + } + } + return $SelectedIndices + } + else { + if ($UserInput -match '^[A-Z]$') { + $SelectedIndex = [int][char]$UserInput - [int][char]'A' + if ($SelectedIndex -ge 0 -and $SelectedIndex -lt $Options.Length) { + return $SelectedIndex + } + else { + Write-Log "Invalid input: $UserInput. Please enter a letter within the valid range of options." -Level 'ERROR' + return -1 + } + } + else { + Write-Log "Invalid input: $UserInput. Please enter a letter between A and Z." -Level 'ERROR' + return -1 + } + } +} + +function Exit-Script { + param ( + [int]$ExitCode, + [bool]$WaitForKeypress = $true + ) + + if ($ExitCode -ne 0) { + Write-Log "Script failed. Would you like to open the log file? (Y/N)" -Level 'PROMPT' + $openLog = Read-Host + if ($openLog -eq 'Y' -or $openLog -eq 'y') { + Start-Process notepad.exe -ArgumentList $LogFilePath + } + } + elseif ($WaitForKeypress) { + Write-Log "Press any key to exit..." + $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null + } + + return $ExitCode +} + +function Test-PythonExecutable { + param( + [string]$PythonExecutable + ) + + $DeactivateVenv = Join-Path $VenvDir "Scripts\Deactivate.bat" + if (Test-Path $DeactivateVenv) { + Write-Host "Deactivating virtual environment..." 2>&1 | Out-File $LogFilePath -Append + & $DeactivateVenv + Write-Host "Virtual environment deactivated." 2>&1 | Out-File $LogFilePath -Append + } + else { + Write-Host "Deactivation script not found: $DeactivateVenv" 2>&1 | Out-File $LogFilePath -Append + } + + $PythonCmd = Get-Command $PythonExecutable -ErrorAction SilentlyContinue + if ($PythonCmd) { + $VersionOutput = & $PythonCmd.Source --version 2>&1 + if ($LASTEXITCODE -eq 0) { + $Version = $VersionOutput | Select-String -Pattern "Python (\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } + Write-Log "Python version $Version found using executable '$PythonExecutable'." + return $true + } + else { + Write-Log "Python executable '$PythonExecutable' not working correctly." -Level 'ERROR' + return $false + } + } + else { + Write-Log "Python executable '$PythonExecutable' not found." -Level 'ERROR' + return $false + } +} + +function Find-PythonExecutable { + $PythonExecutables = @( + "python", + "python3.12", + "python3.11", + "python3.10", + "python3.9", + "python3", + "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe", + "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe", + "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe", + "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe", + "C:\Python312\python.exe", + "C:\Python311\python.exe", + "C:\Python310\python.exe", + "C:\Python39\python.exe" + ) + + + foreach ($Executable in $PythonExecutables) { + if (Test-PythonExecutable -PythonExecutable $Executable) { + return $Executable + } + } + + return $null +} +function Main { + "Starting the operations..." | Out-File $LogFilePath -Append + "Current directory: $(Get-Location)" | Out-File $LogFilePath -Append + + # Exit on lower versions than Python 3.9 or when Python executable not found + $PythonExecutable = Find-PythonExecutable + if ($null -eq $PythonExecutable) { + Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR' + Exit 1 + } + + # Define the path to the Python executable in the virtual environment + $ActivateVenv = "$VenvDir\Scripts\Activate.ps1" + + # Check if the virtual environment exists, if not, create it + if (-Not (Test-Path $ActivateVenv)) { + Write-Log "Virtual environment not found. Creating virtual environment..." -Level 'ERROR' + & $PythonExecutable -m venv $VenvName 2>&1 | Out-File $LogFilePath -Append + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to create virtual environment." -Level 'ERROR' + Exit-Script -exitCode 1 + } + else { + Write-Log "Virtual environment created." + } + } + + # Activate the virtual environment and check if it was successful + Write-Log "Virtual environment found. Activating virtual environment..." + & $ActivateVenv 2>&1 | Out-File $LogFilePath -Append + # Check if virtual environment is activated + if ($env:VIRTUAL_ENV) { + Write-Log "Virtual environment is activated at: $($env:VIRTUAL_ENV)" + } + else { + Write-Log "Failed to activate virtual environment." -Level 'ERROR' + Exit-Script -exitCode 1 + } + + # Ensure pip + python -m ensurepip --default-pip 2>&1 | Out-File $LogFilePath -Append + + # Pull latest updates only if the repository state is not dirty + Write-Log "Checking if the repository is clean..." + $Status = & "git" status --porcelain + if ($Status) { + Write-Log "Changes in local git repository. Skipping git pull." + } + else { + Write-Log "Pulling latest updates..." + & "git" pull 2>&1 | Out-File $LogFilePath -Append + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to pull updates from Git." -Level 'ERROR' + Exit-Script -exitCode 1 + } + } + + if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) { + # Install TA-Lib using the virtual environment's pip + Write-Log "Installing TA-Lib using virtual environment's pip..." + python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to install TA-Lib." -Level 'ERROR' + Exit-Script -exitCode 1 + } + } + + # Present options for requirement files + $SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A' + + # Cache the selected requirement files + $SelectedRequirementFiles = @() + $PipInstallArguments = @() + foreach ($Index in $SelectedIndices) { + $RelativePath = $RequirementFiles[$Index] + if (Test-Path $RelativePath) { + $SelectedRequirementFiles += $RelativePath + $PipInstallArguments += "-r", $RelativePath # Add each flag and path as separate elements + } + else { + Write-Log "Requirement file not found: $RelativePath" -Level 'ERROR' + Exit-Script -exitCode 1 + } + } + if ($PipInstallArguments.Count -ne 0) { + & pip install @PipInstallArguments # Use array splatting to pass arguments correctly + } + + # Install freqtrade from setup using the virtual environment's Python + Write-Log "Installing freqtrade from setup..." + pip install -e . 2>&1 | Out-File $LogFilePath -Append + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to install freqtrade." -Level 'ERROR' + Exit-Script -exitCode 1 + } + + Write-Log "Installing freqUI..." + python freqtrade install-ui 2>&1 | Out-File $LogFilePath -Append + if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to install freqUI." -Level 'ERROR' + Exit-Script -exitCode 1 + } + + Write-Log "Installation/Update complete!" + Exit-Script -exitCode 0 +} + +# Call the Main function +Main diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index fce01b9ee..887dfe3a4 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -50,6 +50,7 @@ def freqai_conf(default_conf, tmp_path): freqaiconf.update( { "datadir": Path(default_conf["datadir"]), + "runmode": "backtest", "strategy": "freqai_test_strat", "user_data_dir": tmp_path, "strategy-path": "freqtrade/tests/strategy/strats", diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index b1fed192b..f6c58a1e7 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -773,6 +773,7 @@ def test_VolumePairList_whitelist_gen( whitelist_result, caplog, ) -> None: + whitelist_conf["runmode"] = "backtest" whitelist_conf["pairlists"] = pairlists whitelist_conf["stake_currency"] = base_currency @@ -1270,6 +1271,7 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: {"method": "StaticPairList"}, {"method": "ShuffleFilter", "seed": 43}, ] + whitelist_conf["runmode"] = "backtest" exchange = get_patched_exchange(mocker, whitelist_conf) plm = PairListManager(exchange, whitelist_conf) diff --git a/tests/setup.Tests.ps1 b/tests/setup.Tests.ps1 new file mode 100644 index 000000000..e58a4729d --- /dev/null +++ b/tests/setup.Tests.ps1 @@ -0,0 +1,177 @@ + +Describe "Setup and Tests" { + BeforeAll { + # Setup variables + $SetupScriptPath = Join-Path $PSScriptRoot "..\setup.ps1" + $Global:LogFilePath = Join-Path $env:TEMP "script_log.txt" + + # Check if the setup script exists + if (-Not (Test-Path -Path $SetupScriptPath)) { + Write-Host "Error: setup.ps1 script not found at path: $SetupScriptPath" + exit 1 + } + + # Mock main to prevent it from running + Mock Main {} + + . $SetupScriptPath + } + + Context "Write-Log Tests" -Tag "Unit" { + It "should write INFO level log" { + if (Test-Path $Global:LogFilePath){ + Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue + } + + Write-Log -Message "Test Info Message" -Level "INFO" + $Global:LogFilePath | Should -Exist + + $LogContent = Get-Content $Global:LogFilePath + $LogContent | Should -Contain "INFO: Test Info Message" + } + + It "should write ERROR level log" { + if (Test-Path $Global:LogFilePath){ + Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue + } + + Write-Log -Message "Test Error Message" -Level "ERROR" + $Global:LogFilePath | Should -Exist + + $LogContent = Get-Content $Global:LogFilePath + $LogContent | Should -Contain "ERROR: Test Error Message" + } + } + + Describe "Get-UserSelection Tests" { + Context "Valid input" { + It "Should return the correct index for a valid single selection" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "B" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options + $Result | Should -Be 1 + } + + It "Should return the correct index for a valid single selection" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "b" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options + $Result | Should -Be 1 + } + + It "Should return the default choice when no input is provided" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options -defaultChoice "C" + $Result | Should -Be 2 + } + } + + Context "Invalid input" { + It "Should return -1 for an invalid letter selection" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "X" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options + $Result | Should -Be -1 + } + + It "Should return -1 for a selection outside the valid range" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "D" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options + $Result | Should -Be -1 + } + + It "Should return -1 for a non-letter input" { + $Options = @("Option1", "Option2", "Option3") + Mock Read-Host { return "1" } + $Result = Get-UserSelection -prompt "Select an option" -options $Options + $Result | Should -Be -1 + } + + It "Should return -1 for mixed valid and invalid input" { + Mock Read-Host { return "A,X,B,Y,C,Z" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A" + $Indices | Should -Be -1 + } + } + + Context "Multiple selections" { + It "Should handle valid input correctly" { + Mock Read-Host { return "A, B, C" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A" + $Indices | Should -Be @(0, 1, 2) + } + + It "Should handle valid input without whitespace correctly" { + Mock Read-Host { return "A,B,C" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A" + $Indices | Should -Be @(0, 1, 2) + } + + It "Should return indices for selected options" { + Mock Read-Host { return "a,b" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options + $Indices | Should -Be @(0, 1) + } + + It "Should return default choice if no input" { + Mock Read-Host { return "" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "C" + $Indices | Should -Be @(2) + } + + It "Should handle invalid input gracefully" { + Mock Read-Host { return "x,y,z" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A" + $Indices | Should -Be -1 + } + + It "Should handle input without whitespace" { + Mock Read-Host { return "a,b,c" } + $Options = @("Option1", "Option2", "Option3") + $Indices = Get-UserSelection -prompt "Select options" -options $Options + $Indices | Should -Be @(0, 1, 2) + } + } + } + + Describe "Exit-Script Tests" -Tag "Unit" { + BeforeEach { + Mock Write-Log {} + Mock Start-Process {} + Mock Read-Host { return "Y" } + } + + It "should exit with the given exit code without waiting for key press" { + $ExitCode = Exit-Script -ExitCode 0 -isSubShell $true -waitForKeypress $false + $ExitCode | Should -Be 0 + } + + It "should prompt to open log file on error" { + Exit-Script -ExitCode 1 -isSubShell $true -waitForKeypress $false + Assert-MockCalled Read-Host -Exactly 1 + Assert-MockCalled Start-Process -Exactly 1 + } + } + + Context 'Find-PythonExecutable' { + It 'Returns the first valid Python executable' { + Mock Test-PythonExecutable { $true } -ParameterFilter { $PythonExecutable -eq 'python' } + $Result = Find-PythonExecutable + $Result | Should -Be 'python' + } + + It 'Returns null if no valid Python executable is found' { + Mock Test-PythonExecutable { $false } + $Result = Find-PythonExecutable + $Result | Should -Be $null + } + } +}