424 lines
17 KiB
Go
424 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
)
|
|
|
|
func tradingCommands() []*discordgo.ApplicationCommand {
|
|
return []*discordgo.ApplicationCommand{
|
|
{Name: "trading_add", Description: "Erfasst einen Trading-Run", Options: []*discordgo.ApplicationCommandOption{
|
|
{Name: "zweck", Description: "Zweck des Runs", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: tradingPurposeChoices()},
|
|
{Name: "schiff", Description: "Schiff", Type: discordgo.ApplicationCommandOptionString, Required: true, Autocomplete: true},
|
|
{Name: "einkaufswert", Description: "Einkaufswert in aUEC", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0)},
|
|
{Name: "verkaufswert", Description: "Verkaufswert in aUEC", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0)},
|
|
{Name: "kosten", Description: "Kosten in aUEC", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0)},
|
|
{Name: "wiederholungen", Description: "Anzahl Wiederholungen", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(1)},
|
|
{Name: "scu", Description: "Transportierte SCU je Wiederholung", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0)},
|
|
{Name: "distanz", Description: "Distanz je Wiederholung", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0)},
|
|
{Name: "scu_gr", Description: "SCU-Größe / Container-Größe", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
|
{Name: "ware", Description: "Ware", Type: discordgo.ApplicationCommandOptionString, Required: true, Autocomplete: true},
|
|
{Name: "tradingpost_a", Description: "Start / Einkauf", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
|
{Name: "tradingpost_b", Description: "Ziel / Verkauf", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
|
{Name: "monat", Description: "Monat 1-12, Standard aktueller Monat", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
|
{Name: "jahr", Description: "Jahr, Standard aktuelles Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
|
{Name: "bemerkung", Description: "Optionale Bemerkung", Type: discordgo.ApplicationCommandOptionString, Required: false},
|
|
}},
|
|
{Name: "trading_liste", Description: "Zeigt Trading-Einträge mit Filter", Options: []*discordgo.ApplicationCommandOption{
|
|
{Name: "monat", Description: "Monat 1-12", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
|
{Name: "jahr", Description: "Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
|
{Name: "nutzer", Description: "Discord-Nutzer", Type: discordgo.ApplicationCommandOptionUser, Required: false},
|
|
{Name: "limit", Description: "Maximal 50", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(1), MaxValue: float64(50)},
|
|
}},
|
|
{Name: "trading_delete", Description: "Admin: löscht einen Trading-Eintrag", Options: []*discordgo.ApplicationCommandOption{
|
|
{Name: "id", Description: "Trading-ID", Type: discordgo.ApplicationCommandOptionInteger, Required: true},
|
|
}},
|
|
{Name: "trading_report", Description: "Admin: postet die Monatsauswertung", Options: []*discordgo.ApplicationCommandOption{
|
|
{Name: "monat", Description: "Monat 1-12", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
|
{Name: "jahr", Description: "Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
|
}},
|
|
{Name: "trading_abrechnung", Description: "Admin: sendet Mitgliedern ihre monatliche Trading-Abrechnung per DM", Options: []*discordgo.ApplicationCommandOption{
|
|
{Name: "monat", Description: "Monat 1-12", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
|
{Name: "jahr", Description: "Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func tradingPurposeChoices() []*discordgo.ApplicationCommandOptionChoice {
|
|
return []*discordgo.ApplicationCommandOptionChoice{
|
|
{Name: "Trade", Value: TradingPurposeTrade},
|
|
{Name: "Hauling", Value: TradingPurposeHauling},
|
|
{Name: "Event", Value: TradingPurposeEvent},
|
|
{Name: "Intern", Value: TradingPurposeIntern},
|
|
}
|
|
}
|
|
|
|
func (b *Bot) cmdTradingAdd(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !b.canUseTrading(i) {
|
|
b.replyEphemeral(s, i, "Du hast keine Berechtigung, Trading-Runs einzutragen.")
|
|
return
|
|
}
|
|
now := time.Now()
|
|
month := int(optIntDefault(i, "monat", int64(now.Month())))
|
|
year := int(optIntDefault(i, "jahr", int64(now.Year())))
|
|
purpose := optString(i, "zweck")
|
|
ship := strings.TrimSpace(optString(i, "schiff"))
|
|
commodity := strings.TrimSpace(optString(i, "ware"))
|
|
if !validTradingPurpose(purpose) {
|
|
b.replyEphemeral(s, i, "Ungültiger Zweck.")
|
|
return
|
|
}
|
|
if ship == "" || commodity == "" {
|
|
b.replyEphemeral(s, i, "Schiff und Ware sind Pflichtfelder.")
|
|
return
|
|
}
|
|
if len(b.cfg.TradingShips) > 0 && !containsFold(b.cfg.TradingShips, ship) {
|
|
b.replyEphemeral(s, i, "Dieses Schiff ist nicht in TRADING_SHIPS konfiguriert.")
|
|
return
|
|
}
|
|
if len(b.cfg.TradingCommodities) > 0 && !containsFold(b.cfg.TradingCommodities, commodity) {
|
|
b.replyEphemeral(s, i, "Diese Ware ist nicht in TRADING_COMMODITIES konfiguriert.")
|
|
return
|
|
}
|
|
|
|
run := &TradingRun{
|
|
UserID: userID(i),
|
|
UserName: userString(i),
|
|
Purpose: purpose,
|
|
Ship: ship,
|
|
BuyValue: optInt(i, "einkaufswert"),
|
|
SellValue: optInt(i, "verkaufswert"),
|
|
Costs: optInt(i, "kosten"),
|
|
Repetitions: optInt(i, "wiederholungen"),
|
|
SCU: optFloat(i, "scu"),
|
|
Distance: optFloat(i, "distanz"),
|
|
SCUGrade: strings.TrimSpace(optString(i, "scu_gr")),
|
|
Commodity: commodity,
|
|
PostA: strings.TrimSpace(optString(i, "tradingpost_a")),
|
|
PostB: strings.TrimSpace(optString(i, "tradingpost_b")),
|
|
Note: strings.TrimSpace(optString(i, "bemerkung")),
|
|
Month: month,
|
|
Year: year,
|
|
OrgPercent: b.cfg.TradingOrgSharePercent[purpose],
|
|
}
|
|
id, err := b.store.CreateTradingRun(run)
|
|
if err != nil {
|
|
b.replyEphemeral(s, i, "Trading-Run konnte nicht gespeichert werden: "+err.Error())
|
|
return
|
|
}
|
|
run.ID = id
|
|
b.audit(fmt.Sprintf("Trading-Run #%d eingetragen durch %s: %s, Gewinn %s aUEC, Orga-Anteil %s aUEC", id, userString(i), run.Commodity, formatInt(tradingProfit(*run)), formatInt(tradingOrgShare(*run))))
|
|
b.replyEphemeral(s, i, fmt.Sprintf("Trading-Run #%d gespeichert. Gewinn: %s aUEC, Orga-Anteil: %s aUEC.", id, formatInt(tradingProfit(*run)), formatInt(tradingOrgShare(*run))))
|
|
}
|
|
|
|
func (b *Bot) cmdTradingList(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !b.canUseTrading(i) {
|
|
b.replyEphemeral(s, i, "Du hast keine Berechtigung, Trading-Runs anzuzeigen.")
|
|
return
|
|
}
|
|
filter := TradingListFilter{
|
|
Month: int(optInt(i, "monat")),
|
|
Year: int(optInt(i, "jahr")),
|
|
Limit: int(optIntDefault(i, "limit", 20)),
|
|
}
|
|
if u := optUser(i, s, "nutzer"); u != nil {
|
|
filter.UserID = u.ID
|
|
}
|
|
if filter.UserID == "" && !b.isTeamOrAdmin(i) {
|
|
filter.UserID = userID(i)
|
|
}
|
|
runs, err := b.store.ListTradingRuns(filter)
|
|
if err != nil {
|
|
b.replyEphemeral(s, i, err.Error())
|
|
return
|
|
}
|
|
if len(runs) == 0 {
|
|
b.replyEphemeral(s, i, "Keine Trading-Einträge gefunden.")
|
|
return
|
|
}
|
|
b.replyEphemeral(s, i, "Trading-Einträge\n"+tradingTable(runs))
|
|
}
|
|
|
|
func (b *Bot) cmdTradingDelete(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !b.isAdmin(i) {
|
|
b.replyEphemeral(s, i, "Nur Admins dürfen Trading-Einträge löschen.")
|
|
return
|
|
}
|
|
id := optInt(i, "id")
|
|
run, err := b.store.DeleteTradingRun(id, userID(i))
|
|
if err != nil {
|
|
b.replyEphemeral(s, i, "Eintrag konnte nicht gelöscht werden: "+err.Error())
|
|
return
|
|
}
|
|
b.audit(fmt.Sprintf("Trading-Run #%d gelöscht durch %s. Ursprünglicher Nutzer: %s", id, userString(i), run.UserName))
|
|
b.replyEphemeral(s, i, fmt.Sprintf("Trading-Run #%d wurde gelöscht.", id))
|
|
}
|
|
|
|
func (b *Bot) cmdTradingReport(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !b.isAdmin(i) {
|
|
b.replyEphemeral(s, i, "Nur Admins dürfen Trading-Reports posten.")
|
|
return
|
|
}
|
|
month := int(optInt(i, "monat"))
|
|
year := int(optInt(i, "jahr"))
|
|
summary, err := b.store.SummarizeTradingRuns(month, year)
|
|
if err != nil {
|
|
b.replyEphemeral(s, i, "Report konnte nicht erstellt werden: "+err.Error())
|
|
return
|
|
}
|
|
if summary.Count == 0 {
|
|
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
|
|
}
|
|
|
|
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. Vergleich zu %02d/%d:\n%s", month, year, prevMonth, prevYear, comparisonSummaryText(summary, prevSummary)))
|
|
}
|
|
|
|
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},
|
|
{Name: "Orga-Anteil", Value: formatAUEC(sum.OrgShare), Inline: true},
|
|
{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 inklusive Vergleich zum Vormonat.", Color: 0x2ecc71, Fields: fields, Timestamp: time.Now().UTC().Format(time.RFC3339)}
|
|
}
|
|
|
|
func topTraderText(items []TradingTraderSummary) string {
|
|
if len(items) == 0 {
|
|
return "Keine Daten"
|
|
}
|
|
var lines []string
|
|
for idx, t := range items {
|
|
name := t.UserName
|
|
if t.UserID != "" {
|
|
name = fmt.Sprintf("<@%s>", t.UserID)
|
|
}
|
|
lines = append(lines, fmt.Sprintf("%d. %s — Gewinn %s, Orga %s, SCU %s, Runs %d", idx+1, name, formatAUEC(t.Profit), formatAUEC(t.OrgShare), formatAmount(t.SCU), t.Runs))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func purposeSummaryText(items []TradingPurposeSummary) string {
|
|
if len(items) == 0 {
|
|
return "Keine Daten"
|
|
}
|
|
var lines []string
|
|
for _, p := range items {
|
|
lines = append(lines, fmt.Sprintf("%s — Gewinn %s, Orga %s, SCU %s, Runs %d", tradingPurposeLabel(p.Purpose), formatAUEC(p.Profit), formatAUEC(p.OrgShare), formatAmount(p.SCU), p.Runs))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func tradingTable(runs []TradingRun) string {
|
|
var b strings.Builder
|
|
b.WriteString("```text\n")
|
|
b.WriteString(fmt.Sprintf("%-4s %-7s %-15s %-9s %-14s %-12s %-12s %-8s\n", "ID", "Monat", "Nutzer", "Zweck", "Ware", "Gewinn", "Orga", "SCU"))
|
|
b.WriteString(strings.Repeat("-", 90) + "\n")
|
|
for _, r := range runs {
|
|
line := fmt.Sprintf("%-4d %02d/%-4d %-15s %-9s %-14s %-12s %-12s %-8s\n", r.ID, r.Month, r.Year, truncateText(r.UserName, 15), tradingPurposeLabel(r.Purpose), truncateText(r.Commodity, 14), formatInt(tradingProfit(r)), formatInt(tradingOrgShare(r)), formatAmount(r.SCU*float64(r.Repetitions)))
|
|
if b.Len()+len(line)+4 > 1900 {
|
|
b.WriteString("… Ausgabe gekürzt\n")
|
|
break
|
|
}
|
|
b.WriteString(line)
|
|
}
|
|
b.WriteString("```")
|
|
return b.String()
|
|
}
|
|
|
|
func (b *Bot) canUseTrading(i *discordgo.InteractionCreate) bool {
|
|
return b.isTeamOrAdmin(i) || hasAnyRole(i, b.cfg.TradingRoleIDs)
|
|
}
|
|
|
|
func validTradingPurpose(v string) bool {
|
|
switch v {
|
|
case TradingPurposeTrade, TradingPurposeHauling, TradingPurposeEvent, TradingPurposeIntern:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func tradingPurposeLabel(v string) string {
|
|
switch v {
|
|
case TradingPurposeTrade:
|
|
return "Trade"
|
|
case TradingPurposeHauling:
|
|
return "Hauling"
|
|
case TradingPurposeEvent:
|
|
return "Event"
|
|
case TradingPurposeIntern:
|
|
return "Intern"
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func formatAUEC(v int64) string { return formatInt(v) + " aUEC" }
|
|
|
|
func formatInt(v int64) string {
|
|
neg := v < 0
|
|
if neg {
|
|
v = -v
|
|
}
|
|
s := fmt.Sprintf("%d", v)
|
|
for i := len(s) - 3; i > 0; i -= 3 {
|
|
s = s[:i] + "." + s[i:]
|
|
}
|
|
if neg {
|
|
return "-" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
func containsFold(values []string, needle string) bool {
|
|
needle = strings.TrimSpace(needle)
|
|
for _, v := range values {
|
|
if strings.EqualFold(strings.TrimSpace(v), needle) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func autocompleteFromList(values []string, query string, limit int) []*discordgo.ApplicationCommandOptionChoice {
|
|
query = strings.ToLower(strings.TrimSpace(query))
|
|
var filtered []string
|
|
for _, v := range values {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if query == "" || strings.Contains(strings.ToLower(v), query) {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
sort.Slice(filtered, func(i, j int) bool { return strings.ToLower(filtered[i]) < strings.ToLower(filtered[j]) })
|
|
if limit <= 0 || limit > 25 {
|
|
limit = 25
|
|
}
|
|
if len(filtered) > limit {
|
|
filtered = filtered[:limit]
|
|
}
|
|
choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(filtered))
|
|
for _, v := range filtered {
|
|
choices = append(choices, &discordgo.ApplicationCommandOptionChoice{Name: v, Value: v})
|
|
}
|
|
return choices
|
|
}
|
|
|
|
func optUser(i *discordgo.InteractionCreate, s *discordgo.Session, name string) *discordgo.User {
|
|
for _, o := range i.ApplicationCommandData().Options {
|
|
if o.Name == name {
|
|
return o.UserValue(s)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func roundMoney(v float64) int64 { return int64(math.Round(v)) }
|
|
|
|
func (b *Bot) cmdTradingSettlement(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !b.isAdmin(i) {
|
|
b.replyEphemeral(s, i, "Nur Admins dürfen Trading-Abrechnungen versenden.")
|
|
return
|
|
}
|
|
month := int(optInt(i, "monat"))
|
|
year := int(optInt(i, "jahr"))
|
|
users, err := b.store.SummarizeTradingByUser(month, year)
|
|
if err != nil {
|
|
b.replyEphemeral(s, i, "Abrechnung konnte nicht erstellt werden: "+err.Error())
|
|
return
|
|
}
|
|
if len(users) == 0 {
|
|
b.replyEphemeral(s, i, "Für diesen Monat gibt es keine Trading-Einträge.")
|
|
return
|
|
}
|
|
|
|
sent, skipped, failed := 0, 0, 0
|
|
var failedNames []string
|
|
for _, u := range users {
|
|
if u.OrgShare <= 0 {
|
|
skipped++
|
|
continue
|
|
}
|
|
if strings.TrimSpace(u.UserID) == "" {
|
|
failed++
|
|
failedNames = append(failedNames, u.UserName)
|
|
continue
|
|
}
|
|
body := b.tradingSettlementText(u, month, year)
|
|
if err := b.dmErr(u.UserID, body); err != nil {
|
|
failed++
|
|
failedNames = append(failedNames, u.UserName)
|
|
log.Println("trading settlement dm failed:", u.UserName, err)
|
|
continue
|
|
}
|
|
sent++
|
|
}
|
|
|
|
msg := fmt.Sprintf("Abrechnung %02d/%d versendet. Erfolgreich: %d, übersprungen ohne Orga-Anteil: %d, fehlgeschlagen: %d.", month, year, sent, skipped, failed)
|
|
if len(failedNames) > 0 {
|
|
msg += "\nFehlgeschlagen: " + strings.Join(failedNames, ", ")
|
|
}
|
|
b.audit(fmt.Sprintf("Trading-Abrechnung %02d/%d durch %s versendet: %d erfolgreich, %d ohne Anteil, %d fehlgeschlagen", month, year, userString(i), sent, skipped, failed))
|
|
b.replyEphemeral(s, i, msg)
|
|
}
|
|
|
|
func (b *Bot) tradingSettlementText(u TradingTraderSummary, month, year int) string {
|
|
return fmt.Sprintf(
|
|
"Abrechnung Trading %02d/%d\n\nHallo %s,\n\nfür deine erfassten Trading-Runs ergibt sich folgende Abrechnung:\n\nRuns: %d\nGewinn: %s\nTransportierte SCU: %s\nDein Orga-Anteil: %s\n\nBitte überweise den Orga-Anteil an: %s\n\nDanke dir!",
|
|
month,
|
|
year,
|
|
u.UserName,
|
|
u.Runs,
|
|
formatAUEC(u.Profit),
|
|
formatAmount(u.SCU),
|
|
formatAUEC(u.OrgShare),
|
|
b.cfg.TradingPayoutRecipient,
|
|
)
|
|
}
|