Skip to content

Commit 402f97b

Browse files
committed
Market trend adaptive PB configuration
1 parent 171eb97 commit 402f97b

21 files changed

+763
-63
lines changed

PbLayla.Tests/PbDori/PbDoriApiTest.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Net.Http;
45
using System.Net.Http.Headers;
56
using System.Text;
@@ -12,7 +13,7 @@ namespace PbLayla.Tests.PbDori;
1213

1314
public class PbDoriApiTest
1415
{
15-
[Fact]
16+
[Fact(Skip = "Manual test")]
1617
public async Task QueryStrategyApiResult()
1718
{
1819
HttpClient client = new HttpClient();
@@ -33,4 +34,19 @@ public async Task QueryStrategyApiResult()
3334
var response = JsonSerializer.Deserialize<StrategyApiResult>(responseContent);
3435
Assert.NotNull(response);
3536
}
37+
38+
[Fact(Skip = "Manual test")]
39+
public async Task QueryMarketTrend()
40+
{
41+
HttpClient client = new HttpClient();
42+
const string url = "";
43+
const string username = "";
44+
const string password = "";
45+
string query = "MarketTrend";
46+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")));
47+
string responseContent = await client.GetStringAsync($"{url}{query}");
48+
Assert.NotNull(responseContent);
49+
var response = JsonSerializer.Deserialize<MarketTrendApiResult>(responseContent);
50+
Assert.NotNull(response);
51+
}
3652
}

PbLayla/Configuration/Account.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@ public class Account
1616
public double OverExposeFilterFactor { get; set; } = 1.1;
1717
public string UnstuckConfig { get; set; } = string.Empty;
1818
public string DoriConfig { get; set; } = string.Empty;
19+
public string CautiousDoriConfig { get; set; } = string.Empty;
20+
public string CautiousUnstuckConfig { get; set; } = string.Empty;
1921
public double UnstuckExposure { get; set; } = 1.0;
22+
public double NormalPbStuckThreshold { get; set; } = 0.97;
23+
public double FastReducePbStuckThreshold { get; set; } = 0.3;
24+
public double NormalPbLossAllowance { get; set; } = 0.01;
25+
public double FastReducePbLossAllowance { get; set; } = 0.05;
2026
public bool DisableOthersWhileUnstucking { get; set; }
21-
public double PriceDistanceStuck { get; set; } = 0.05;
22-
public double PriceDistanceCloseHedge { get; set; } = 0.04;
23-
public double PriceDistanceUnstuckStuck { get; set; } = 0.1;
24-
public double PriceDistanceUnstuckCloseHedge { get; set; } = 0.09;
27+
public double PriceDistanceStuck { get; set; } = 0.12;
28+
public double PriceDistanceCloseHedge { get; set; } = 0.118;
29+
public double PriceDistanceUnstuckStuck { get; set; } = 0.2;
30+
public double PriceDistanceUnstuckCloseHedge { get; set; } = 0.198;
31+
public double CautiousDistanceStuck { get; set; } = 0.25;
32+
public double CautiousDistanceCloseHedge { get; set; } = 0.248;
33+
public double CautiousDistanceUnstuckStuck { get; set; } = 0.3;
34+
public double CautiousDistanceUnstuckCloseHedge { get; set; } = 0.298;
2535
public int MaxHedgeReleaseAttempts { get; set; } = 30;
2636
public TimeSpan MaxHedgeReleaseAttemptsPeriod { get; set; } = TimeSpan.FromHours(24);
2737
public int MaxUnstuckSymbols { get; set; } = 1;
@@ -41,5 +51,6 @@ public class Account
4151
public TimeSpan TransferProfitLookBack { get; set; } = TimeSpan.FromDays(3);
4252
public TimeSpan TransferProfitLogHistory { get; set; } = TimeSpan.FromDays(30);
4353
public PbVersion PbVersion { get; set; } = PbVersion.V610;
54+
public bool MarketTrendAdaptive { get; set; }
4455
}
4556
}

