mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 01:21:11 +00:00
Merge branch 'develop' into ci/ccxt.pro
This commit is contained in:
commit
0f9335d242
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
@ -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)
|
||||
|
@ -566,11 +577,11 @@ jobs:
|
|||
docker version -f '{{.Server.Experimental}}'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
|
|
@ -16,7 +16,7 @@ repos:
|
|||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.7
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20240523
|
||||
- types-requests==2.32.0.20240602
|
||||
- types-tabulate==0.9.0.20240106
|
||||
- types-python-dateutil==2.9.0.20240316
|
||||
- SQLAlchemy==2.0.30
|
||||
|
@ -31,7 +31,7 @@ repos:
|
|||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.4.5'
|
||||
rev: 'v0.4.7'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp312-cp312-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.30-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
markdown==3.6
|
||||
mkdocs==1.6.0
|
||||
mkdocs-material==9.5.24
|
||||
mkdocs-material==9.5.25
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.8.1
|
||||
jinja2==3.1.4
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ class Exchange:
|
|||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange
|
||||
self._api_async: ccxt_pro.Exchange = None
|
||||
self._api_async: ccxt_pro.Exchange
|
||||
self._ws_async: ccxt_pro.Exchange = None
|
||||
self._exchange_ws: Optional[ExchangeWS] = None
|
||||
self._markets: Dict = {}
|
||||
|
@ -252,7 +252,7 @@ class Exchange:
|
|||
self.required_candle_call_count = 1
|
||||
if validate:
|
||||
# Initial markets load
|
||||
self._load_markets()
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
|
@ -387,7 +387,7 @@ class Exchange:
|
|||
"""exchange ccxt markets"""
|
||||
if not self._markets:
|
||||
logger.info("Markets were not loaded. Loading them now..")
|
||||
self._load_markets()
|
||||
self.reload_markets(True)
|
||||
return self._markets
|
||||
|
||||
@property
|
||||
|
@ -570,30 +570,26 @@ class Exchange:
|
|||
if self._exchange_ws:
|
||||
self._exchange_ws.reset_connections()
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
if self._api_async:
|
||||
self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={}))
|
||||
markets = self.loop.run_until_complete(
|
||||
self._api_async.load_markets(reload=reload, params={})
|
||||
)
|
||||
|
||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||
logger.warning("Could not load async markets. Reason: %s", e)
|
||||
return
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
return markets
|
||||
except asyncio.TimeoutError as e:
|
||||
logger.warning("Could not load markets. Reason: %s", e)
|
||||
raise TemporaryError from e
|
||||
|
||||
def _load_markets(self) -> None:
|
||||
"""Initialize markets both sync and async"""
|
||||
try:
|
||||
self._markets = self._api.load_markets(params={})
|
||||
self._load_async_markets()
|
||||
self._last_markets_refresh = dt_ts()
|
||||
if self._ft_has["needs_trading_fees"]:
|
||||
self._trading_fees = self.fetch_trading_fees()
|
||||
def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None:
|
||||
"""
|
||||
Reload / Initialize markets both sync and async if refresh interval has passed
|
||||
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Unable to initialize markets.")
|
||||
|
||||
def reload_markets(self, force: bool = False) -> None:
|
||||
"""Reload markets both sync and async if refresh interval has passed"""
|
||||
"""
|
||||
# Check whether markets have to be reloaded
|
||||
is_initial = self._last_markets_refresh == 0
|
||||
if (
|
||||
not force
|
||||
and self._last_markets_refresh > 0
|
||||
|
@ -602,16 +598,21 @@ class Exchange:
|
|||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
self._markets = self._api.load_markets(reload=True, params={})
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
# Reload async markets, then assign them to sync api
|
||||
self._markets = self._load_async_markets(reload=True)
|
||||
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
|
||||
if self._exchange_ws:
|
||||
# Set markets to avoid reloading on websocket api
|
||||
self._ws_async.set_markets(self._api.markets, self._api.currencies)
|
||||
self._last_markets_refresh = dt_ts()
|
||||
self.fill_leverage_tiers()
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
if is_initial and self._ft_has["needs_trading_fees"]:
|
||||
self._trading_fees = self.fetch_trading_fees()
|
||||
|
||||
if load_leverage_tiers and self.trading_mode == TradingMode.FUTURES:
|
||||
self.fill_leverage_tiers()
|
||||
except (ccxt.BaseError, TemporaryError):
|
||||
logger.exception("Could not load markets.")
|
||||
|
||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Requirements for freqtrade client library
|
||||
requests==2.32.2
|
||||
requests==2.32.3
|
||||
python-rapidjson==1.17
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
site_name: Freqtrade
|
||||
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/']
|
||||
site_description: Freqtrade is a free and open source crypto trading bot written in Python, designed to support all major exchanges and be controlled via Telegram or builtin Web UI
|
||||
repo_url: https://github.com/freqtrade/freqtrade
|
||||
edit_uri: edit/develop/docs/
|
||||
use_directory_urls: True
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==4.0.1
|
||||
ruff==0.4.5
|
||||
ruff==0.4.7
|
||||
mypy==1.10.0
|
||||
pre-commit==3.7.1
|
||||
pytest==8.2.1
|
||||
|
@ -27,6 +27,6 @@ nbconvert==7.16.4
|
|||
# mypy types
|
||||
types-cachetools==5.3.0.7
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.32.0.20240523
|
||||
types-requests==2.32.0.20240602
|
||||
types-tabulate==0.9.0.20240106
|
||||
types-python-dateutil==2.9.0.20240316
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
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
|
||||
ccxt==4.3.38
|
||||
cryptography==42.0.7
|
||||
aiohttp==3.9.5
|
||||
SQLAlchemy==2.0.30
|
||||
|
@ -11,10 +13,10 @@ python-telegram-bot==21.2
|
|||
httpx>=0.24.1
|
||||
humanize==4.9.0
|
||||
cachetools==5.3.3
|
||||
requests==2.32.2
|
||||
requests==2.32.3
|
||||
urllib3==2.2.1
|
||||
jsonschema==4.22.0
|
||||
TA-Lib==0.4.29
|
||||
TA-Lib==0.4.30
|
||||
technical==1.4.3
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.1.0
|
||||
|
@ -37,8 +39,8 @@ sdnotify==0.3.2
|
|||
|
||||
# API Server
|
||||
fastapi==0.111.0
|
||||
pydantic==2.7.1
|
||||
uvicorn==0.29.0
|
||||
pydantic==2.7.2
|
||||
uvicorn==0.30.1
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.2.1
|
||||
psutil==5.9.8
|
||||
|
|
285
setup.ps1
Normal file
285
setup.ps1
Normal file
|
@ -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
|
|
@ -238,7 +238,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
|
|||
def patch_exchange(
|
||||
mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True
|
||||
) -> None:
|
||||
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
|
||||
mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id))
|
||||
|
@ -248,6 +247,7 @@ def patch_exchange(
|
|||
mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers")
|
||||
|
||||
if mock_markets:
|
||||
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
||||
if isinstance(mock_markets, bool):
|
||||
mock_markets = get_markets()
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets))
|
||||
|
|
|
@ -181,7 +181,7 @@ def test_remove_exchange_credentials(default_conf) -> None:
|
|||
|
||||
|
||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
|
||||
|
||||
|
@ -518,7 +518,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
|
|||
mocker.patch(f"{EXMS}._init_ccxt")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_markets")
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
exchange = Exchange(default_conf)
|
||||
|
@ -527,28 +527,26 @@ def test__load_async_markets(default_conf, mocker, caplog):
|
|||
assert exchange._api_async.load_markets.call_count == 1
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
||||
exchange._load_async_markets()
|
||||
|
||||
assert log_has("Could not load async markets. Reason: deadbeef", caplog)
|
||||
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
|
||||
with pytest.raises(ccxt.BaseError, match="deadbeef"):
|
||||
exchange._load_async_markets()
|
||||
|
||||
|
||||
def test__load_markets(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
|
||||
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
Exchange(default_conf)
|
||||
assert log_has("Unable to initialize markets.", caplog)
|
||||
assert log_has("Could not load markets.", caplog)
|
||||
|
||||
expected_return = {"ETH/BTC": "available"}
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value=expected_return)
|
||||
api_mock.load_markets = get_mock_coro(return_value=expected_return)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
|
||||
ex = Exchange(default_conf)
|
||||
|
@ -563,12 +561,12 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
|
|||
start_dt = dt_now()
|
||||
time_machine.move_to(start_dt, tick=False)
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value=initial_markets)
|
||||
api_mock.load_markets = get_mock_coro(return_value=initial_markets)
|
||||
default_conf["exchange"]["markets_refresh_interval"] = 10
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, id="binance", mock_markets=False
|
||||
)
|
||||
exchange._load_async_markets = MagicMock()
|
||||
lam_spy = mocker.spy(exchange, "_load_async_markets")
|
||||
assert exchange._last_markets_refresh == dt_ts()
|
||||
|
||||
assert exchange.markets == initial_markets
|
||||
|
@ -577,42 +575,45 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
|
|||
# less than 10 minutes have passed, no reload
|
||||
exchange.reload_markets()
|
||||
assert exchange.markets == initial_markets
|
||||
assert exchange._load_async_markets.call_count == 0
|
||||
assert lam_spy.call_count == 0
|
||||
|
||||
api_mock.load_markets = MagicMock(return_value=updated_markets)
|
||||
api_mock.load_markets = get_mock_coro(return_value=updated_markets)
|
||||
# more than 10 minutes have passed, reload is executed
|
||||
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
|
||||
exchange.reload_markets()
|
||||
assert exchange.markets == updated_markets
|
||||
assert exchange._load_async_markets.call_count == 1
|
||||
assert lam_spy.call_count == 1
|
||||
assert log_has("Performing scheduled market reload..", caplog)
|
||||
|
||||
# Not called again
|
||||
exchange._load_async_markets.reset_mock()
|
||||
lam_spy.reset_mock()
|
||||
|
||||
exchange.reload_markets()
|
||||
assert exchange._load_async_markets.call_count == 0
|
||||
assert lam_spy.call_count == 0
|
||||
|
||||
|
||||
def test_reload_markets_exception(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
|
||||
api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
|
||||
default_conf["exchange"]["markets_refresh_interval"] = 10
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, id="binance", mock_markets=False
|
||||
)
|
||||
|
||||
exchange._last_markets_refresh = 2
|
||||
# less than 10 minutes have passed, no reload
|
||||
exchange.reload_markets()
|
||||
assert exchange._last_markets_refresh == 0
|
||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||
assert exchange._last_markets_refresh == 2
|
||||
assert log_has_re(r"Could not load markets\..*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
|
||||
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
||||
default_conf["stake_currency"] = stake_currency
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -623,7 +624,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
|||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
Exchange(default_conf)
|
||||
|
||||
|
@ -631,7 +631,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
|||
def test_validate_stakecurrency_error(default_conf, mocker, caplog):
|
||||
default_conf["stake_currency"] = "XRP"
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -642,14 +642,13 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
|
|||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
|
||||
):
|
||||
Exchange(default_conf)
|
||||
|
||||
type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError("No connection."))
|
||||
type(api_mock).load_markets = get_mock_coro(side_effect=ccxt.NetworkError("No connection."))
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
|
||||
with pytest.raises(
|
||||
|
@ -694,24 +693,26 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
|
|||
assert ex.get_pair_base_currency(pair) == expected
|
||||
|
||||
|
||||
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
||||
def test_validate_pairs(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
"XRP/BTC": {"quote": "BTC"},
|
||||
"NEO/BTC": {"quote": "BTC"},
|
||||
}
|
||||
)
|
||||
id_mock = PropertyMock(return_value="test_exchange")
|
||||
type(api_mock).id = id_mock
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(
|
||||
f"{EXMS}._load_async_markets",
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
"XRP/BTC": {"quote": "BTC"},
|
||||
"NEO/BTC": {"quote": "BTC"},
|
||||
},
|
||||
)
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
# test exchange.validate_pairs directly
|
||||
# No assert - but this should not fail (!)
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
|
@ -751,7 +752,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||
|
||||
def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -761,7 +762,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||
)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
|
||||
|
@ -774,9 +774,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||
)
|
||||
|
||||
|
||||
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
|
||||
def test_validate_pairs_stakecompatibility(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -787,17 +787,16 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
|
|||
)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
|
||||
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
default_conf["stake_currency"] = ""
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -808,7 +807,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
|
|||
)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
||||
|
@ -816,10 +814,10 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
|
|||
assert type(api_mock).load_markets.call_count == 1
|
||||
|
||||
|
||||
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
|
||||
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
|
||||
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/BTC": {"quote": "BTC"},
|
||||
"LTC/BTC": {"quote": "BTC"},
|
||||
|
@ -830,7 +828,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
|
|||
)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
|
||||
|
@ -847,7 +844,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
|
|||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -865,7 +862,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -895,7 +892,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||
del api_mock.timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
with pytest.raises(
|
||||
|
@ -917,7 +914,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||
del api_mock.timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={"timeframes": None}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
with pytest.raises(
|
||||
|
@ -939,7 +936,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
|||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -955,7 +952,7 @@ def test_validate_pricing(default_conf, mocker):
|
|||
}
|
||||
type(api_mock).has = PropertyMock(return_value=has)
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
|
@ -991,7 +988,7 @@ def test_validate_ordertypes(default_conf, mocker):
|
|||
|
||||
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
|
@ -1050,7 +1047,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
|
|||
default_conf["margin_mode"] = MarginMode.ISOLATED
|
||||
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
|
@ -1075,7 +1072,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
|
|||
def test_validate_order_types_not_in_config(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -1947,7 +1944,9 @@ def test_fetch_trading_fees(default_conf, mocker):
|
|||
assert api_mock.fetch_trading_fees.call_count == 1
|
||||
|
||||
api_mock.fetch_trading_fees.reset_mock()
|
||||
|
||||
# Reload-markets calls fetch_trading_fees, too - so the explicit calls in the below
|
||||
# exception test would be called twice.
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
ccxt_exceptionhandlers(
|
||||
mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees"
|
||||
)
|
||||
|
|
|
@ -699,18 +699,20 @@ def test_process_trade_creation(
|
|||
|
||||
|
||||
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
|
||||
# TODO: Move this test to test_worker
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
reload_markets=MagicMock(side_effect=TemporaryError),
|
||||
reload_markets=MagicMock(),
|
||||
create_order=MagicMock(side_effect=TemporaryError),
|
||||
)
|
||||
sleep_mock = mocker.patch("time.sleep")
|
||||
|
||||
worker = Worker(args=None, config=default_conf_usdt)
|
||||
patch_get_signal(worker.freqtrade)
|
||||
mocker.patch(f"{EXMS}.reload_markets", MagicMock(side_effect=TemporaryError))
|
||||
|
||||
worker._process_running()
|
||||
assert sleep_mock.called is True
|
||||
|
|
|
@ -1063,7 +1063,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
|
|||
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
|
||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||
mocker.patch(f"{EXMS}._load_markets")
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets()))
|
||||
(tmp_path / "hyperopt_results").mkdir(parents=True)
|
||||
# Dummy-reduce points to ensure scikit-learn is forced to generate new values
|
||||
|
|
177
tests/setup.Tests.ps1
Normal file
177
tests/setup.Tests.ps1
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user