SageTrader.psm1
Class ChiaAsset { [string]$name [string]$id [Uint64]$denom [string]$tibet_liquidity_asset_id [string]$tibet_pair_id [string]$code [UInt64]$amount [decimal]$formatted_amount ChiaAsset(){} ChiaAsset([PSCustomobject]$props) { $this.Init([PSCustomObject]$props) } [void] Init([PSCustomobject]$props) { if(-not $null -eq $props.name){ $this.name = $props.name } $this.id = $props.id $this.denom = [UInt64]$props.denom if(-not $null -eq $props.tibet_liquidity_asset_id){ $this.tibet_liquidity_asset_id = $props.tibet_liquidity_asset_id $this.tibet_pair_id = $props.tibet_pair_id } $this.code = $props.code if(-not $null -eq $props.amount){ $this.amount = [UInt64]$props.amount } else { $this.amount = 0 } } [void] setAmount([decimal]$amt){ $this.amount = $amt * $this.denom } [void] setAmountFromMojo([UInt64]$mojo){ $this.amount = $mojo } [decimal] getFormattedAmount() { if($this.amount -eq 0){ return 0 } $this.formatted_amount = $this.amount / $this.denom return $this.formatted_amount } [UInt64] getBalance() { if($this.id -eq "XCH"){ $xch = Get-SageSyncStatus return $xch.balance } $cat = Get-SageCat -asset_id $this.id return $cat.balance } } Class DexieQuote{ [ChiaAsset]$from [ChiaAsset]$to [UInt64]$suggested_tx_fee [UInt64]$combination_fee [decimal]$price [PSObject]$sageoffer DexieQuote([PSCustomObject]$Props){ $this.Init([PSCustomObject]$Props) } Build(){ $offer = Build-SageOffer if($this.from.id -eq "xch"){ $offer.offerXch($this.from.amount) $offer.requestCat($this.to.id, $this.to.amount) } else { $offer.offerCat($this.from.id, $this.from.amount) $offer.requestXch($this.to.amount) } $this.sageoffer = $offer } [void] Init([PSCustomobject]$props) { $this.from = (Get-ChiaAsset -id ($props.from)) $this.from.setAmountFromMojo([UInt64]$props.from_amount) $this.to = (Get-ChiaAsset -id ($props.to)) $this.to.setAmountFromMojo([UInt64]$props.to_amount) if($props.from -eq "XCH"){ $this.price = [Math]::Round($this.to.getFormattedAmount() / $this.from.getFormattedAmount(),3) } else { $this.price = [Math]::Round($this.from.getFormattedAmount() / $this.to.getFormattedAmount(),3) } $this.suggested_tx_fee = [UInt64]$props.suggested_tx_fee $this.combination_fee = [UInt64]$props.combination_fee } } Class ChiaDCABot{ [string]$id [string]$name [ChiaAsset]$offered_asset [ChiaAsset]$requested_asset [UInt64]$minutes_between_trades [decimal]$minimum_price [decimal]$maximum_price [UInt64]$max_token_spend [UInt64]$current_token_spend [bool]$active [datetime]$last_trade_time [datetime]$last_attemted_trade_time [datetime]$next_trade_time [array]$trade_history [uint64]$default_fee [uint64]$fingerprint ChiaDCABot(){ $this.id = [Guid]::NewGuid().ToString() $this.last_attemted_trade_time = Get-Date $this.last_trade_time = Get-Date $this.next_trade_time = Get-Date $this.active = $false $this.trade_history = @() $this.default_fee = 0 $this.current_token_spend = 0 } ChiaDCABot([PSCustomobject]$props) {$this.Init([PSCustomObject]$props)} [void] destroy(){ $path = Get-SageTraderPath("DCABots") $path = Join-Path -Path $path -ChildPath "$($this.id).json" $check = Read-SpectreConfirm -Message "Are you sure you want to delete this bot?" -DefaultAnswer "n" if($check -eq $true){ if(Test-Path -Path $path){ Remove-Item -Path $path -Force Write-SpectreHost -Message "[green]Bot deleted successfully.[/]" } else { Write-SpectreHost -Message "[red]Bot not found.[/]" } } else { Write-SpectreHost -Message "[yellow]Bot deletion cancelled.[/]" } } [void] Init([PSCustomobject]$props) { $this.id = $props.id $this.name = $props.name $this.offered_asset = [ChiaAsset]::new($props.offered_asset) $this.requested_asset = [ChiaAsset]::new($props.requested_asset) $this.minutes_between_trades = [UInt64]$props.minutes_between_trades $this.minimum_price = [decimal]$props.minimum_price $this.maximum_price = [decimal]$props.maximum_price $this.active = $props.active $this.last_trade_time = [datetime]::Parse($props.last_trade_time) $this.last_attemted_trade_time = [datetime]::Parse($props.last_attemted_trade_time) $this.next_trade_time = [datetime]::Parse($props.next_trade_time) $this.default_fee = [UInt64]$props.default_fee $this.fingerprint = [UInt64]$props.fingerprint $this.max_token_spend = [UInt64]$props.max_token_spend $this.current_token_spend = [UInt64]$props.current_token_spend } [DexieQuote] GetQuote(){ $quote = Get-DexieQuote -from $this.offered_asset.id -to $this.requested_asset.id -from_amount $this.offered_asset.amount if ($null -eq $quote) { Write-SpectreHost -Message "[red]Failed to get a quote. Please check your assets and try again.[/]" return $null } return [DexieQuote]::new($($quote.quote)) } [bool] hasValidBalance(){ if($this.max_token_spend -gt 0){ Write-SpectreHost -message "[green]Bot [/][blue]$($this.name)[/][green] has spent [/][Magenta2_1]$($this.current_token_spend) / $($this.max_token_spend)[/]." } else { Write-SpectreHost -message "[green]Bot [/][blue]$($this.name)[/][green] has spent [/][Magenta2_1]$($this.current_token_spend)[/]." } $ballance = $this.offered_asset.getBalance() if($ballance -lt $this.offered_asset.amount){ Write-SpectreHost -Message "[red]Insufficient balance for bot[/][blue] $($this.name)[/][red]. You need at least [/][green]$($this.offered_asset.getFormattedAmount()) $($this.offered_asset.name)[/][red] to run this bot.[/]" return $false } if(($this.max_token_spend -ne 0) -and (($this.current_token_spend + $this.offered_asset.amount) -gt $this.max_token_spend)){ Write-SpectreHost -Message " [red]Bot [/][blue]$($this.name)[/][red] has reached the maximum token spend of [/][green]$($this.max_token_spend)[/]. [red]Disabling the bot.[/]" $this.deactivate() return $false } return $true } [bool] isLoggedIn(){ $fp = (Invoke-SageRPC -endpoint get_key -json @{}) if($null -eq $fp){ Write-SpectreHost -Message "[red]Bot [/][blue]$($this.name)[/][red] does not have access to this wallet. Please log in with the fingerprint: [/][blue]$($this.fingerprint)[/]" return $false } if($fp.key.fingerprint -eq $this.fingerprint){ return $true } Write-SpectreHost -Message " [red]Bot [/][blue]$($this.name)[/][red] does not have access to this wallet. Please log in with the fingerprint: [/][blue]$($this.fingerprint)[/]" return $false } [void] activate(){ $this.active = $true $this.save() } [void] deactivate(){ $this.active = $false $this.save() } [void] runNow(){ $this.next_trade_time = Get-Date $this.Handle() } [void] login(){ if(-not $this.isLoggedIn()){ try{ Connect-SageFingerprint -fingerprint ($this.fingerprint) } catch { Write-SpectreHost -Message "[red]Failed to connect to Sage with fingerprint $($this.fingerprint). Please check your Sage configuration.[/]" return } } else { Write-SpectreHost -Message "[green]Already logged in with fingerprint $($this.fingerprint).[/]" } } [bool] hasValidTradeTime(){ $now = Get-Date if($this.next_trade_time -gt $now){ Write-SpectreHost -Message "[yellow]Bot [/][blue]$($this.name)[/][yellow] is not ready to trade yet. Next trade time is $($this.next_trade_time).[/]" return $false } return $true } [void] Save(){ $path = Get-SageTraderPath("DCABots") $file = Join-Path -Path $path -ChildPath "$($this.id).json" if(-not (Test-Path -Path $path)){ New-Item -Path $path -ItemType Directory | Out-Null } $this | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 } [void] summary(){ Write-SpectreHost -Message " This BOT spend $($this.offered_asset.getFormattedAmount()) $($this.offered_asset.name) to buy $($this.requested_asset.name) every $($this.minutes_between_trades) minutes. " if($this.minimum_price -ne 0){ Write-SpectreHost -Message "This BOT will only trade if the price is above [green]$($this.minimum_price)[/]." } if($this.maximum_price -ne 0){ Write-SpectreHost -Message "This BOT will only trade if the price is below [red]$($this.maximum_price)[/]." } } [void] InitialSave(){ $check = Read-Spectreconfirm -Message "Do you want to save this bot?" -DefaultAnswer "y" if($check -eq $true){ $this.Save() Write-SpectreHost -Message "[green]Bot saved successfully.[/]" } else { Write-SpectreHost -Message "[yellow]Bot not saved.[/]" } } [bool] quoteIsValid([DexieQuote]$quote){ if($null-eq $quote){ Write-SpectreHost -Message "[red]Quote is null. Cannot validate.[/]" return $false } if($quote.price -lt $this.minimum_price -and $this.minimum_price -ne 0){ Write-SpectreHost -Message "[red]Quote price is below the minimum price of $($this.minimum_price).[/]" return $false } if($quote.price -gt $this.maximum_price -and $this.maximum_price -ne 0){ Write-SpectreHost -Message "[red]Quote price is above the maximum price of $($this.maximum_price).[/]" return $false } return $true } [array] getLog(){ $path = Get-SageTraderPath("offerlogs") $file = Join-Path -Path $path -ChildPath "$($this.id).csv" if(-not (Test-Path -Path $file)){ Write-SpectreHost -Message "[red]No logs found for this bot.[/]" return @() } $log = Import-Csv -Path $file if($null -eq $log){ Write-SpectreHost -Message "[red]No logs found for this bot.[/]" return @() } if($log.count -eq 0){ Write-SpectreHost -Message "[red]No logs found for this bot.[/]" return @() } return $log } [bool] isActive(){ if($this.active -eq $true){ Write-SpectreHost -Message "[green]Bot [/][blue]$($this.name)[/][green] is active.[/]" return $true } else { Write-SpectreHost -Message "[red]Bot [/][blue]$($this.name)[/][red] is not active.[/]" return $false } } [void] Handle(){ if($this.isLoggedIn() -and $this.hasValidBalance() -and $this.hasValidTradeTime() -and $this.isActive()){ Write-SpectreHost -Message "[green]Retrieving Quote for Bot [/][blue]$($this.name)[/][green][/]" $quote = $this.GetQuote() if($null -eq $quote){ return } if(-not $this.quoteIsValid($quote)){ Write-SpectreHost -Message "[red]Quote is not valid for this bot. Skipping trade.[/]" return } Write-SpectreHost -Message "[gray] Offered: [/][green] $($quote.from.getFormattedAmount()) [/][blue]$($quote.from.name)[/] [gray]Requested: [/][green] $($quote.to.getFormattedAmount()) [/][blue]$($quote.to.name)[/] [gray]Price: [/][green]$($quote.price) [/] " $quote.Build() if($null -eq $quote.sageoffer){ Write-SpectreHost -Message "[red]Failed to build the offer. Please check your assets and try again.[/]" return } # Adding Default Fee $quote.sageoffer.fee = $this.default_fee # Create the offer in sage. $quote.sageoffer.createoffer() write-spectrehost -Message "[green]Offer created successfully.[/]" $dexie = Submit-DexieSwap -offer $quote.sageoffer.offer_data.offer if(-not $null -eq $dexie){ Write-SpectreHost -Message "[green]Offer [/][blue] - $($dexie.id) - [/][green] submitted to Dexie successfully.[/]" $this.current_token_spend += $quote.from.amount $this.last_trade_time = Get-Date $this.next_trade_time = $this.last_trade_time.AddMinutes($this.minutes_between_trades) $this.last_attemted_trade_time = Get-Date $this.save() } $log = [PSCustomObject]@{ offer_id = $quote.sageoffer.offer_data.offer_id bot_type = $this.GetType().Name bot_id = $this.id offered_asset_id = $quote.from.code offered_asset_amount = [decimal]($quote.from.getFormattedAmount() * -1) requested_asset_id = $quote.to.code requested_asset_amount = [decimal]($quote.to.getFormattedAmount()) status = "pending" created_at = (Get-Date) updated_at = (Get-Date) fingerprint = $this.fingerprint dexie_id = ($dexie.id) } $this.logOffer($log) } } [array] showLog(){ $path = Get-SageTraderPath("offerlogs") $file = Join-Path -Path $path -ChildPath "$($this.id).csv" if(-not (Test-Path -Path $file)){ Write-SpectreHost -Message "[red]No logs found for this bot.[/]" return @() } $log = Import-Csv -Path $file if($log.count -eq 0){ Write-SpectreHost -Message "[red]No logs found for this bot.[/]" return @() } return $log } [void] logOffer($log){ $path = Get-SageTraderPath("offerlogs") $file = Join-Path -Path $path -ChildPath "$($this.id).csv" if(-not (Test-Path -Path $path)){ New-Item -Path $path -ItemType Directory | Out-Null } if(-not (Test-Path -Path $file)){ $log | Export-Csv -Path $file -NoTypeInformation } else { $log | Export-Csv -Path $file -NoTypeInformation -Append } } } function Get-SageTraderPath() { [CmdletBinding()] param( [string]$subfolder = $null ) <# .SYNOPSIS Get the path to the SageTrader folder. .DESCRIPTION Returns the path to the SageTrader folder, optionally including a subfolder. .PARAMETER subfolder The subfolder to include in the path. If not specified, returns the main SageTrader folder. .EXAMPLE Get-SageTraderPath -subfolder "DCABots" Returns the path to the DCABots subfolder within the SageTrader folder. #> if($isWindows){ if(-not (Test-Path -Path "$env:LOCALAPPDATA\SageTrader")){ New-Item -Path "$env:LOCALAPPDATA\SageTrader" -ItemType Directory | Out-Null } if($null -eq $subfolder){ return "$env:LOCALAPPDATA\SageTrader" } if(-not (Test-Path -Path "$env:LOCALAPPDATA\SageTrader\$subfolder")){ New-Item -Path "$env:LOCALAPPDATA\SageTrader\$subfolder" -ItemType Directory | Out-Null } return "$env:LOCALAPPDATA\SageTrader\$subfolder" } if($IsLinux){ if(-not (Test-Path -Path "$HOME/.local/share/SageTrader")){ New-Item -Path "$HOME/.local/share/SageTrader" -ItemType Directory | Out-Null } if($null -eq $subfolder){ return "$HOME/.local/share/SageTrader" } if(-not (Test-Path -Path "$HOME/.local/share/SageTrader/$subfolder")){ New-Item -Path "$HOME/.local/share/SageTrader/$subfolder" -ItemType Directory | Out-Null } return "$HOME/.local/share/SageTrader/$subfolder" } if($IsMacOS){ if(-not (Test-Path -Path "$HOME/Library/Application Support/SageTrader")){ New-Item -Path "$HOME/Library/Application Support/SageTrader" -ItemType Directory | Out-Null } if($null -eq $subfolder){ return "$HOME/Library/Application Support/SageTrader" } if(-not (Test-Path -Path "$HOME/Library/Application Support/SageTrader/$subfolder")){ New-Item -Path "$HOME/Library/Application Support/SageTrader/$subfolder" -ItemType Directory | Out-Null } return "$HOME/Library/Application Support/SageTrader/$subfolder" } } function Get-ChiaAsset { <# .SYNOPSIS Get a specific Chia Asset by code or id. .DESCRIPTION Retrieves a specific Chia Asset by its code or id. .PARAMETER id This can be either the code or the id of the asset. .EXAMPLE Get-ChiaAsset -Code "XCH" Retrieves the Chia Asset with the code "XCH". .EXAMPLE Get-ChiaAsset -Id "fa4a180ac326e67ea289b869e3448256f6af05721f7cf934cb9901baa6b7a99d" Retrieves the Chia Asset with the specified id (). .NOTES This function retrieves a Chia Asset from the local assets.json file. #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string]$id ) $assets = Get-ChiaAssets return $assets | Where-Object { $_.id -eq $id -or $_.code -eq $id } } function Sync-ChiaAssets{ $path = Get-SageTraderPath $file = Join-Path -Path $path -ChildPath "assets.json" $page = 1 $assets = Get-DexieAssets -page_size 100 -page $page -cats $tokens = @() $pairs = Invoke-RestMethod -uri "https://api.v2.tibetswap.io/pairs?skip=0&limit=10000" -Method Get $xch = @{} $xch.name = "XCH" $xch.code = "XCH" $xch.id = "xch" $xch.denom = 1000000000000 $assetarray = @() $assetarray += @($xch) while ($tokens.count -lt $assets.count){ foreach ($asset in $assets.assets){ $token= @{} $token.name = $asset.name $token.code = $asset.code $token.id = $asset.id $token.denom = $asset.denom $pair = $pairs | Where-Object { $_.asset_id -eq $asset.id} if($pair){ $token.tibet_pair_id = $pair.launcher_id $token.tibet_liquidity_asset_id = $pair.liquidity_asset_id } $assetarray += @($token) } $page++ $assets = Get-DexieAssets -page_size 100 -page $page -cats } $assetarray | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 } function Get-ChiaAssets { <# .SYNOPSIS Get a list of all Chia Assets. .DESCRIPTION Gets an array of all Chia Assets. .EXAMPLE Get-ChiaAssets Retrieves and displays the list of Chia assets. #> $path = Get-SageTraderPath $file = Join-Path -Path $path -ChildPath "assets.json" if(-not (Test-Path -Path $file)){ Sync-ChiaAssets } $assets = Get-Content -Path $file | ConvertFrom-Json $assetList = @() Foreach ($asset in $assets){$as $asset = [ChiaAsset]::new($asset) $assetList += $asset } return $assetList } function Sync-ChiaSwapAssets { <# .SYNOPSIS Sync Chia Swap Assets. .DESCRIPTION This function syncs the Chia Swap assets by fetching them from the API and saving them to a local file. .EXAMPLE Sync-ChiaSwapAssets Syncs the Chia Swap assets and saves them to a local file. #> $path = Get-SageTraderPath $file = Join-Path -Path $path -ChildPath "swapassets.json" Write-SpectreHost -Message "[green]Syncing Chia Swap Assets...[/]" $uri = 'https://api.dexie.space/v1/swap/tokens' $response = Invoke-RestMethod -Uri $uri -Method Get if ($response -and $response.tokens) { $tokens = $response.tokens $tokens | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 } else { Write-Host "Failed to retrieve Chia Swap assets." } } function Get-ChiaSwapAssets { <# .SYNOPSIS Get a list of all Chia Swap Assets. .DESCRIPTION Gets an array of all Chia Swap Assets. .EXAMPLE Get-ChiaSwapAssets Retrieves and displays the list of Chia Swap assets. #> $path = Get-SageTraderPath $file = Join-Path -Path $path -ChildPath "swapassets.json" if(-not (Test-Path -Path $file)){ Sync-ChiaSwapAssets } $assets = Get-Content -Path $file | ConvertFrom-Json $assetList = @() Foreach ($asset in $assets){ $assetObj = [ChiaAsset]::new($asset) $assetList += $assetObj } return $assetList } function New-ChiaBot { Clear-Host Write-SpectreFigletText -Text "Create a New Chia Bot" -Color green Center Read-SpectreSelection -Message "What type of bot would you like to create?" -Choices @("Dollar Cost Averaging") -EnableSearch | Select-ChiaBotAnswer } function Select-ChiaBotAnswer{ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string]$bot ) switch ($bot) { "Dollar Cost Averaging" { New-ChiaDCABot } "Active Grid Trading" { New-ChiaGridBot } default { Write-Host "Invalid selection. Please choose a valid bot type." } } } function New-ChiaDCABot{ $bot = [ChiaDCABot]::new() $direction_choice = '' Clear-Host Write-SpectreFigletText -Text "New DCA Bot" -Color green Center Write-SpectreHost -Message "Select enter the asset you want to Dollar Cost Average. " $asset = Select-ChiaSwapAsset $direction = Read-SpectreSelection -Message "Do you want to [green]Buy (XCH->$($asset.code))[/] or [red]Sell ($($asset.code)->XCH)[/] this $($asset.name)?" -Choices @("[green]Buy[/]", "[red]Sell[/]") if ($direction -eq "[green]Buy[/]") { $direction_choice = 'buy' $requested_asset = $asset $offered_asset = (Get-ChiaAsset -id "xch") } else { $direction_choice = 'sell' $requested_asset = (Get-ChiaAsset -id "xch") $offered_asset = $asset } $amount = Read-SpectreText -Message "How much $($($offered_asset).name) do you want to spend each time?" -DefaultAnswer "0.1" $offered_asset.setAmount([decimal]$amount) $bot.offered_asset = $offered_asset $bot.requested_asset = $requested_asset $quote = Get-DexieQuote -from $offered_asset.id -to $requested_asset.id -from_amount $offered_asset.amount if ($null -eq $quote) { Write-SpectreHost -Message "[red]Failed to get a quote. Please check your assets and try again.[/]" return } $dexie_quote = [DexieQuote]::new($($quote.quote)) Clear-Host Write-SpectreHost -Message "The current price for [blue]$($offered_asset.name)[/] to [blue]$($requested_asset.name)[/] is [green]$($dexie_quote.price)[/]. " $restrict = Read-SpectreConfirm -Message "Do you want to restrict this bot to a specific price range?" -DefaultAnswer "n" if($restrict -eq $true){ Write-SpectreHost -Message " You are $($direction_choice)ing [blue]$($offered_asset.getFormattedAmount()) $($offered_asset.code)[/] for [blue]$($requested_asset.code)[/]. If buying (receive CAT), you want to set a [red]minimum price[/]. [gray]Example: If the price is 10 wUSDC.b/XCH, you want to sent a minimum of 10.0 so you don't send XCH when you don't receive as much CAT.[/] If selling (sending CAT), you want to set a [green]maximum price[/] [gray]Example: If the price is 10 wUSDC.b/XCH, you want to set a maximum of 10.0 so you don't send more CAT when you don't receive as much XCH.[/] THE CURRENT PRICE IS [green]$($dexie_quote.price)[/]. " if($direction_choice -eq 'buy'){ $bot.minimum_price = Get-MinPrice $bot.maximum_price = 0 } else { $bot.minimum_price = 0 $bot.maximum_price = Get-MaxPrice } } $max_spend = Get-MaxTokenSpend -asset $offered_asset if ($max_spend -gt 0) { $bot.max_token_spend = $max_spend Write-SpectreHost -Message "This bot will not spend more than [purple]$($max_spend / $offered_asset.denom)[/] $($offered_asset.name) in total." } else { $bot.max_token_spend = 0 Write-SpectreHost -Message "This bot will spend [purple]unlimited[/] $($offered_asset.name)." } $bot.minutes_between_trades = Get-MinutesBetweenTrades Clear-Host $bot.summary() $bot.name = Read-SpectreText -Message "What do you want to name this bot?" -DefaultAnswer "My DCA Bot" $bot.default_fee = Get-ChiaDefaultFee $bot.fingerprint = Get-ChiaFingerprint $bot.InitialSave() $act = Read-SpectreConfirm -Message "Do you want to activate this bot now?" -DefaultAnswer "y" if($act -eq $true){ $bot.activate() Write-SpectreHost -Message "[green]Bot [/][blue]$($bot.name)[/] [green]is now active.[/]" } else { Write-SpectreHost -Message "[yellow]Bot [/][blue]$($bot.name)[/] [yellow]is not active. You can activate it later.[/]" } Start-SageTrader } function Get-ChiaFingerprint { $fingerprints = Get-SageKeys $fingerprint = Read-SpectreSelection -Message "Authorize Bot to access specific fingerprint." -Choices ($fingerprints.name) -EnableSearch -SearchHighlightColor purple return ($fingerprints | Where-Object { $_.name -eq $fingerprint }).fingerprint } function Connect-ChiaFingerprint { $fingerprints = Get-SageKeys $fingerprint = Read-SpectreSelection -Message "Select which wallet to log into." -Choices ($fingerprints.name) -EnableSearch -SearchHighlightColor purple $selected_fingerprint = ($fingerprints | Where-Object { $_.name -eq $fingerprint }).fingerprint if ($null -eq $selected_fingerprint) { Write-SpectreHost -Message "[red]No fingerprint selected. Please try again.[/]" return Connect-ChiaFingerprint } try { Connect-SageFingerprint -fingerprint $selected_fingerprint Write-SpectreHost -Message "[green]Successfully connected to fingerprint $selected_fingerprint.[/]" } catch { Write-SpectreHost -Message "[red]Failed to connect to fingerprint $selected_fingerprint. Please check your Sage configuration.[/]" } } function Format-ChiaAssetBalance { $data = @() $xch = Get-SageSyncStatus if ($xch -and $xch.balance) { $xch_balance = [decimal]($xch.balance / 1000000000000) $data += [pscustomobject]@{ Image = "https://icons.dexie.space/xch.webp" Asset = "XCH" Balance = $xch_balance } } $cats = Get-SageCats | Sort-Object -Property balance -Descending if ($cats -and $cats.Count -gt 0) { foreach ($cat in $cats) { if($cat.balance -gt 0) { $balance = [decimal]($cat.balance / 1000) $data += [pscustomobject]@{ Image = ($cat.icon_url) Asset = $cat.ticker Balance = $balance } } } } return $data } function Get-ChiaDefaultFee{ $asset = Get-ChiaAsset -id "xch" $fee = Read-SpectreText -Message "What is the default fee for this bot? (0 for no fee)" -DefaultAnswer "0" if ($fee -match '^\d+(\.\d{1,12})?$') { return [UInt64]$fee * $asset.denom } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-ChiaDefaultFee } } function Get-MinutesBetweenTrades { $minutes = Read-SpectreText -Message "How many [blue]minutes[/] between trades?" if ($minutes -match '^\d+$') { return [int]$minutes } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-MinutesBetweenTrades } } function Get-MaxTokenSpend { param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ChiaAsset]$asset ) $max_spend = Read-SpectreText -Message "What is the maximum [blue]$($asset.code)[/] this bot can spend in total? (0 for no limit)" -DefaultAnswer "0" if ($max_spend -match '^\d+(\.\d{1,12})?$') { return ([UInt64]$max_spend * $asset.denom) } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-MaxTokenSpend } } function Get-MinPrice { $min_price = Read-SpectreText -Message "What is the [red]minimum price[/] you are willing to accept for this trade?" -AllowEmpty if ($min_price -eq '') { return 0 } if ($min_price -match '^\d+(\.\d{1,3})?$') { return [decimal]$min_price } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-MinPrice } } function Get-MaxPrice { $max_price = Read-SpectreText -Message "What is the [green]maximum price[/] you are willing to pay for this trade?" -AllowEmpty if ($max_price -eq '') { return 0 } if ($max_price -match '^\d+(\.\d{1,3})?$') { return [decimal]$max_price } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-MaxPrice } } function Get-XCHInput { $xch = Read-SpectreText -Message "How much [purple]XCH[/] do you want to spend per trade?" -DefaultAnswer "0.1" if ($xch -match '^\d+(\.\d{1,12})?$') { return [decimal]$xch } else { Write-SpectreHost -Message "[red]Invalid input. Please enter a valid number.[/]" return Get-XCHInput } } function Select-ChiaSwapAsset { $assets = Get-ChiaSwapAssets $result = Read-SpectreSelection -Message "Select a [purple]Chia Asset[/]" -Choices ($assets.code ) -EnableSearch -SearchHighlightColor purple $asset = Get-ChiaAsset -id $result return $asset } function Get-ChiaBots { $bots = @() $dcabots = Get-ChiaDCABots if($null -eq $dcabots){ Write-SpectreHost -Message "[red]No DCA Bots found.[/]" return } foreach($bot in $dcabots) { $bots += $bot } return $bots } function Get-ChiaDCABots { $bots = @() $path = Get-SageTraderPath("DCABots") if(-not (Test-Path -Path $path)){ Write-SpectreHost -Message "[red]No bots found.[/]" return } $files = Get-ChildItem -Path $path -Filter "*.json" if($files.Count -eq 0){ Write-SpectreHost -Message "[red]No bots found.[/]" return } foreach ($file in $files) { $bot = Get-Content -Path $file.FullName | ConvertFrom-Json $bots += [ChiaDCABot]::new($bot) } return $bots } function Get-ChiaOfferLog { <# .SYNOPSIS Get the Chia Offer Log. .DESCRIPTION Retrieves the Chia Offer Log from the local file. .EXAMPLE Get-ChiaOfferLog Retrieves and displays the Chia Offer Log. #> $path = Get-SageTraderPath("offerlogs") $file = Join-Path -Path $path -ChildPath "offers.csv" if(-not (Test-Path -Path $file)){ Write-SpectreHost -Message "[red]No offer logs found.[/]" return @() } return Import-Csv -Path $file } function Update-ChiaOfferLog { param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [array]$logs ) } function New-ChiaOfferLog{ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string]$bot_type, [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] [string]$bot_id, [Parameter(Mandatory = $true, Position = 2, ValueFromPipeline = $true)] [string]$offered_asset_id, [Parameter(Mandatory = $true, Position = 3, ValueFromPipeline = $true)] [Int128]$offered_asset_amount, [Parameter(Mandatory = $true, Position = 4, ValueFromPipeline = $true)] [string]$requested_asset_id, [Parameter(Mandatory = $true, Position = 5, ValueFromPipeline = $true)] [Int128]$requested_asset_amount, [Parameter(Mandatory = $true, Position = 6, ValueFromPipeline = $true)] [string]$status, [Parameter(Mandatory = $true, Position = 7, ValueFromPipeline = $true)] [datetime]$created_at, [Parameter(Mandatory = $true, Position = 8, ValueFromPipeline = $true)] [datetime]$updated_at, [Parameter(Mandatory = $true, Position = 9, ValueFromPipeline = $true)] [string]$offer_id, [Parameter(Mandatory = $true, Position = 10, ValueFromPipeline = $true)] [string]$fingerprint, [Parameter(Mandatory = $true, Position = 11, ValueFromPipeline = $true)] [string]$dexie_id ) $log = [PSCustomObject]@{ bot_type = $bot_type bot_id = $bot_id offered_asset_id = $offered_asset_id offered_asset_amount = $offered_asset_amount requested_asset_id = $requested_asset_id requested_asset_amount = $requested_asset_amount status = $status created_at = $created_at updated_at = $updated_at offer_id = $offer_id fingerprint = $fingerprint dexie_id = $dexie_id } $path = Get-SageTraderPath("offerlogs") $file = Join-Path -Path $path -ChildPath "offers.csv" if(-not (Test-Path -Path $file)){ $log | Export-Csv -Path $file -NoTypeInformation } else { $log | Export-Csv -Path $file -NoTypeInformation -Append } } function Start-Bots { while($true){ Write-SpectreHost -Message "[purple] $(Get-Date) [/]" Write-SpectreRule -Color purple $bots = Get-ChiaBots if($null -eq $bots){ Write-SpectreHost -Message "[red]No bots found.[/]" return } foreach ($bot in $bots) { Write-Information "Starting bot: $($bot.name)" $bot.Handle() } Write-SpectreHost -Message "[green]All bots have been processed. Waiting for the next cycle...[/]" Write-SpectreRule -Color purple Start-Sleep -Seconds 60 # Wait for 60 seconds before the next cycle } } function Show-PanelMainMenu{ param ( $Item, $SelectedItem ) $itemList = $Item | ForEach-Object { $name = $_.Name if ($_.Name -eq $SelectedItem.Name) { $name = "[green]$($name)[/]" } return $name } | Out-String return Format-SpectrePanel -Header "[white]Main Menu[/]" -Data $itemList.Trim() -Expand -Color darkseagreen } function Get-PanelMainMenuItems{ return @( [PSCustomObject]@{ Name = "Create Chia Bot" Description ="Create a new trading bot for Chia." Action = { New-ChiaBot } }, [PSCustomObject]@{ Name = "Show Bots" Description = "Show all existing Chia bots." Action = { Show-AppMenu -Item (Get-PanelBotMenuItems) -title "Chia Bots" } }, [PSCustomObject]@{ Name = "Run All Bots" Description = "Start up all the bots. They will start to actively trade as long as Sage is running and logged in with the correct fingerprint." Action = { Start-Bots } } [PSCustomObject]@{ Name = "Exit" Description = "Exit the Sage Trader application." Action = { return } } ) } function Start-SageTrader { Show-AppMenu -Item (Get-PanelMainMenuItems) -title "Sage-Trader" } function Get-PanelBotMenuItems { $bots = Get-ChiaBots if ($null -eq $bots) { return @( [PSCustomObject]@{ Name = "Main Menu" Description = "No Chia bots found. Please create a bot first." Action = { Start-SageTrader } } ) } $list = @() $mainmenu = [PSCustomObject]@{ Name = "Main Menu" Description = "Return to the main menu." Action = { Start-SageTrader } } $list += $mainmenu foreach ($bot in $bots) { $name = $bot.name $boj = [PSCustomObject]@{ Name = $name Description = " Bot ID: $($bot.id) Bot Name: [blue]$($bot.name)[/] Fingerprint: $($bot.fingerprint) Offered Asset: $($bot.offered_asset.code) Requested Asset: $($bot.requested_asset.code) Type: $($bot.GetType().Name) Status: $($bot.active ? '[green]Active[/]' : '[red]Inactive[/]') Last Trade Time: $($bot.last_trade_time) Next Trade Time: $($bot.next_trade_time) " } $boj | Add-Member -MemberType ScriptProperty -Name "Action" -Value { Show-BotMenu -name $this.Name } $list += $boj } return $list } function Get-PanelBotDetails{ param($bot) $items = @( [pscustomobject]@{ Name = ".. Go Back" Description = "Return to the bot list." Action = { Show-AppMenu -Item (Get-PanelBotMenuItems) -title "Chia Bots" } }, [pscustomobject]@{ Name = "Bot Details" Description = " Bot ID: $($bot.id) Bot Name: [blue]$($bot.name)[/] Fingerprint: $($bot.fingerprint) Offered Asset: $($bot.offered_asset.code) Requested Asset: $($bot.requested_asset.code) Type: $($bot.GetType().Name) Status: $($bot.active ? '[green]Active[/]' : '[red]Inactive[/]') Last Trade Time: $($bot.last_trade_time) Next Trade Time: $($bot.next_trade_time) " Id = $bot.id } ) $activate = [PSCustomObject]@{ Name = $($bot.active ? "Deactivate Bot" : "Activate Bot") Description = $($bot.active ? "Deactivate this bot." : "Activate this bot.") Id = $bot.id } $activate | Add-Member -MemberType ScriptProperty -Name "Action" -Value { Show-BotMenu -name $this.Name } $items += $activate return $items } function Get-ChiaBot{ param($name) $bots = Get-ChiaBots if ($null -eq $bots) { Write-SpectreHost -Message "[red]No Chia bots found.[/]" return $null } $bot = $bots | Where-Object { $_.name -eq $name } if ($null -eq $bot) { Write-SpectreHost -Message "[red]Bot with name '$name' not found.[/]" return $null } return $bot } function Show-AppMenu{ param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [array]$Item, [Parameter(Mandatory = $true, Position = 2, ValueFromPipeline = $true)] [string]$title ) $layout = New-SpectreLayout -Name "root" -Rows @( # Row 1 ( New-SpectreLayout -Name "header" -MinimumSize 9 -Ratio 1 -Data ("empty") ), # Row 2 ( New-SpectreLayout -Name "content" -Ratio 10 -Columns @( ( New-SpectreLayout -Name "menu" -Ratio 2 -Data "empty" ), ( New-SpectreLayout -Name "preview" -Ratio 4 -Data "empty" ) ) ) # Row 3 ) $titlePanel = Write-SpectreFigletText -Text $title -Color DarkSeaGreen -Alignment Center -PassThru | Format-SpectrePanel -Expand -Height 9 -Color darkseagreen function Get-PreviewPanel { param ( $SelectedItem ) $result = $SelectedItem.Description return $result | Format-SpectrePanel -Header "[white]Description[/]" -Expand -Color darkseagreen } function Get-LastKeyPressed { $lastKeyPressed = $null while ([Console]::KeyAvailable) { $lastKeyPressed = [Console]::ReadKey($true) } return $lastKeyPressed } $response = Invoke-SpectreLive -Data $layout -ScriptBlock { param ( [Spectre.Console.LiveDisplayContext] $Context ) # State $itemList = $Item $selectedItem = $itemList[0] while ($true) { # Handle input $lastKeyPressed = Get-LastKeyPressed if ($null -ne $lastKeyPressed ) { if ($lastKeyPressed.Key -eq "DownArrow") { $selectedItem = $itemList[($itemList.IndexOf($selectedItem) + 1) % $itemList.Count] } elseif ($lastKeyPressed.Key -eq "UpArrow") { $selectedItem = $itemList[($itemList.IndexOf($selectedItem) - 1 + $itemList.Count) % $itemList.Count] } elseif ($lastKeyPressed.Key -eq "Enter") { # Handle the selection return $selectedItem } elseif ($lastKeyPressed.Key -eq "Escape") { return } } # Generate new data $menu = Show-PanelMainMenu -Item $itemList -SelectedItem $selectedItem $previewPanel = Get-PreviewPanel -SelectedItem $selectedItem # Update layout $layout["header"].Update($titlePanel) | Out-Null $layout["menu"].Update($menu) | Out-Null $layout["preview"].Update($previewPanel) | Out-Null # Draw changes $Context.Refresh() Start-Sleep -Milliseconds 200 } } if ($null -ne $response) { & $response.Action } } function Show-BotMenu{ param( [Parameter(Mandatory = $true, Position = 2, ValueFromPipeline = $true)] [string]$name ) $bot = Get-ChiaBot -name $name $title = $name $Item = @( [PSCustomObject]@{ Name = "Main Menu" Description = "Return to the main menu." }, [PSCustomObject]@{ Name = "Bot Details" Description = " Id: $($bot.id) Name: $($bot.name) Fingerprint: $($bot.fingerprint) Offered Asset: $($bot.offered_asset.code) Requested Asset: $($bot.requested_asset.code) Type: $($bot.GetType().Name) Status: $($bot.active ? '[green]Active[/]' : '[red]Inactive[/]') Last Trade Time: $($bot.last_trade_time) Next Trade Time: $($bot.next_trade_time) Max Spend: $($bot.max_token_spend / $bot.offered_asset.denom) $($bot.offered_asset.code) " }, [PSCustomObject]@{ Name = $($bot.active ? "Deactivate" : "Activate") Description = $($bot.active ? "Deactivate this bot." : "Activate this bot.") }, [PSCustomObject]@{ Name = "Delete Bot" Description = "Delete this bot." }, [PSCustomObject]@{ Name = "Show Trades" Description = "View the offer log for this bot." } ) $layout = New-SpectreLayout -Name "root" -Rows @( # Row 1 ( New-SpectreLayout -Name "header" -MinimumSize 9 -Ratio 1 -Data ("empty") ), # Row 2 ( New-SpectreLayout -Name "content" -Ratio 10 -Columns @( ( New-SpectreLayout -Name "menu" -Ratio 2 -Data "empty" ), ( New-SpectreLayout -Name "preview" -Ratio 4 -Data "empty" ) ) ) # Row 3 ) $titlePanel = Write-SpectreFigletText -Text $title -Color DarkSeaGreen -Alignment Center -PassThru | Format-SpectrePanel -Expand -Height 9 -Color darkseagreen function Get-PreviewPanel { param ( $SelectedItem ) $result = $SelectedItem.Description return $result | Format-SpectrePanel -Header "[white]Description[/]" -Expand -Color darkseagreen } function Get-LastKeyPressed { $lastKeyPressed = $null while ([Console]::KeyAvailable) { $lastKeyPressed = [Console]::ReadKey($true) } return $lastKeyPressed } $response = Invoke-SpectreLive -Data $layout -ScriptBlock { param ( [Spectre.Console.LiveDisplayContext] $Context ) # State $itemList = $Item $selectedItem = $itemList[0] while ($true) { # Handle input $lastKeyPressed = Get-LastKeyPressed if ($null -ne $lastKeyPressed ) { if ($lastKeyPressed.Key -eq "DownArrow") { $selectedItem = $itemList[($itemList.IndexOf($selectedItem) + 1) % $itemList.Count] } elseif ($lastKeyPressed.Key -eq "UpArrow") { $selectedItem = $itemList[($itemList.IndexOf($selectedItem) - 1 + $itemList.Count) % $itemList.Count] } elseif ($lastKeyPressed.Key -eq "Enter") { switch($selectedItem.Name) { "Main Menu" { return {Start-SageTrader} } "Activate" { Get-ChiaBot -name $title | ForEach-Object { $_.activate() } $selectedItem.Name = "Deactivate" $selectedItem.Description = "Deactivate this bot." } "Deactivate" { Get-ChiaBot -name $title | ForEach-Object { $_.deactivate() } $selectedItem.Name = "Activate" $selectedItem.Description = "Activate this bot." } "Delete Bot" { Get-ChiaBot -name $title | ForEach-Object { $_.destroy() } return {Start-SageTrader} } "Show Trades" { $bot = Get-ChiaBot -name $title $trades = $bot.showLog() $selectedItem.Description = $trades | Select-Object -Property offered_asset_id,offered_asset_amount,requested_asset_id,requested_asset_amount | Format-SpectreTable -Expand } } } elseif ($lastKeyPressed.Key -eq "Escape") { return } } # Generate new data $menu = Show-PanelMainMenu -Item $itemList -SelectedItem $selectedItem $previewPanel = Get-PreviewPanel -SelectedItem $selectedItem # Update layout $layout["header"].Update($titlePanel) | Out-Null $layout["menu"].Update($menu) | Out-Null $layout["preview"].Update($previewPanel) | Out-Null # Draw changes $Context.Refresh() Start-Sleep -Milliseconds 200 } } if ($null -ne $response) { & $response } } Export-ModuleMember -Function * |