PbLayla/Helpers/PbMultiConfigExtensions.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json;
2+
using CryptoExchange.Net.CommonObjects;
23
using Hjson;
34
using Microsoft.Extensions.Logging;
45
using PbLayla.Model.Dori;
@@ -15,6 +16,95 @@ public static string GenerateNormalConfig(this IPbMultiConfig template)
1516
return serializedConfig;
1617
}
1718

19+
public static string GenerateNormalAdaptiveTrendConfig(this IPbMultiConfig template,
20+
string normalConfig,
21+
string unstuckConfig,
22+
double pbStuckThreshold,
23+
double pbLossAllowance)
24+
{
25+
var config = template.Clone();
26+
config.LossAllowancePct = pbLossAllowance;
27+
config.StuckThreshold = pbStuckThreshold;
28+
var symbols = config.ParseSymbols();
29+
foreach (var symbol in symbols)
30+
{
31+
if (symbol.LongMode == TradeMode.Normal)
32+
{
33+
// continue trading with normal config
34+
symbol.LiveConfigPath = FormattableString.Invariant($"configs/{normalConfig}");
35+
}
36+
else if (symbol.LongMode == TradeMode.GracefulStop)
37+
{
38+
// Dori wants to exit this symbol
39+
symbol.LiveConfigPath = FormattableString.Invariant($"configs/{unstuckConfig}");
40+
}
41+
}
42+
config.UpdateSymbols(symbols);
43+
44+
var serializedConfig = config.SerializeConfig();
45+
return serializedConfig;
46+
}
47+
48+
public static string GenerateUnstuckAdaptiveTrendConfig(this IPbMultiConfig template,
49+
HashSet<string> symbolsToUnstuck,
50+
string normalConfig,
51+
string unstuckConfig,
52+
double pbStuckThreshold,
53+
double pbLossAllowance,
54+
double unstuckExposure,
55+
bool disableOthers)
56+
{
57+
var config = template.Clone();
58+
config.LossAllowancePct = pbLossAllowance;
59+
config.StuckThreshold = pbStuckThreshold;
60+
var symbols = config.ParseSymbols();
61+
var foundSymbols = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
62+
foreach (var symbolConfig in symbols)
63+
{
64+
if (symbolsToUnstuck.Contains(symbolConfig.Symbol))
65+
{
66+
foundSymbols.Add(symbolConfig.Symbol);
67+
symbolConfig.LongMode = TradeMode.GracefulStop;
68+
symbolConfig.LiveConfigPath = FormattableString.Invariant($"configs/{unstuckConfig}");
69+
symbolConfig.WalletExposureLimitLong = unstuckExposure;
70+
}
71+
else
72+
{
73+
if (disableOthers && symbolConfig.LongMode is TradeMode.GracefulStop or TradeMode.Normal)
74+
{
75+
symbolConfig.LongMode = TradeMode.TakeProfitOnly;
76+
symbolConfig.LiveConfigPath = FormattableString.Invariant($"configs/{unstuckConfig}");
77+
}
78+
else if (symbolConfig.LongMode == TradeMode.Normal)
79+
{
80+
symbolConfig.LiveConfigPath = FormattableString.Invariant($"configs/{normalConfig}");
81+
}
82+
}
83+
}
84+
85+
// add stuck symbols that we have not found in config
86+
foreach (var symbolToUnstuck in symbolsToUnstuck)
87+
{
88+
if (!foundSymbols.Contains(symbolToUnstuck))
89+
{
90+
var newSymbol = new SymbolOptions
91+
{
92+
LongMode = TradeMode.GracefulStop,
93+
LiveConfigPath = FormattableString.Invariant($"configs/{unstuckConfig}"),
94+
Symbol = symbolToUnstuck,
95+
WalletExposureLimitLong = unstuckExposure,
96+
ShortMode = TradeMode.Manual,
97+
};
98+
symbols = symbols.Append(newSymbol).ToArray();
99+
}
100+
}
101+
102+
config.UpdateSymbols(symbols);
103+
104+
var serializedConfig = config.SerializeConfig();
105+
return serializedConfig;
106+
}
107+
18108
public static string GenerateUnstuckConfig(this IPbMultiConfig template, HashSet<string> symbolsToUnstuck, string unstuckConfig, double unstuckExposure, bool disableOthers)
19109
{
20110
var config = template.Clone();

PbLayla/Model/Dori/MarketTrend.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace PbLayla.Model.Dori;
4+
5+
public class MarketTrend
6+
{
7+
[JsonPropertyName("last_updated")]
8+
public DateTime LastUpdated { get; set; }
9+
[JsonPropertyName("global_trend")]
10+
public Trend GlobalTrend { get; set; }
11+
[JsonPropertyName("bullish_count")]
12+
public int BullishCount { get; set; }
13+
[JsonPropertyName("bearish_count")]
14+
public int BearishCount { get; set; }
15+
[JsonPropertyName("unknown_count")]
16+
public int UnknownCount { get; set; }
17+
[JsonPropertyName("symbol_trends")]
18+
public SymbolTrend[] SymbolTrends { get; set; } = [];
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace PbLayla.Model.Dori;
4+
5+
public class MarketTrendApiResult
6+
{
7+
[JsonPropertyName("market_trend")]
8+
public MarketTrend? MarketTrend { get; set; }
9+
[JsonPropertyName("data_available")]
10+
public bool DataAvailable { get; set; }
11+
}

PbLayla/Model/Dori/SymbolTrend.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace PbLayla.Model.Dori;
4+
5+
public class SymbolTrend
6+
{
7+
[JsonPropertyName("symbol")]
8+
public string Symbol { get; set; } = string.Empty;
9+
[JsonPropertyName("trend")]
10+
public Trend Trend { get; set; }
11+
}

PbLayla/Model/Dori/Trend.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace PbLayla.Model.Dori;
2+
3+
public enum Trend
4+
{
5+
Unknown = 0,
6+
Bullish = 1,
7+
Bearish = 2,
8+
}

PbLayla/Model/PbConfig/IPbMultiConfig.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
public interface IPbMultiConfig
44
{
5+
double StuckThreshold { get; set; }
6+
7+
double LossAllowancePct { get; set; }
8+
59
SymbolOptions[] ParseSymbols();
610

711
int GetSymbolCount();

PbLayla/Processing/AccountState.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
public enum AccountState
44
{
5+
// non adaptive states
6+
57
/// <summary>
68
/// No risk state
79
/// </summary>
@@ -18,5 +20,30 @@ public enum AccountState
1820
/// Other existing positions will have close orders set at take profit distance to try to reduce the total wallet exposure.
1921
/// Other existing positions will not add up any more exposure at this stage.
2022
/// </summary>
21-
StageOneStuck
23+
StageOneStuck,
24+
25+
// adaptive states based on Dori trend
26+
27+
/// <summary>
28+
/// Use normal configuration
29+
/// </summary>
30+
AdaptiveNormal,
31+
/// <summary>
32+
/// Use normal configuration with unstuck
33+
/// </summary>
34+
AdaptiveNormalStuck,
35+
/// <summary>
36+
/// Use cautious configuration with wider grid
37+
/// </summary>
38+
AdaptiveCautious,
39+
/// <summary>
40+
/// Use cautious configuration with wider grid and unstuck
41+
/// </summary>
42+
AdaptiveCautiousStuck,
43+
/// <summary>
44+
/// Use cautious configuration with wider grid and reduce position size faster
45+
/// There are probably some stuck positions for normal config or already over exposed account
46+
/// We don't want to play with any extra wallet exposure and reduce positions as fast as possible
47+
/// </summary>
48+
AdaptiveCautiousFastReduce,
2249
}

PbLayla/Processing/Dori/DoriService.cs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.Options;
66
using PbLayla.Model.Dori;
77
using PbLayla.Model.PbConfig;
8+
using PbLayla.Repositories;
89

910
namespace PbLayla.Processing.Dori;
1011

@@ -13,10 +14,12 @@ public class DoriService : IDoriService
1314
private readonly IOptions<DoriServiceOptions> m_options;
1415
private readonly ConcurrentDictionary<string, DoriQuery> m_queries;
1516
private readonly ConcurrentDictionary<string, StrategyApiResult> m_results;
17+
private readonly IMarketTrendRepository m_marketTrendRepository;
1618

17-
public DoriService(IOptions<DoriServiceOptions> options)
19+
public DoriService(IOptions<DoriServiceOptions> options, IMarketTrendRepository marketTrendRepository)
1820
{
1921
m_options = options;
22+
m_marketTrendRepository = marketTrendRepository;
2023
m_queries = new ConcurrentDictionary<string, DoriQuery>();
2124
m_results = new ConcurrentDictionary<string, StrategyApiResult>();
2225
}
@@ -63,11 +66,8 @@ public async Task<bool> QueryDoriAsync(string strategyName, CancellationToken ca
6366
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{m_options.Value.Username}:{m_options.Value.Password}")));
6467
}
6568

66-
string url = m_options.Value.Url;
67-
if (string.IsNullOrEmpty(url))
69+
if (!GetNormalizedUrl(out var url))
6870
return false;
69-
if (!url.EndsWith('/'))
70-
url += '/';
7171
string fullUrl = FormattableString.Invariant($"{url}{formattedQuery}");
7272
string responseContent = await client.GetStringAsync(fullUrl, cancel);
7373
if (string.IsNullOrEmpty(responseContent))
@@ -83,10 +83,51 @@ public async Task<bool> QueryDoriAsync(string strategyName, CancellationToken ca
8383
return true;
8484
}
8585

86+
private bool GetNormalizedUrl(out string url)
87+
{
88+
url = m_options.Value.Url;
89+
if (string.IsNullOrEmpty(url))
90+
return false;
91+
if (!url.EndsWith('/'))
92+
url += '/';
93+
return true;
94+
}
95+
96+
public async Task<bool> QueryDoriMarketTrendAsync(CancellationToken cancel = default)
97+
{
98+
using var client = new HttpClient();
99+
if (!string.IsNullOrEmpty(m_options.Value.Username) && !string.IsNullOrEmpty(m_options.Value.Password))
100+
{
101+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
102+
"Basic",
103+
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{m_options.Value.Username}:{m_options.Value.Password}")));
104+
}
105+
string query = "MarketTrend";
106+
if (!GetNormalizedUrl(out var url))
107+
return false;
108+
string fullUrl = FormattableString.Invariant($"{url}{query}");
109+
string responseContent = await client.GetStringAsync(fullUrl, cancel);
110+
if (string.IsNullOrEmpty(responseContent))
111+
return false;
112+
var response = JsonSerializer.Deserialize<MarketTrendApiResult>(responseContent);
113+
if (response == null)
114+
return false;
115+
if (!response.DataAvailable || response.MarketTrend == null)
116+
return false;
117+
await m_marketTrendRepository.SaveMarketTrendAsync(response.MarketTrend, cancel);
118+
return true;
119+
}
120+
86121
public Task<StrategyApiResult?> TryGetDoriStrategyAsync(string strategyName, CancellationToken cancel = default)
87122
{
88123
if (m_results.TryGetValue(strategyName, out var result))
89124
return Task.FromResult<StrategyApiResult?>(result);
90125
return Task.FromResult<StrategyApiResult?>(null);
91126
}
127+
128+
public async Task<MarketTrend?> TryGetMarketTrendAsync(CancellationToken cancel = default)
129+
{
130+
var marketTrend = await m_marketTrendRepository.TryLoadMarketTrendAsync(cancel);
131+
return marketTrend;
132+
}
92133
}

0 commit comments

Comments
 (0)