Update mit Trading-Modul
release-tag / release-image (push) Successful in 2m6s

This commit is contained in:
2026-05-30 12:05:35 +02:00
parent d884331c61
commit 9b02905000
7 changed files with 746 additions and 22 deletions
+205
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
@@ -102,6 +103,9 @@ CREATE INDEX IF NOT EXISTS idx_order_events_order ON order_events(order_id);
if err != nil {
return err
}
if err := s.createTradingSchema(); err != nil {
return err
}
if _, err := s.db.Exec(`INSERT OR IGNORE INTO storage_locations(name, normalized_name, created_by_id, created_by_name, created_at, updated_at)
SELECT DISTINCT TRIM(location), lower(TRIM(location)), 'migration', 'migration', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM inventory
@@ -608,3 +612,204 @@ func formatAmount(v float64) string {
}
return fmt.Sprintf("%.2f", v)
}
func (s *Store) createTradingSchema() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS trading_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
user_name TEXT NOT NULL,
purpose TEXT NOT NULL,
ship TEXT NOT NULL,
buy_value INTEGER NOT NULL DEFAULT 0,
sell_value INTEGER NOT NULL DEFAULT 0,
costs INTEGER NOT NULL DEFAULT 0,
repetitions INTEGER NOT NULL DEFAULT 1,
scu REAL NOT NULL DEFAULT 0,
distance REAL NOT NULL DEFAULT 0,
scu_grade TEXT NOT NULL DEFAULT '',
commodity TEXT NOT NULL,
post_a TEXT NOT NULL DEFAULT '',
post_b TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
month INTEGER NOT NULL CHECK(month >= 1 AND month <= 12),
year INTEGER NOT NULL CHECK(year >= 2000 AND year <= 2200),
org_percent REAL NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME,
deleted_by_id TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_trading_month_year ON trading_runs(year, month);
CREATE INDEX IF NOT EXISTS idx_trading_user ON trading_runs(user_id);
CREATE INDEX IF NOT EXISTS idx_trading_deleted ON trading_runs(deleted_at);
`)
return err
}
func (s *Store) CreateTradingRun(t *TradingRun) (int64, error) {
now := time.Now().UTC()
if t.Month < 1 || t.Month > 12 {
return 0, errors.New("monat muss zwischen 1 und 12 liegen")
}
if t.Year < 2000 || t.Year > 2200 {
return 0, errors.New("jahr muss zwischen 2000 und 2200 liegen")
}
if t.Repetitions <= 0 {
t.Repetitions = 1
}
res, err := s.db.Exec(`INSERT INTO trading_runs
(user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
t.UserID, t.UserName, t.Purpose, t.Ship, t.BuyValue, t.SellValue, t.Costs, t.Repetitions, t.SCU, t.Distance, t.SCUGrade, t.Commodity, t.PostA, t.PostB, t.Note, t.Month, t.Year, t.OrgPercent, now, now)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func scanTradingRun(scanner interface{ Scan(dest ...any) error }) (*TradingRun, error) {
t := &TradingRun{}
if err := scanner.Scan(&t.ID, &t.UserID, &t.UserName, &t.Purpose, &t.Ship, &t.BuyValue, &t.SellValue, &t.Costs, &t.Repetitions, &t.SCU, &t.Distance, &t.SCUGrade, &t.Commodity, &t.PostA, &t.PostB, &t.Note, &t.Month, &t.Year, &t.OrgPercent, &t.CreatedAt, &t.UpdatedAt, &t.DeletedAt, &t.DeletedByID); err != nil {
return nil, err
}
return t, nil
}
func (s *Store) GetTradingRun(id int64) (*TradingRun, error) {
row := s.db.QueryRow(`SELECT id, user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at, deleted_at, deleted_by_id FROM trading_runs WHERE id=?`, id)
return scanTradingRun(row)
}
func (s *Store) DeleteTradingRun(id int64, actorID string) (*TradingRun, error) {
t, err := s.GetTradingRun(id)
if err != nil {
return nil, err
}
if t.DeletedAt.Valid {
return nil, errors.New("trading-run ist bereits gelöscht")
}
_, err = s.db.Exec(`UPDATE trading_runs SET deleted_at=?, deleted_by_id=?, updated_at=? WHERE id=? AND deleted_at IS NULL`, time.Now().UTC(), actorID, time.Now().UTC(), id)
if err != nil {
return nil, err
}
return s.GetTradingRun(id)
}
func (s *Store) ListTradingRuns(f TradingListFilter) ([]TradingRun, error) {
if f.Limit <= 0 {
f.Limit = 20
}
if f.Limit > 5000 {
f.Limit = 5000
}
q := `SELECT id, user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at, deleted_at, deleted_by_id FROM trading_runs WHERE deleted_at IS NULL`
args := []any{}
if f.Month > 0 {
q += ` AND month=?`
args = append(args, f.Month)
}
if f.Year > 0 {
q += ` AND year=?`
args = append(args, f.Year)
}
if strings.TrimSpace(f.UserID) != "" {
q += ` AND user_id=?`
args = append(args, f.UserID)
}
q += ` ORDER BY year DESC, month DESC, created_at DESC LIMIT ?`
args = append(args, f.Limit)
rows, err := s.db.Query(q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TradingRun
for rows.Next() {
t, err := scanTradingRun(rows)
if err != nil {
return nil, err
}
out = append(out, *t)
}
return out, rows.Err()
}
func (s *Store) SummarizeTradingRuns(month, year int) (*TradingSummary, error) {
runs, err := s.ListTradingRuns(TradingListFilter{Month: month, Year: year, Limit: 100000})
if err != nil {
return nil, err
}
sum := &TradingSummary{Month: month, Year: year, Count: len(runs)}
traderMap := map[string]*TradingTraderSummary{}
purposeMap := map[string]*TradingPurposeSummary{}
for _, r := range runs {
profit := tradingProfit(r)
share := tradingOrgShare(r)
scu := r.SCU * float64(r.Repetitions)
costs := r.Costs * r.Repetitions
distance := r.Distance * float64(r.Repetitions)
sum.GrossProfit += profit
sum.OrgShare += share
sum.TotalSCU += scu
sum.TotalCosts += costs
sum.TotalDistance += distance
key := r.UserID
if key == "" {
key = r.UserName
}
tr := traderMap[key]
if tr == nil {
tr = &TradingTraderSummary{UserID: r.UserID, UserName: r.UserName}
traderMap[key] = tr
}
tr.Profit += profit
tr.OrgShare += share
tr.SCU += scu
tr.Runs++
ps := purposeMap[r.Purpose]
if ps == nil {
ps = &TradingPurposeSummary{Purpose: r.Purpose}
purposeMap[r.Purpose] = ps
}
ps.Profit += profit
ps.OrgShare += share
ps.SCU += scu
ps.Runs++
}
for _, tr := range traderMap {
sum.TopTraders = append(sum.TopTraders, *tr)
}
for _, ps := range purposeMap {
sum.PurposeSummary = append(sum.PurposeSummary, *ps)
}
sortTradingSummary(sum)
return sum, nil
}
func tradingProfit(r TradingRun) int64 {
return (r.SellValue - r.BuyValue - r.Costs) * r.Repetitions
}
func tradingOrgShare(r TradingRun) int64 {
profit := tradingProfit(r)
if profit <= 0 || r.OrgPercent <= 0 {
return 0
}
return int64(math.Round(float64(profit) * r.OrgPercent))
}
func sortTradingSummary(sum *TradingSummary) {
sort.Slice(sum.TopTraders, func(i, j int) bool {
if sum.TopTraders[i].Profit == sum.TopTraders[j].Profit {
return sum.TopTraders[i].UserName < sum.TopTraders[j].UserName
}
return sum.TopTraders[i].Profit > sum.TopTraders[j].Profit
})
if len(sum.TopTraders) > 3 {
sum.TopTraders = sum.TopTraders[:3]
}
sort.Slice(sum.PurposeSummary, func(i, j int) bool { return sum.PurposeSummary[i].Purpose < sum.PurposeSummary[j].Purpose })
}