Делаем результаты DataTable более читаемыми

У меня есть метод ниже, который генерирует результаты тестирования на истории. Однако это очень нечитабельно. Что вы могли бы мне предложить, чтобы сделать его более читабельным?

public DataTable GenerateResultTable(List<BacktestResult> data)
{
    var table = new DataTable();

    table.Columns.Add("Pair");
    table.Columns.Add("Buys");
    table.Columns.Add("Avg Profit %");
    table.Columns.Add("Cum Profit %");
    table.Columns.Add($"Tot Profit {_backtestOptions.StakeCurrency}");
    table.Columns.Add("Tot Profit %");
    table.Columns.Add("Avg Duration");
    table.Columns.Add("Wins");
    table.Columns.Add("Draws");
    table.Columns.Add("Losses");

    // the number of the pairs in the backtest results
    var maxOpenOrders = data.GroupBy(e => e.Pair).Count();
    
    // Split results for each pair
    var query = data.GroupBy(e => e.Pair).Select(e => new { Pair = e.Key, Count = e.Count(), Value = e });

    // in case there are no records to show
    // if there are no rows, it throws an exception..
    if (!query.Any())
    {
        table.Rows.Add("", "", "", "", "", "", "", "", "", "");
    }

    foreach (var result in query)
    {
        var _profitSum = result.Value.Sum(e => e.ProfitPercentage);
        var _profitTotal = _profitSum / maxOpenOrders;

        var key = result.Pair;
        var trades = result.Count;
        var profitMean = trades > 0 ? result.Value.Average(e => e.ProfitPercentage) : 0;
        var profitMeanPercentage = trades > 0 ? result.Value.Average(e => e.ProfitPercentage) * 100 : 0;
        var profitSum = _profitSum;
        var profitSumPercentage = _profitSum * 100;
        var profitTotalAbs = result.Value.Sum(e => e.ProfitAbs);
        var profitTotal = _profitTotal;
        var profitTotalPercentage = _profitTotal * 100;
        var avgDuration = trades > 0 ? Math.Round(result.Value.Average(e => e.TradeDuration)) : 0;
        var wins = result.Value.Count(e => e.ProfitAbs > 0);
        var draws = result.Value.Count(e => e.ProfitAbs == 0);
        var losses = result.Value.Count(e => e.ProfitAbs < 0);

        table.Rows.Add(key, trades, $"{profitMeanPercentage:f2}", $"{profitSumPercentage:f2}", $"{profitTotalAbs:f8}", $"{profitTotalPercentage:f2}",
            $"{TimeSpan.FromMinutes((double)avgDuration):h\:mm\:ss}", wins, draws, losses);
    }

    return table;
}

public class BacktestOptions
{
    public string StakeCurrency { get; set; }
    public List<string> Pairs { get; set; }
    public string StrategyName { get; set; }
    public decimal StakeAmount { get; set; }
    public decimal OpenFee { get; set; }
    public decimal CloseFee { get; set; }
}

public class BacktestResult
{
    public string Pair { get; set; }
    public decimal ProfitPercentage { get; set; }
    public decimal ProfitAbs { get; set; }
    public decimal OpenRate { get; set; }
    public decimal CloseRate { get; set; }
    public DateTime OpenDate { get; set; }
    public DateTime CloseDate { get; set; }
    public decimal OpenFee { get; set; }
    public decimal CloseFee { get; set; }
    public decimal Amount { get; set; }
    public decimal TradeDuration { get; set; }
    public SellType SellReason { get; set; }
}

appsettings.json

{
  "BacktestConfiguration": {
    "StakeCurrency": "USDT",
    "Pairs": [ "TRXUSDT" ],
    "StrategyName": "RsiStrategy",
    "StakeAmount": 1000,
    "OpenFee": 0.001,
    "CloseFee": 0.001
  }
}

1 ответ
1

Вот мои наблюдения:

  • List<BacktestResult> data: Пожалуйста, используйте более выразительные имена, чем data. Он слишком общий и не помогает читателю / сопровождающему кода. (то же самое относится к table и query)

  • Заполнение столбцов: в вашей реализации много повторений: tables.Columns.Add.

    • Один из способов уменьшить это, используя AddRange:
