This commit is contained in:
3
go.mod
3
go.mod
@@ -1,10 +1,11 @@
|
||||
module starauftrag-bot
|
||||
|
||||
go 1.22
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.28.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
golang.org/x/image v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
2
go.sum
2
go.sum
@@ -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=
|
||||
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/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/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
356
trading_charts.go
Normal file
356
trading_charts.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"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.")
|
||||
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
|
||||
if channelID == "" {
|
||||
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 {
|
||||
b.replyEphemeral(s, i, "Report konnte nicht gepostet werden: "+err.Error())
|
||||
return
|
||||
}
|
||||
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{
|
||||
{Name: "Einträge", Value: fmt.Sprintf("%d", sum.Count), 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: "Kosten gesamt", Value: formatAUEC(sum.TotalCosts), 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: "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 {
|
||||
|
||||
Reference in New Issue
Block a user