@@ -1,10 +1,11 @@
|
|||||||
module starauftrag-bot
|
module starauftrag-bot
|
||||||
|
|
||||||
go 1.22
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.28.1
|
github.com/bwmarrin/discordgo v0.28.1
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
golang.org/x/image v0.41.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||||
|
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/png"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/basicfont"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chartBG = color.RGBA{0x10, 0x17, 0x2a, 0xff}
|
||||||
|
chartPanel = color.RGBA{0x11, 0x18, 0x27, 0xff}
|
||||||
|
chartText = color.RGBA{0xe5, 0xe7, 0xeb, 0xff}
|
||||||
|
chartMuted = color.RGBA{0x94, 0xa3, 0xb8, 0xff}
|
||||||
|
chartGrid = color.RGBA{0x33, 0x41, 0x55, 0xff}
|
||||||
|
chartCurrent = color.RGBA{0x38, 0xbd, 0xf8, 0xff}
|
||||||
|
chartPrevious = color.RGBA{0xf5, 0x9e, 0x0b, 0xff}
|
||||||
|
purposeColors = []color.RGBA{
|
||||||
|
{0x38, 0xbd, 0xf8, 0xff}, {0x22, 0xc5, 0x5e, 0xff}, {0xf5, 0x9e, 0x0b, 0xff}, {0xef, 0x44, 0x44, 0xff},
|
||||||
|
{0xa7, 0x8b, 0xfa, 0xff}, {0xf4, 0x72, 0xb6, 0xff},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type chartMetric struct {
|
||||||
|
Label string
|
||||||
|
Current float64
|
||||||
|
Previous float64
|
||||||
|
Format func(float64) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type piePart struct {
|
||||||
|
Label string
|
||||||
|
Value float64
|
||||||
|
Color color.RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTradingCharts(cur, prev *TradingSummary) (comparisonPNG []byte, purposePNG []byte, err error) {
|
||||||
|
comparisonPNG, err = buildTradingComparisonChart(cur, prev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
purposePNG, err = buildTradingPurposePie(cur)
|
||||||
|
if err != nil {
|
||||||
|
return comparisonPNG, nil, err
|
||||||
|
}
|
||||||
|
return comparisonPNG, purposePNG, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTradingComparisonChart(cur, prev *TradingSummary) ([]byte, error) {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 1100, 700))
|
||||||
|
draw.Draw(img, img.Bounds(), &image.Uniform{chartBG}, image.Point{}, draw.Src)
|
||||||
|
fillRect(img, image.Rect(24, 24, 1076, 676), chartPanel)
|
||||||
|
text(img, 48, 62, "Trading-Vergleich zum Vormonat", chartText, true)
|
||||||
|
text(img, 48, 88, fmt.Sprintf("%02d/%d vs. %02d/%d", cur.Month, cur.Year, prev.Month, prev.Year), chartMuted, false)
|
||||||
|
legendX := 760
|
||||||
|
fillRect(img, image.Rect(legendX, 46, legendX+18, 64), chartCurrent)
|
||||||
|
text(img, legendX+26, 60, fmt.Sprintf("Aktuell %02d/%d", cur.Month, cur.Year), chartText, false)
|
||||||
|
fillRect(img, image.Rect(legendX, 72, legendX+18, 90), chartPrevious)
|
||||||
|
text(img, legendX+26, 86, fmt.Sprintf("Vormonat %02d/%d", prev.Month, prev.Year), chartText, false)
|
||||||
|
|
||||||
|
metrics := []chartMetric{
|
||||||
|
{Label: "Gewinn", Current: float64(cur.GrossProfit), Previous: float64(prev.GrossProfit), Format: func(v float64) string { return formatAUEC(int64(math.Round(v))) }},
|
||||||
|
{Label: "Orga-Anteil", Current: float64(cur.OrgShare), Previous: float64(prev.OrgShare), Format: func(v float64) string { return formatAUEC(int64(math.Round(v))) }},
|
||||||
|
{Label: "SCU", Current: cur.TotalSCU, Previous: prev.TotalSCU, Format: func(v float64) string { return formatAmount(v) }},
|
||||||
|
{Label: "Kosten", Current: float64(cur.TotalCosts), Previous: float64(prev.TotalCosts), Format: func(v float64) string { return formatAUEC(int64(math.Round(v))) }},
|
||||||
|
}
|
||||||
|
|
||||||
|
cardW, cardH := 490, 240
|
||||||
|
positions := []image.Point{{50, 120}, {560, 120}, {50, 390}, {560, 390}}
|
||||||
|
for idx, m := range metrics {
|
||||||
|
pos := positions[idx]
|
||||||
|
drawMetricCard(img, pos.X, pos.Y, cardW, cardH, m)
|
||||||
|
}
|
||||||
|
return encodePNG(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawMetricCard(img *image.RGBA, x, y, w, h int, m chartMetric) {
|
||||||
|
fillRect(img, image.Rect(x, y, x+w, y+h), color.RGBA{0x16, 0x21, 0x33, 0xff})
|
||||||
|
strokeRect(img, image.Rect(x, y, x+w, y+h), chartGrid)
|
||||||
|
text(img, x+16, y+24, m.Label, chartText, true)
|
||||||
|
maxV := math.Max(math.Abs(m.Current), math.Abs(m.Previous))
|
||||||
|
if maxV <= 0 {
|
||||||
|
maxV = 1
|
||||||
|
}
|
||||||
|
chartBottom := y + h - 44
|
||||||
|
chartTop := y + 54
|
||||||
|
chartH := chartBottom - chartTop
|
||||||
|
barW := 90
|
||||||
|
gap := 80
|
||||||
|
bar1X := x + 100
|
||||||
|
bar2X := bar1X + barW + gap
|
||||||
|
for i := 0; i <= 4; i++ {
|
||||||
|
gy := chartBottom - i*chartH/4
|
||||||
|
line(img, x+24, gy, x+w-24, gy, chartGrid)
|
||||||
|
text(img, x+28, gy-4, m.Format(maxV*float64(i)/4), chartMuted, false)
|
||||||
|
}
|
||||||
|
curH := int((math.Abs(m.Current) / maxV) * float64(chartH-10))
|
||||||
|
prevH := int((math.Abs(m.Previous) / maxV) * float64(chartH-10))
|
||||||
|
fillRect(img, image.Rect(bar1X, chartBottom-curH, bar1X+barW, chartBottom), chartCurrent)
|
||||||
|
fillRect(img, image.Rect(bar2X, chartBottom-prevH, bar2X+barW, chartBottom), chartPrevious)
|
||||||
|
textCentered(img, bar1X+barW/2, chartBottom+20, "Aktuell", chartText, false)
|
||||||
|
textCentered(img, bar2X+barW/2, chartBottom+20, "Vormonat", chartText, false)
|
||||||
|
textCentered(img, bar1X+barW/2, chartBottom-curH-8, m.Format(m.Current), chartText, false)
|
||||||
|
textCentered(img, bar2X+barW/2, chartBottom-prevH-8, m.Format(m.Previous), chartText, false)
|
||||||
|
text(img, x+16, y+h-14, deltaText(m.Current, m.Previous, m.Format), chartMuted, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTradingPurposePie(cur *TradingSummary) ([]byte, error) {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 1100, 700))
|
||||||
|
draw.Draw(img, img.Bounds(), &image.Uniform{chartBG}, image.Point{}, draw.Src)
|
||||||
|
fillRect(img, image.Rect(24, 24, 1076, 676), chartPanel)
|
||||||
|
text(img, 48, 62, "Verteilung nach Zweck", chartText, true)
|
||||||
|
subtitle := fmt.Sprintf("Monat %02d/%d – Anteil nach Gewinn", cur.Month, cur.Year)
|
||||||
|
|
||||||
|
parts := []piePart{}
|
||||||
|
total := 0.0
|
||||||
|
for idx, p := range cur.PurposeSummary {
|
||||||
|
v := float64(p.Profit)
|
||||||
|
if v < 0 {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
if v <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, piePart{Label: tradingPurposeLabel(p.Purpose), Value: v, Color: purposeColors[idx%len(purposeColors)]})
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
if total <= 0 {
|
||||||
|
subtitle = fmt.Sprintf("Monat %02d/%d – Anteil nach Anzahl Runs", cur.Month, cur.Year)
|
||||||
|
parts = parts[:0]
|
||||||
|
total = 0
|
||||||
|
for idx, p := range cur.PurposeSummary {
|
||||||
|
v := float64(p.Runs)
|
||||||
|
if v <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, piePart{Label: tradingPurposeLabel(p.Purpose), Value: v, Color: purposeColors[idx%len(purposeColors)]})
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total <= 0 {
|
||||||
|
parts = []piePart{{Label: "Keine Daten", Value: 1, Color: chartMuted}}
|
||||||
|
total = 1
|
||||||
|
}
|
||||||
|
text(img, 48, 88, subtitle, chartMuted, false)
|
||||||
|
|
||||||
|
centerX, centerY := 320, 380
|
||||||
|
radius := 180
|
||||||
|
start := -math.Pi / 2
|
||||||
|
for _, p := range parts {
|
||||||
|
angle := 2 * math.Pi * (p.Value / total)
|
||||||
|
fillPieSlice(img, centerX, centerY, radius, start, start+angle, p.Color)
|
||||||
|
start += angle
|
||||||
|
}
|
||||||
|
fillCircle(img, centerX, centerY, 78, chartPanel) // donut hole
|
||||||
|
textCentered(img, centerX, centerY-6, "Gesamt", chartText, true)
|
||||||
|
textCentered(img, centerX, centerY+16, formatInt(int64(math.Round(total))), chartMuted, false)
|
||||||
|
|
||||||
|
legendX := 600
|
||||||
|
legendY := 150
|
||||||
|
for idx, p := range parts {
|
||||||
|
y := legendY + idx*74
|
||||||
|
fillRect(img, image.Rect(legendX, y, legendX+24, y+24), p.Color)
|
||||||
|
strokeRect(img, image.Rect(legendX, y, legendX+24, y+24), chartGrid)
|
||||||
|
text(img, legendX+36, y+18, p.Label, chartText, true)
|
||||||
|
pct := 0.0
|
||||||
|
if total > 0 {
|
||||||
|
pct = (p.Value / total) * 100
|
||||||
|
}
|
||||||
|
text(img, legendX+36, y+42, fmt.Sprintf("%s (%.1f%%)", formatInt(int64(math.Round(p.Value))), pct), chartMuted, false)
|
||||||
|
}
|
||||||
|
return encodePNG(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillPieSlice(img *image.RGBA, cx, cy, r int, start, end float64, c color.RGBA) {
|
||||||
|
for y := cy - r; y <= cy+r; y++ {
|
||||||
|
for x := cx - r; x <= cx+r; x++ {
|
||||||
|
dx := float64(x - cx)
|
||||||
|
dy := float64(y - cy)
|
||||||
|
if dx*dx+dy*dy > float64(r*r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a := math.Atan2(dy, dx)
|
||||||
|
if a < start {
|
||||||
|
a += 2 * math.Pi
|
||||||
|
}
|
||||||
|
adjEnd := end
|
||||||
|
for adjEnd < start {
|
||||||
|
adjEnd += 2 * math.Pi
|
||||||
|
}
|
||||||
|
if a >= start && a <= adjEnd {
|
||||||
|
img.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillCircle(img *image.RGBA, cx, cy, r int, c color.RGBA) {
|
||||||
|
for y := cy - r; y <= cy+r; y++ {
|
||||||
|
for x := cx - r; x <= cx+r; x++ {
|
||||||
|
dx := x - cx
|
||||||
|
dy := y - cy
|
||||||
|
if dx*dx+dy*dy <= r*r {
|
||||||
|
img.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePNG(img image.Image) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := png.Encode(&buf, img); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillRect(img *image.RGBA, rect image.Rectangle, c color.RGBA) {
|
||||||
|
draw.Draw(img, rect, &image.Uniform{c}, image.Point{}, draw.Src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func strokeRect(img *image.RGBA, rect image.Rectangle, c color.RGBA) {
|
||||||
|
line(img, rect.Min.X, rect.Min.Y, rect.Max.X-1, rect.Min.Y, c)
|
||||||
|
line(img, rect.Min.X, rect.Max.Y-1, rect.Max.X-1, rect.Max.Y-1, c)
|
||||||
|
line(img, rect.Min.X, rect.Min.Y, rect.Min.X, rect.Max.Y-1, c)
|
||||||
|
line(img, rect.Max.X-1, rect.Min.Y, rect.Max.X-1, rect.Max.Y-1, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func line(img *image.RGBA, x0, y0, x1, y1 int, c color.RGBA) {
|
||||||
|
dx := int(math.Abs(float64(x1 - x0)))
|
||||||
|
sx := -1
|
||||||
|
if x0 < x1 {
|
||||||
|
sx = 1
|
||||||
|
}
|
||||||
|
dy := -int(math.Abs(float64(y1 - y0)))
|
||||||
|
sy := -1
|
||||||
|
if y0 < y1 {
|
||||||
|
sy = 1
|
||||||
|
}
|
||||||
|
err := dx + dy
|
||||||
|
for {
|
||||||
|
img.Set(x0, y0, c)
|
||||||
|
if x0 == x1 && y0 == y1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e2 := 2 * err
|
||||||
|
if e2 >= dy {
|
||||||
|
err += dy
|
||||||
|
x0 += sx
|
||||||
|
}
|
||||||
|
if e2 <= dx {
|
||||||
|
err += dx
|
||||||
|
y0 += sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func text(img *image.RGBA, x, y int, label string, col color.RGBA, bold bool) {
|
||||||
|
face := basicfont.Face7x13
|
||||||
|
d := &font.Drawer{Dst: img, Src: image.NewUniform(col), Face: face, Dot: fixed.P(x, y)}
|
||||||
|
if bold {
|
||||||
|
// simple faux bold
|
||||||
|
d.DrawString(label)
|
||||||
|
d.Dot = fixed.P(x+1, y)
|
||||||
|
}
|
||||||
|
d.DrawString(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func textCentered(img *image.RGBA, cx, y int, label string, col color.RGBA, bold bool) {
|
||||||
|
w := font.MeasureString(basicfont.Face7x13, label).Round()
|
||||||
|
text(img, cx-w/2, y, label, col, bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deltaText(cur, prev float64, formatter func(float64) string) string {
|
||||||
|
diff := cur - prev
|
||||||
|
sign := "+"
|
||||||
|
if diff < 0 {
|
||||||
|
sign = ""
|
||||||
|
}
|
||||||
|
pctText := "n/a"
|
||||||
|
if math.Abs(prev) > 0.00001 {
|
||||||
|
pctText = fmt.Sprintf("%s%.1f%%", sign, (diff/prev)*100)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Δ %s (%s)", formatter(diff), pctText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparisonSummaryText(cur, prev *TradingSummary) string {
|
||||||
|
lines := []string{
|
||||||
|
compareLineMoney("Gewinn", cur.GrossProfit, prev.GrossProfit),
|
||||||
|
compareLineMoney("Orga-Anteil", cur.OrgShare, prev.OrgShare),
|
||||||
|
compareLineAmount("SCU", cur.TotalSCU, prev.TotalSCU),
|
||||||
|
compareLineMoney("Kosten", cur.TotalCosts, prev.TotalCosts),
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareLineMoney(label string, cur, prev int64) string {
|
||||||
|
return fmt.Sprintf("%s: %s vs. %s (%s)", label, formatAUEC(cur), formatAUEC(prev), diffWithPercent(float64(cur), float64(prev), true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareLineAmount(label string, cur, prev float64) string {
|
||||||
|
return fmt.Sprintf("%s: %s vs. %s (%s)", label, formatAmount(cur), formatAmount(prev), diffWithPercent(cur, prev, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func diffWithPercent(cur, prev float64, money bool) string {
|
||||||
|
diff := cur - prev
|
||||||
|
sign := "+"
|
||||||
|
if diff < 0 {
|
||||||
|
sign = ""
|
||||||
|
}
|
||||||
|
var diffValue string
|
||||||
|
if money {
|
||||||
|
diffValue = signPrefix(diff) + formatAUEC(int64(math.Round(math.Abs(diff))))
|
||||||
|
} else {
|
||||||
|
diffValue = signPrefix(diff) + formatAmount(math.Abs(diff))
|
||||||
|
}
|
||||||
|
if math.Abs(prev) <= 0.00001 {
|
||||||
|
return diffValue + ", kein Vergleich"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s, %s%.1f%%", diffValue, sign, (diff/prev)*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signPrefix(v float64) string {
|
||||||
|
if v > 0 {
|
||||||
|
return "+"
|
||||||
|
}
|
||||||
|
if v < 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return "±"
|
||||||
|
}
|
||||||
|
|
||||||
|
func previousMonth(month, year int) (int, int) {
|
||||||
|
if month <= 1 {
|
||||||
|
return 12, year - 1
|
||||||
|
}
|
||||||
|
return month - 1, year
|
||||||
|
}
|
||||||
|
|
||||||
|
func topTraderBarChartData(sum *TradingSummary) []TradingTraderSummary {
|
||||||
|
items := append([]TradingTraderSummary(nil), sum.TopTraders...)
|
||||||
|
sort.Slice(items, func(i, j int) bool { return items[i].Profit > items[j].Profit })
|
||||||
|
if len(items) > 3 {
|
||||||
|
items = items[:3]
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
+27
-4
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
@@ -176,20 +177,41 @@ func (b *Bot) cmdTradingReport(s *discordgo.Session, i *discordgo.InteractionCre
|
|||||||
b.replyEphemeral(s, i, "Für diesen Monat gibt es keine Trading-Einträge.")
|
b.replyEphemeral(s, i, "Für diesen Monat gibt es keine Trading-Einträge.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
prevMonth, prevYear := previousMonth(month, year)
|
||||||
|
prevSummary, err := b.store.SummarizeTradingRuns(prevMonth, prevYear)
|
||||||
|
if err != nil {
|
||||||
|
prevSummary = &TradingSummary{Month: prevMonth, Year: prevYear}
|
||||||
|
}
|
||||||
|
|
||||||
channelID := b.cfg.TradingReportChannelID
|
channelID := b.cfg.TradingReportChannelID
|
||||||
if channelID == "" {
|
if channelID == "" {
|
||||||
channelID = b.cfg.InternalOrderChannelID
|
channelID = b.cfg.InternalOrderChannelID
|
||||||
}
|
}
|
||||||
_, err = s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{b.tradingReportEmbed(summary)}})
|
|
||||||
|
msg := &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{b.tradingReportEmbed(summary, prevSummary)}}
|
||||||
|
if comparisonPNG, purposePNG, chartErr := createTradingCharts(summary, prevSummary); chartErr == nil {
|
||||||
|
msg.Files = []*discordgo.File{
|
||||||
|
{Name: "trading_compare.png", Reader: bytes.NewReader(comparisonPNG), ContentType: "image/png"},
|
||||||
|
{Name: "trading_purpose.png", Reader: bytes.NewReader(purposePNG), ContentType: "image/png"},
|
||||||
|
}
|
||||||
|
msg.Embeds = append(msg.Embeds,
|
||||||
|
&discordgo.MessageEmbed{Title: "Vormonatsvergleich", Description: fmt.Sprintf("Vergleich %02d/%d zu %02d/%d", month, year, prevMonth, prevYear), Color: 0x3498db, Image: &discordgo.MessageEmbedImage{URL: "attachment://trading_compare.png"}},
|
||||||
|
&discordgo.MessageEmbed{Title: "Verteilung nach Zweck", Description: fmt.Sprintf("Monat %02d/%d", month, year), Color: 0xf1c40f, Image: &discordgo.MessageEmbedImage{URL: "attachment://trading_purpose.png"}},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Println("trading charts failed:", chartErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.ChannelMessageSendComplex(channelID, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.replyEphemeral(s, i, "Report konnte nicht gepostet werden: "+err.Error())
|
b.replyEphemeral(s, i, "Report konnte nicht gepostet werden: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.audit(fmt.Sprintf("Trading-Report %02d/%d gepostet durch %s", month, year, userString(i)))
|
b.audit(fmt.Sprintf("Trading-Report %02d/%d gepostet durch %s", month, year, userString(i)))
|
||||||
b.replyEphemeral(s, i, fmt.Sprintf("Trading-Report %02d/%d wurde gepostet.", month, year))
|
b.replyEphemeral(s, i, fmt.Sprintf("Trading-Report %02d/%d wurde gepostet. Vergleich zu %02d/%d:\n%s", month, year, prevMonth, prevYear, comparisonSummaryText(summary, prevSummary)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) tradingReportEmbed(sum *TradingSummary) *discordgo.MessageEmbed {
|
func (b *Bot) tradingReportEmbed(sum, prev *TradingSummary) *discordgo.MessageEmbed {
|
||||||
fields := []*discordgo.MessageEmbedField{
|
fields := []*discordgo.MessageEmbedField{
|
||||||
{Name: "Einträge", Value: fmt.Sprintf("%d", sum.Count), Inline: true},
|
{Name: "Einträge", Value: fmt.Sprintf("%d", sum.Count), Inline: true},
|
||||||
{Name: "Gewinn gesamt", Value: formatAUEC(sum.GrossProfit), Inline: true},
|
{Name: "Gewinn gesamt", Value: formatAUEC(sum.GrossProfit), Inline: true},
|
||||||
@@ -197,10 +219,11 @@ func (b *Bot) tradingReportEmbed(sum *TradingSummary) *discordgo.MessageEmbed {
|
|||||||
{Name: "Transportierte SCU", Value: formatAmount(sum.TotalSCU), Inline: true},
|
{Name: "Transportierte SCU", Value: formatAmount(sum.TotalSCU), Inline: true},
|
||||||
{Name: "Kosten gesamt", Value: formatAUEC(sum.TotalCosts), Inline: true},
|
{Name: "Kosten gesamt", Value: formatAUEC(sum.TotalCosts), Inline: true},
|
||||||
{Name: "Distanz gesamt", Value: formatAmount(sum.TotalDistance), Inline: true},
|
{Name: "Distanz gesamt", Value: formatAmount(sum.TotalDistance), Inline: true},
|
||||||
|
{Name: "Vergleich zum Vormonat", Value: comparisonSummaryText(sum, prev), Inline: false},
|
||||||
{Name: "Top 3 Trader", Value: topTraderText(sum.TopTraders), Inline: false},
|
{Name: "Top 3 Trader", Value: topTraderText(sum.TopTraders), Inline: false},
|
||||||
{Name: "Nach Zweck", Value: purposeSummaryText(sum.PurposeSummary), Inline: false},
|
{Name: "Nach Zweck", Value: purposeSummaryText(sum.PurposeSummary), Inline: false},
|
||||||
}
|
}
|
||||||
return &discordgo.MessageEmbed{Title: fmt.Sprintf("Trading-Auswertung %02d/%d", sum.Month, sum.Year), Description: "Monatliche Zusammenfassung der erfassten Trading-Runs.", Color: 0x2ecc71, Fields: fields, Timestamp: time.Now().UTC().Format(time.RFC3339)}
|
return &discordgo.MessageEmbed{Title: fmt.Sprintf("Trading-Auswertung %02d/%d", sum.Month, sum.Year), Description: "Monatliche Zusammenfassung der erfassten Trading-Runs inklusive Vergleich zum Vormonat.", Color: 0x2ecc71, Fields: fields, Timestamp: time.Now().UTC().Format(time.RFC3339)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func topTraderText(items []TradingTraderSummary) string {
|
func topTraderText(items []TradingTraderSummary) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user