var table = new DataTable();
table.Columns.AddRange(new []
{
    new DataColumn("Pair"), 
    new DataColumn("Buys"), 
    new DataColumn("Avg Profit %"), 
    new DataColumn("Cum Profit %"), 
    new DataColumn($"Tot Profit {_backtestOptions.StakeCurrency}"), 
    new DataColumn("Tot Profit %"), 
    new DataColumn("Avg Duration"), 
    new DataColumn("Wins"), 
    new DataColumn("Draws"), 
    new DataColumn("Losses"), 
});

Мы почти у цели, здесь мы повторили new DataColumn. С помощью следующей техники вы можете разделить данные и логику:

var columns = new[]
{
    "Pair", "Buys", "Avg Profit %", "Cum Profit %", $"Tot Profit {_backtestOptions.StakeCurrency}",
    "Tot Profit %", "Avg Duration", "Wins", "Draws", "Losses"
};

table.Columns.AddRange(columns.Select(name => new DataColumn(name)).ToArray());
  • data.GroupBy(e => e.Pair): Это было вызвано дважды, но без необходимости.
    • Материализуйте это, вызвав ToList или ToArray в теме:
    • При таком подходе мы могли избавиться от maxOpenOrders переменная.
var resultsGroupByPair = data.GroupBy(e => e.Pair).ToArray();
var query = resultsGroupByPair.Select(br => new { Pair = br.Key, Count = br.Count(), Value = br });
//...
var _profitTotal = _profitSum / resultsGroupByPair.Length; 
  • query: То же самое применимо и здесь, Возможное множественное перечисление
    • Материализуйте это, вызвав ToList или ToArray в теме
  • if there are no rows, it throws an exception..: Этот комментарий не соответствует коду. Попробуйте прокомментировать Почему и Почему нетs, а не какая и Как. Последние два должны быть понятны в самом коде, но выбранная стратегия (и исключенные альтернативы) могут быть не очевидны из кода.
  • profitMean, profitSum, profitTotal: Они не используются. Если они не нужны, удалите их.
  • trades > 0 ?: Это условие повторялось несколько раз.
    • Введите локальную переменную и вместо нее обратитесь к ней: bool isTradesPositive
  • result.Value.Average, result.Value.Sum: То же, что и в предыдущем случае. Если вы используете одно и то же выражение несколько раз, сохраните его результат в переменной.
  • wins, draws, losses: Вместо этого сделайте 3 итерации, вы можете сделать это с помощью простого.
    • Например с Aggregate:
var (wins, losses, draws) = result.Value.Aggregate((0, 0, 0), (outcome, e) =>
{
    if (e.ProfitAbs > 0) outcome.Item1++;
    else if (e.ProfitAbs < 0) outcome.Item2++;
    else outcome.Item3++;
    return outcome;
});

или

var (wins, losses, draws) = result.Value.Aggregate((0, 0, 0), 
    ((int winCount, int loseCount, int drawCount) outcome, BacktestResult e) =>
    {
        if (e.ProfitAbs > 0) outcome.winCount++;
        else if (e.ProfitAbs < 0) outcome.loseCount++;
        else outcome.drawCount++;
        return outcome;
    });

  • Спасибо за исчерпывающий ответ! Второй пример с выигрышем, проигрышем, ничьей не работает из-за ((int winCount, int loseCount, int drawCount) outcome, BacktestResult e).

    — нет


  • @nop Не могли бы вы пояснить, что вы имеете в виду под не работает? Не компилируется? Выдает исключение? Возврат с тремя 0? или еще?

    — Питер Чала

  • Это исключения pastebin.com/EXnVhAtL. i.imgur.com/XUno1Jy.png

    — нет


  • 1

    Это из-за указанных типов аргументов. Понятия не имею, почему это так. Думаю, я буду придерживаться способа .Item1, 2, 3. Спасибо еще раз

    — нет


  • 1

    @aepot Спасибо, исправил в посте.

    — Питер Чала

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *