Files
nofx/kernel/formatter.go
2026-03-12 16:14:56 +08:00

637 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package kernel
import (
"fmt"
"nofx/market"
"nofx/provider/nofxos"
"sort"
"strings"
"time"
)
// ============================================================================
// AI Data Formatter
// ============================================================================
// Converts trading context into AI-friendly format, ensuring AI fully
// understands the data regardless of language.
// ============================================================================
// FormatContextForAI formats trading context into AI-readable text (including schema)
func FormatContextForAI(ctx *Context, lang Language) string {
var sb strings.Builder
// 1. Add schema description (so AI understands data format)
sb.WriteString(GetSchemaPrompt(lang))
sb.WriteString("\n---\n\n")
// 2. Current state overview
sb.WriteString(formatContextData(ctx, lang))
return sb.String()
}
// FormatContextDataOnly formats context data only, without schema (for use when schema is already present)
func FormatContextDataOnly(ctx *Context, lang Language) string {
return formatContextData(ctx, lang)
}
// formatContextData formats the core data section
func formatContextData(ctx *Context, lang Language) string {
var sb strings.Builder
// 1. Current state overview
if lang == LangChinese {
sb.WriteString(formatHeaderZH(ctx))
} else {
sb.WriteString(formatHeaderEN(ctx))
}
// 3. Account information
if lang == LangChinese {
sb.WriteString(formatAccountZH(ctx))
} else {
sb.WriteString(formatAccountEN(ctx))
}
// 4. Historical trading statistics
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
if lang == LangChinese {
sb.WriteString(formatTradingStatsZH(ctx.TradingStats))
} else {
sb.WriteString(formatTradingStatsEN(ctx.TradingStats))
}
}
// 5. Recent trade records
if len(ctx.RecentOrders) > 0 {
if lang == LangChinese {
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders))
} else {
sb.WriteString(formatRecentTradesEN(ctx.RecentOrders))
}
}
// 5. Current positions
if len(ctx.Positions) > 0 {
if lang == LangChinese {
sb.WriteString(formatCurrentPositionsZH(ctx))
} else {
sb.WriteString(formatCurrentPositionsEN(ctx))
}
}
// 6. Candidate coins (with market data)
if len(ctx.CandidateCoins) > 0 {
if lang == LangChinese {
sb.WriteString(formatCandidateCoinsZH(ctx))
} else {
sb.WriteString(formatCandidateCoinsEN(ctx))
}
}
// 7. OI ranking data (if available)
if ctx.OIRankingData != nil {
nofxosLang := nofxos.LangEnglish
if lang == LangChinese {
nofxosLang = nofxos.LangChinese
}
sb.WriteString(nofxos.FormatOIRankingForAI(ctx.OIRankingData, nofxosLang))
}
return sb.String()
}
// ========== Chinese Formatting Functions ==========
// formatHeaderZH formats header information (Chinese)
func formatHeaderZH(ctx *Context) string {
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
}
// formatAccountZH formats account information (Chinese)
func formatAccountZH(ctx *Context) string {
acc := ctx.Account
var sb strings.Builder
sb.WriteString("## 账户状态\n\n")
sb.WriteString(fmt.Sprintf("总权益: %.2f USDT | ", acc.TotalEquity))
sb.WriteString(fmt.Sprintf("可用余额: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100))
sb.WriteString(fmt.Sprintf("总盈亏: %+.2f%% | ", acc.TotalPnLPct))
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
// Add risk warnings
if acc.MarginUsedPct > 70 {
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
} else if acc.MarginUsedPct > 50 {
sb.WriteString("⚠️ **风险提示**: 保证金使用率 > 50%,建议谨慎开仓\n\n")
}
return sb.String()
}
// formatTradingStatsZH formats historical trading statistics (Chinese)
func formatTradingStatsZH(stats *TradingStats) string {
var sb strings.Builder
sb.WriteString("## 历史交易统计\n\n")
// Win/loss ratio calculation
var winLossRatio float64
if stats.AvgLoss > 0 {
winLossRatio = stats.AvgWin / stats.AvgLoss
}
// Metric definitions (focusing on core metrics, excluding win rate)
sb.WriteString("**指标说明**:\n")
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利>1.5为良好,>2为优秀\n")
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好>2优秀\n")
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀\n")
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
// Data values
sb.WriteString("**当前数据**:\n")
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor))
sb.WriteString(fmt.Sprintf("- 夏普比率: %.2f\n", stats.SharpeRatio))
sb.WriteString(fmt.Sprintf("- 盈亏比: %.2f\n", winLossRatio))
sb.WriteString(fmt.Sprintf("- 总盈亏: %+.2f USDT\n", stats.TotalPnL))
sb.WriteString(fmt.Sprintf("- 平均盈利: +%.2f USDT\n", stats.AvgWin))
sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
// Comprehensive analysis and decision guidance
sb.WriteString("**决策参考**:\n")
// Provide specific recommendations based on statistics
if stats.TotalTrades < 10 {
sb.WriteString("- 样本量较小(<10笔统计结果参考意义有限\n")
}
if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 {
sb.WriteString("- 📈 表现良好: 可以维持当前策略风格\n")
} else if stats.ProfitFactor >= 1.0 {
sb.WriteString("- 📊 表现正常: 策略可行但有优化空间\n")
}
if stats.ProfitFactor < 1.0 {
sb.WriteString("- ⚠️ 盈利因子<1: 亏损大于盈利,需要提高盈亏比,优化止盈止损\n")
}
if winLossRatio > 0 && winLossRatio < 1.5 {
sb.WriteString("- ⚠️ 盈亏比偏低: 建议让利润奔跑,提高止盈目标\n")
}
if stats.MaxDrawdownPct > 30 {
sb.WriteString("- ⚠️ 最大回撤过高: 建议降低仓位大小控制风险\n")
} else if stats.MaxDrawdownPct < 10 {
sb.WriteString("- ✅ 回撤控制良好: 风险管理有效\n")
}
sb.WriteString("\n")
return sb.String()
}
// formatRecentTradesZH formats recent trades (Chinese)
func formatRecentTradesZH(orders []RecentOrder) string {
var sb strings.Builder
sb.WriteString("## 最近完成的交易\n\n")
for i, order := range orders {
// Determine profit or loss
profitOrLoss := "盈利"
if order.RealizedPnL < 0 {
profitOrLoss = "亏损"
}
sb.WriteString(fmt.Sprintf("%d. %s %s | 进场 %.4f 出场 %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n",
i+1,
order.Symbol,
order.Side,
order.EntryPrice,
order.ExitPrice,
profitOrLoss,
order.RealizedPnL,
order.PnLPct,
order.EntryTime,
order.ExitTime,
order.HoldDuration,
))
}
sb.WriteString("\n")
return sb.String()
}
// formatCurrentPositionsZH formats current positions (Chinese)
func formatCurrentPositionsZH(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## 当前持仓\n\n")
for i, pos := range ctx.Positions {
// Calculate drawdown
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
sb.WriteString(fmt.Sprintf("进场 %.4f 当前 %.4f | ", pos.EntryPrice, pos.MarkPrice))
sb.WriteString(fmt.Sprintf("数量 %.4f | ", pos.Quantity))
sb.WriteString(fmt.Sprintf("仓位价值 %.2f USDT | ", pos.Quantity*pos.MarkPrice))
sb.WriteString(fmt.Sprintf("盈亏 %+.2f%% | ", pos.UnrealizedPnLPct))
sb.WriteString(fmt.Sprintf("盈亏金额 %+.2f USDT | ", pos.UnrealizedPnL))
sb.WriteString(fmt.Sprintf("峰值盈亏 %.2f%% | ", pos.PeakPnLPct))
sb.WriteString(fmt.Sprintf("杠杆 %dx | ", pos.Leverage))
sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed))
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
// Add analysis hints
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
}
if pos.UnrealizedPnLPct < -4.0 {
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
}
// Show current price (if market data available)
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
}
}
sb.WriteString("\n")
}
return sb.String()
}
// formatCandidateCoinsZH formats candidate coins (Chinese)
func formatCandidateCoinsZH(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## 候选币种\n\n")
for i, coin := range ctx.CandidateCoins {
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
// Current price
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
// Kline data (multi-timeframe)
if mdata.TimeframeData != nil {
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
}
}
}
// OI data (if available)
if ctx.OITopDataMap != nil {
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
oiData.Rank,
oiData.OIDeltaPercent,
oiData.OIDeltaValue/1_000_000,
oiData.PriceDeltaPercent,
))
// OI interpretation
oiChange := "增加"
if oiData.OIDeltaPercent < 0 {
oiChange = "减少"
}
priceChange := "上涨"
if oiData.PriceDeltaPercent < 0 {
priceChange = "下跌"
}
interpretation := getOIInterpretationZH(oiChange, priceChange)
sb.WriteString(fmt.Sprintf("**市场解读**: %s\n\n", interpretation))
}
}
}
return sb.String()
}
// formatKlineDataZH formats kline data (Chinese)
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder
for _, tf := range timeframes {
if data, ok := tfData[tf]; ok && len(data.Klines) > 0 {
sb.WriteString(fmt.Sprintf("#### %s 时间框架 (从旧到新)\n\n", tf))
sb.WriteString("```\n")
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
// Only show the latest 30 klines
startIdx := 0
if len(data.Klines) > 30 {
startIdx = len(data.Klines) - 30
}
for i := startIdx; i < len(data.Klines); i++ {
k := data.Klines[i]
t := time.UnixMilli(k.Time).UTC()
sb.WriteString(fmt.Sprintf("%s %.4f %.4f %.4f %.4f %.2f\n",
t.Format("01-02 15:04"),
k.Open,
k.High,
k.Low,
k.Close,
k.Volume,
))
}
// Mark the last kline
if len(data.Klines) > 0 {
sb.WriteString(" <- 当前\n")
}
sb.WriteString("```\n\n")
}
}
return sb.String()
}
// getOIInterpretationZH returns OI change interpretation (Chinese)
func getOIInterpretationZH(oiChange, priceChange string) string {
if oiChange == "增加" && priceChange == "上涨" {
return OIInterpretation.OIUp_PriceUp.ZH
} else if oiChange == "增加" && priceChange == "下跌" {
return OIInterpretation.OIUp_PriceDown.ZH
} else if oiChange == "减少" && priceChange == "上涨" {
return OIInterpretation.OIDown_PriceUp.ZH
} else {
return OIInterpretation.OIDown_PriceDown.ZH
}
}
// ========== English Formatting Functions ==========
// formatHeaderEN formats header information (English)
func formatHeaderEN(ctx *Context) string {
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
}
// formatAccountEN formats account information (English)
func formatAccountEN(ctx *Context) string {
acc := ctx.Account
var sb strings.Builder
sb.WriteString("## Account Status\n\n")
sb.WriteString(fmt.Sprintf("Total Equity: %.2f USDT | ", acc.TotalEquity))
sb.WriteString(fmt.Sprintf("Available Balance: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100))
sb.WriteString(fmt.Sprintf("Total PnL: %+.2f%% | ", acc.TotalPnLPct))
sb.WriteString(fmt.Sprintf("Margin Usage: %.1f%% | ", acc.MarginUsedPct))
sb.WriteString(fmt.Sprintf("Positions: %d\n\n", acc.PositionCount))
// Risk warning
if acc.MarginUsedPct > 70 {
sb.WriteString("⚠️ **Risk Alert**: Margin usage > 70%, high risk!\n\n")
} else if acc.MarginUsedPct > 50 {
sb.WriteString("⚠️ **Risk Notice**: Margin usage > 50%, be cautious with new positions\n\n")
}
return sb.String()
}
// formatTradingStatsEN formats historical trading statistics (English)
func formatTradingStatsEN(stats *TradingStats) string {
var sb strings.Builder
sb.WriteString("## Historical Trading Statistics\n\n")
// Win/Loss ratio calculation
var winLossRatio float64
if stats.AvgLoss > 0 {
winLossRatio = stats.AvgWin / stats.AvgLoss
}
// Metric definitions (focus on core metrics, remove win rate)
sb.WriteString("**Metric Definitions**:\n")
sb.WriteString("- Profit Factor: Total profits ÷ Total losses (>1 = profitable, >1.5 = good, >2 = excellent)\n")
sb.WriteString("- Sharpe Ratio: (Avg return - Risk-free rate) ÷ Std dev of returns (>1 = good, >2 = excellent)\n")
sb.WriteString("- Win/Loss Ratio: Avg win ÷ Avg loss (>1.5 = good, >2 = excellent)\n")
sb.WriteString("- Max Drawdown: Largest peak-to-trough decline in equity curve (<20% = low risk)\n\n")
// Data values
sb.WriteString("**Current Data**:\n")
sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", stats.TotalTrades))
sb.WriteString(fmt.Sprintf("- Profit Factor: %.2f\n", stats.ProfitFactor))
sb.WriteString(fmt.Sprintf("- Sharpe Ratio: %.2f\n", stats.SharpeRatio))
sb.WriteString(fmt.Sprintf("- Win/Loss Ratio: %.2f\n", winLossRatio))
sb.WriteString(fmt.Sprintf("- Total PnL: %+.2f USDT\n", stats.TotalPnL))
sb.WriteString(fmt.Sprintf("- Avg Win: +%.2f USDT\n", stats.AvgWin))
sb.WriteString(fmt.Sprintf("- Avg Loss: -%.2f USDT\n", stats.AvgLoss))
sb.WriteString(fmt.Sprintf("- Max Drawdown: %.1f%%\n\n", stats.MaxDrawdownPct))
// Analysis and decision guidance
sb.WriteString("**Decision Guidance**:\n")
// Specific recommendations based on stats
if stats.TotalTrades < 10 {
sb.WriteString("- Small sample size (<10 trades), statistics have limited significance\n")
}
if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 {
sb.WriteString("- 📈 Good performance: Maintain current strategy approach\n")
} else if stats.ProfitFactor >= 1.0 {
sb.WriteString("- 📊 Normal performance: Strategy viable but has room for optimization\n")
}
if stats.ProfitFactor < 1.0 {
sb.WriteString("- ⚠️ Profit factor <1: Losses exceed profits, improve win/loss ratio, optimize TP/SL\n")
}
if winLossRatio > 0 && winLossRatio < 1.5 {
sb.WriteString("- ⚠️ Low win/loss ratio: Let profits run, increase take-profit targets\n")
}
if stats.MaxDrawdownPct > 30 {
sb.WriteString("- ⚠️ High max drawdown: Consider reducing position sizes to control risk\n")
} else if stats.MaxDrawdownPct < 10 {
sb.WriteString("- ✅ Good drawdown control: Risk management is effective\n")
}
sb.WriteString("\n")
return sb.String()
}
// formatRecentTradesEN formats recent trades (English)
func formatRecentTradesEN(orders []RecentOrder) string {
var sb strings.Builder
sb.WriteString("## Recent Completed Trades\n\n")
for i, order := range orders {
profitOrLoss := "Profit"
if order.RealizedPnL < 0 {
profitOrLoss = "Loss"
}
sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Exit %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n",
i+1,
order.Symbol,
order.Side,
order.EntryPrice,
order.ExitPrice,
profitOrLoss,
order.RealizedPnL,
order.PnLPct,
order.EntryTime,
order.ExitTime,
order.HoldDuration,
))
}
sb.WriteString("\n")
return sb.String()
}
// formatCurrentPositionsEN formats current positions (English)
func formatCurrentPositionsEN(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## Current Positions\n\n")
for i, pos := range ctx.Positions {
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
sb.WriteString(fmt.Sprintf("Entry %.4f Current %.4f | ", pos.EntryPrice, pos.MarkPrice))
sb.WriteString(fmt.Sprintf("Qty %.4f | ", pos.Quantity))
sb.WriteString(fmt.Sprintf("Value %.2f USDT | ", pos.Quantity*pos.MarkPrice))
sb.WriteString(fmt.Sprintf("PnL %+.2f%% | ", pos.UnrealizedPnLPct))
sb.WriteString(fmt.Sprintf("PnL Amount %+.2f USDT | ", pos.UnrealizedPnL))
sb.WriteString(fmt.Sprintf("Peak PnL %.2f%% | ", pos.PeakPnLPct))
sb.WriteString(fmt.Sprintf("Leverage %dx | ", pos.Leverage))
sb.WriteString(fmt.Sprintf("Margin %.0f USDT | ", pos.MarginUsed))
sb.WriteString(fmt.Sprintf("Liq Price %.4f\n", pos.LiquidationPrice))
// Analysis hints
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
sb.WriteString(fmt.Sprintf(" ⚠️ **Take Profit Alert**: PnL dropped from peak %.2f%% to %.2f%%, drawdown %.2f%%, consider taking profit\n",
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
}
if pos.UnrealizedPnLPct < -4.0 {
sb.WriteString(" ⚠️ **Stop Loss Alert**: Loss approaching -5% threshold, consider cutting loss\n")
}
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(fmt.Sprintf(" 📈 Current Price: %.4f\n", mdata.CurrentPrice))
}
}
sb.WriteString("\n")
}
return sb.String()
}
// formatCandidateCoinsEN formats candidate coins (English)
func formatCandidateCoinsEN(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## Candidate Coins\n\n")
for i, coin := range ctx.CandidateCoins {
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("Current Price: %.4f\n\n", mdata.CurrentPrice))
if mdata.TimeframeData != nil {
sb.WriteString(formatKlineDataEN(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
}
}
}
if ctx.OITopDataMap != nil {
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("**OI Change**: Rank #%d | Change %+.2f%% (%+.2fM USDT) | Price Change %+.2f%%\n\n",
oiData.Rank,
oiData.OIDeltaPercent,
oiData.OIDeltaValue/1_000_000,
oiData.PriceDeltaPercent,
))
oiChange := "increase"
if oiData.OIDeltaPercent < 0 {
oiChange = "decrease"
}
priceChange := "up"
if oiData.PriceDeltaPercent < 0 {
priceChange = "down"
}
interpretation := getOIInterpretationEN(oiChange, priceChange)
sb.WriteString(fmt.Sprintf("**Market Interpretation**: %s\n\n", interpretation))
}
}
}
return sb.String()
}
// formatKlineDataEN formats kline data (English)
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder
// Sort timeframes for consistent output
sortedTF := make([]string, len(timeframes))
copy(sortedTF, timeframes)
sort.Strings(sortedTF)
for _, tf := range sortedTF {
if data, ok := tfData[tf]; ok && len(data.Klines) > 0 {
sb.WriteString(fmt.Sprintf("#### %s Timeframe (oldest → latest)\n\n", tf))
sb.WriteString("```\n")
sb.WriteString("Time(UTC) Open High Low Close Volume\n")
startIdx := 0
if len(data.Klines) > 30 {
startIdx = len(data.Klines) - 30
}
for i := startIdx; i < len(data.Klines); i++ {
k := data.Klines[i]
t := time.UnixMilli(k.Time).UTC()
sb.WriteString(fmt.Sprintf("%s %.4f %.4f %.4f %.4f %.2f\n",
t.Format("01-02 15:04"),
k.Open,
k.High,
k.Low,
k.Close,
k.Volume,
))
}
if len(data.Klines) > 0 {
sb.WriteString(" <- current\n")
}
sb.WriteString("```\n\n")
}
}
return sb.String()
}
// getOIInterpretationEN returns OI change interpretation (English)
func getOIInterpretationEN(oiChange, priceChange string) string {
if oiChange == "increase" && priceChange == "up" {
return OIInterpretation.OIUp_PriceUp.EN
} else if oiChange == "increase" && priceChange == "down" {
return OIInterpretation.OIUp_PriceDown.EN
} else if oiChange == "decrease" && priceChange == "up" {
return OIInterpretation.OIDown_PriceUp.EN
} else {
return OIInterpretation.OIDown_PriceDown.EN
}
}