first commit of restructured project
This commit is contained in:
247
trahn-trade-backend/internal/strategy/grid.go
Normal file
247
trahn-trade-backend/internal/strategy/grid.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GridLevel struct {
|
||||
Index int `json:"index"`
|
||||
Price float64 `json:"price"`
|
||||
Side string `json:"side"` // "buy" or "sell"
|
||||
Quantity float64 `json:"quantity"`
|
||||
Filled bool `json:"filled"`
|
||||
FilledAt *time.Time `json:"filledAt,omitempty"`
|
||||
TxHash *string `json:"txHash,omitempty"`
|
||||
}
|
||||
|
||||
type GridStats struct {
|
||||
Levels int `json:"levels"`
|
||||
LowestPrice *float64 `json:"lowestPrice"`
|
||||
HighestPrice *float64 `json:"highestPrice"`
|
||||
FilledLevels int `json:"filledLevels"`
|
||||
PendingBuys int `json:"pendingBuys"`
|
||||
PendingSells int `json:"pendingSells"`
|
||||
FilledBuys int `json:"filledBuys"`
|
||||
FilledSells int `json:"filledSells"`
|
||||
}
|
||||
|
||||
type FallbackSR struct {
|
||||
Support float64 `json:"support"`
|
||||
Resistance float64 `json:"resistance"`
|
||||
Midpoint float64 `json:"midpoint"`
|
||||
Method string `json:"method"`
|
||||
LookbackDays int `json:"lookbackDays"`
|
||||
}
|
||||
|
||||
func CalculateMidpoint(support, resistance float64) (float64, error) {
|
||||
if support >= resistance {
|
||||
return 0, fmt.Errorf("invalid S/R: support (%.2f) >= resistance (%.2f)", support, resistance)
|
||||
}
|
||||
return (support + resistance) / 2, nil
|
||||
}
|
||||
|
||||
type GridParams struct {
|
||||
CenterPrice float64
|
||||
LevelCount int
|
||||
SpacingPercent float64
|
||||
AmountPerGrid float64
|
||||
}
|
||||
|
||||
func CalculateGridLevels(p GridParams) ([]GridLevel, error) {
|
||||
if p.CenterPrice <= 0 {
|
||||
return nil, fmt.Errorf("center price must be positive")
|
||||
}
|
||||
if p.LevelCount < 2 {
|
||||
return nil, fmt.Errorf("level count must be at least 2")
|
||||
}
|
||||
if p.SpacingPercent <= 0 {
|
||||
return nil, fmt.Errorf("spacing percent must be positive")
|
||||
}
|
||||
if p.AmountPerGrid <= 0 {
|
||||
return nil, fmt.Errorf("amount per grid must be positive")
|
||||
}
|
||||
|
||||
halfLevels := p.LevelCount / 2
|
||||
even := p.LevelCount%2 == 0
|
||||
|
||||
var grid []GridLevel
|
||||
|
||||
for i := -halfLevels; i <= halfLevels; i++ {
|
||||
if i == 0 && even {
|
||||
continue
|
||||
}
|
||||
|
||||
multiplier := math.Pow(1+p.SpacingPercent/100, float64(i))
|
||||
levelPrice := p.CenterPrice * multiplier
|
||||
|
||||
side := "sell"
|
||||
if i < 0 {
|
||||
side = "buy"
|
||||
}
|
||||
|
||||
quantity := p.AmountPerGrid / levelPrice
|
||||
|
||||
grid = append(grid, GridLevel{
|
||||
Price: levelPrice,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(grid, func(i, j int) bool {
|
||||
return grid[i].Price < grid[j].Price
|
||||
})
|
||||
|
||||
for i := range grid {
|
||||
grid[i].Index = i
|
||||
}
|
||||
|
||||
return grid, nil
|
||||
}
|
||||
|
||||
func FindTriggeredLevel(currentPrice float64, grid []GridLevel) *GridLevel {
|
||||
for i := range grid {
|
||||
if grid[i].Filled {
|
||||
continue
|
||||
}
|
||||
if grid[i].Side == "buy" && currentPrice <= grid[i].Price {
|
||||
return &grid[i]
|
||||
}
|
||||
if grid[i].Side == "sell" && currentPrice >= grid[i].Price {
|
||||
return &grid[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetOppositeLevelIndex(filledLevel *GridLevel, gridLength int) *int {
|
||||
var idx int
|
||||
if filledLevel.Side == "buy" {
|
||||
idx = filledLevel.Index + 1
|
||||
} else {
|
||||
idx = filledLevel.Index - 1
|
||||
}
|
||||
if idx >= 0 && idx < gridLength {
|
||||
return &idx
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetGridStats(grid []GridLevel) GridStats {
|
||||
if len(grid) == 0 {
|
||||
return GridStats{}
|
||||
}
|
||||
|
||||
s := GridStats{Levels: len(grid)}
|
||||
lo := grid[0].Price
|
||||
hi := grid[len(grid)-1].Price
|
||||
s.LowestPrice = &lo
|
||||
s.HighestPrice = &hi
|
||||
|
||||
for _, l := range grid {
|
||||
switch {
|
||||
case l.Side == "buy" && l.Filled:
|
||||
s.FilledBuys++
|
||||
s.FilledLevels++
|
||||
case l.Side == "buy" && !l.Filled:
|
||||
s.PendingBuys++
|
||||
case l.Side == "sell" && l.Filled:
|
||||
s.FilledSells++
|
||||
s.FilledLevels++
|
||||
case l.Side == "sell" && !l.Filled:
|
||||
s.PendingSells++
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func FormatGridDisplay(grid []GridLevel, centerPrice, amountPerGrid float64) string {
|
||||
if len(grid) == 0 {
|
||||
return "No grid levels initialized."
|
||||
}
|
||||
|
||||
sorted := make([]GridLevel, len(grid))
|
||||
copy(sorted, grid)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Price > sorted[j].Price
|
||||
})
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("┌─────────────────────────────────────────────────┐\n")
|
||||
b.WriteString("│ GRID LEVELS (USD) │\n")
|
||||
b.WriteString("├─────────────────────────────────────────────────┤\n")
|
||||
|
||||
for _, level := range sorted {
|
||||
sideIcon := "BUY "
|
||||
if level.Side == "sell" {
|
||||
sideIcon = "SELL"
|
||||
}
|
||||
status := "[ ]"
|
||||
if level.Filled {
|
||||
status = "[X]"
|
||||
}
|
||||
fmt.Fprintf(&b, "│ %s %s @ %10.2f │ %15s │\n",
|
||||
status, sideIcon, level.Price,
|
||||
fmt.Sprintf("%.6f ETH", level.Quantity))
|
||||
}
|
||||
|
||||
b.WriteString("├─────────────────────────────────────────────────┤\n")
|
||||
fmt.Fprintf(&b, "│ Center: $%8.2f │ $%.0f/level │\n", centerPrice, amountPerGrid)
|
||||
b.WriteString("└─────────────────────────────────────────────────┘")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func CreateFallbackSR(currentPrice float64) FallbackSR {
|
||||
return FallbackSR{
|
||||
Support: currentPrice * 0.9,
|
||||
Resistance: currentPrice * 1.1,
|
||||
Midpoint: currentPrice,
|
||||
Method: "fallback",
|
||||
}
|
||||
}
|
||||
|
||||
func IsPriceOutsideGrid(currentPrice float64, grid []GridLevel) bool {
|
||||
if len(grid) == 0 {
|
||||
return true
|
||||
}
|
||||
lo := grid[0].Price
|
||||
hi := grid[0].Price
|
||||
for _, l := range grid[1:] {
|
||||
if l.Price < lo {
|
||||
lo = l.Price
|
||||
}
|
||||
if l.Price > hi {
|
||||
hi = l.Price
|
||||
}
|
||||
}
|
||||
return currentPrice < lo || currentPrice > hi
|
||||
}
|
||||
|
||||
func AreAllSideFilled(grid []GridLevel, side string) bool {
|
||||
count := 0
|
||||
filled := 0
|
||||
for _, l := range grid {
|
||||
if l.Side == side {
|
||||
count++
|
||||
if l.Filled {
|
||||
filled++
|
||||
}
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return false
|
||||
}
|
||||
return filled == count
|
||||
}
|
||||
|
||||
func CalculateSRChange(newMidpoint, oldMidpoint float64) float64 {
|
||||
if oldMidpoint == 0 {
|
||||
return 100
|
||||
}
|
||||
return math.Abs((newMidpoint - oldMidpoint) / oldMidpoint * 100)
|
||||
}
|
||||
Reference in New Issue
Block a user